diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..461646baa33 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,350 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_visual_guides = none +ij_wrap_on_typing = false + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = none +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_while_brace_force = never +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = never +ij_java_imports_layout = *,|,javax.**,java.**,|,$* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_at_first_column = true +ij_java_message_dd_suffix = EJB +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_new_line_after_lparen_in_record_header = false +ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_java_parameter_annotation_wrap = off +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_record_header = false +ij_java_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdd,*.wsdl,*.xjb,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal +ij_xml_use_custom_settings = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}] +indent_size = 2 +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 + +[{*.properties,spring.handlers,spring.schemas}] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index d26d7485344..4cc56e55058 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - java: [8] + java: [8, 11] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index f5d07b2639d..b34d4827eae 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -14,7 +14,7 @@ jobs: - name: Extract tag from commit message run: | target_tag=${COMMIT_MSG#"Prebid Server prepare release "} - echo "::set-env name=TARGET_TAG::$target_tag" + echo "TARGET_TAG=$target_tag" >> $GITHUB_ENV env: COMMIT_MSG: ${{ github.event.head_commit.message }} - name: Create and publish release diff --git a/README.md b/README.md index e7edc4aca48..c4f9d91d0fa 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,28 @@ -### This code is being used in production by multiple Prebid.org members, but is not the "official" version. See https://github.com/prebid/prebid-server/ +### This is the Java version of Prebid Server. See the Prebid Server [Feature List](https://docs.prebid.org/prebid-server/features/pbs-feature-idx.html) and [FAQ entry](https://docs.prebid.org/faq/prebid-server-faq.html#why-are-there-two-versions-of-prebid-server-are-they-kept-in-sync) to understand the differences between PBS-Java and [PBS-Go](https://github.com/prebid/prebid-server). -# Prebid Server +# Prebid Server (Java) -[![GitHub version](https://badge.fury.io/gh/rubicon-project%2fprebid-server-java.svg)](http://badge.fury.io/gh/rubicon-project%2fprebid-server-java) -[![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/rubicon-project/prebid-server-java.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/rubicon-project/prebid-server-java/context:java) -[![Total alerts](https://img.shields.io/lgtm/alerts/g/rubicon-project/prebid-server-java.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/rubicon-project/prebid-server-java/alerts/) -[![GitHub contributors](https://img.shields.io/github/contributors/rubicon-project/prebid-server-java.svg)](https://GitHub.com/rubicon-project/prebid-server-java/contributors/) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/rubicon-project/prebid-server-java/blob/master/docs/contributing.md) -[![GitHub pull-requests closed](https://img.shields.io/github/issues-pr-closed/rubicon-project/prebid-server-java.svg)](https://GitHub.com/rubicon-project/prebid-server-java/pull/) +[![GitHub version](https://badge.fury.io/gh/prebid%2fprebid-server-java.svg)](http://badge.fury.io/gh/prebid%2fprebid-server-java) +[![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/prebid/prebid-server-java.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/prebid/prebid-server-java/context:java) +[![Total alerts](https://img.shields.io/lgtm/alerts/g/prebid/prebid-server-java.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/prebid/prebid-server-java/alerts/) +[![GitHub contributors](https://img.shields.io/github/contributors/prebid/prebid-server-java.svg)](https://GitHub.com/prebid/prebid-server-java/contributors/) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/prebid/prebid-server-java/blob/master/docs/contributing.md) +[![GitHub pull-requests closed](https://img.shields.io/github/issues-pr-closed/prebid/prebid-server-java.svg)](https://GitHub.com/prebid/prebid-server-java/pull/) Prebid Server is an open source implementation of Server-Side Header Bidding. -It is managed by [Prebid.org](http://prebid.org/overview/what-is-prebid-org.html), -and upholds the principles from the [Prebid Code of Conduct](http://prebid.org/wrapper_code_of_conduct.html). +It is managed by Prebid.org, +and upholds the principles from the [Prebid Code of Conduct](https://prebid.org/wrapper_code_of_conduct.html). This project does not support the same set of Bidders as Prebid.js, although there is overlap. The current set can be found in the [adapters](./src/main/java/org/prebid/server/bidder) package. If you don't see the one you want, feel free to [contribute it](docs/developers/add-new-bidder.md). For more information, see: -- [What is Prebid?](http://prebid.org/overview/intro.html) -- [Getting started with Prebid Server](http://prebid.org/dev-docs/get-started-with-prebid-server.html) -- [Current Bidders](http://prebid.org/dev-docs/prebid-server-bidders.html) +- [What is Prebid?](https://prebid.org/why-prebid/) +- [Getting started with Prebid Server](https://docs.prebid.org/prebid-server/overview/prebid-server-overview.html) +- [Current Bidders](https://docs.prebid.org/dev-docs/pbs-bidders.html) + +Please consider [registering your Prebid Server](https://docs.prebid.org/prebid-server/hosting/pbs-hosting.html#optional-registration) to get on the mailing list for updates, etc. # Getting Started @@ -28,23 +30,21 @@ The server makes the following assumptions: - No ranking or decisioning is performed by this server. It just proxies requests. - No ad quality management (e.g., malware, viruses, deceptive creatives) is performed by this server. - This server does no fraud scanning and does nothing to prevent bad traffic. -- This server does no logging. -- This server has not user profiling or user data collection capabilities. +- This server logs errors but not requests. +- This server has no user profiling or user data collection capabilities. This project is built upon [Vert.x](http://vertx.io) to achieve high request throughput. We use [Maven](https://maven.apache.org) and attempt to introduce minimal dependencies. When running, the server responds to several HTTP [endpoints](docs/endpoints). -To start the Prebid Server you need to do the following steps: - ## Building Follow next steps to create JAR file which can be deployed locally. - Download or clone a project: ```bash -git clone https://github.com/rubicon-project/prebid-server-java.git +git clone https://github.com/prebid/prebid-server-java.git ``` - Move to project directory: @@ -84,28 +84,27 @@ and verify response status is `200 OK`. # Documentation ## Development -- [Differences Between Prebid Server Go and Java](differenceBetweenPBSGo-and-Java.md) -- [Endpoints](endpoints) -- [Adding new bidder](developers/add-new-bidder.md) -- [Adding new analytics module](developers/add-new-analytics-module.md) -- [Adding viewability support](developers/add-viewability-vendors.md) -- [Auction result post-processing](developers/auction-result-post-processing.md) -- [Cookie Syncs](developers/cookie-syncs.md) -- [Stored Requests](developers/stored-requests.md) -- [Unit Tests](developers/unit-tests.md) -- [GDPR](developers/gdpr.md) +- [Endpoints](docs/endpoints) +- [Adding new bidder](docs/developers/add-new-bidder.md) +- [Adding new analytics module](docs/developers/add-new-analytics-module.md) +- [Adding viewability support](docs/developers/add-viewability-vendors.md) +- [Auction result post-processing](docs/developers/auction-result-post-processing.md) +- [Cookie Syncs](docs/developers/cookie-syncs.md) +- [Stored Requests](docs/developers/stored-requests.md) +- [Unit Tests](docs/developers/unit-tests.md) +- [GDPR](docs/developers/gdpr.md) ## Maintenance -- [Build for local](build.md) -- [Build for AWS](build-aws.md) -- [Configure application](config.md) - - [Full list of configuration options](config-app.md) - - [Application settings](application-settings.md) -- [Run with optimizations](run.md) -- [Metrics](metrics.md) +- [Build for local](docs/build.md) +- [Build for AWS](docs/build-aws.md) +- [Configure application](docs/config.md) + - [Full list of configuration options](docs/config-app.md) + - [Application settings](docs/application-settings.md) +- [Run with optimizations](docs/run.md) +- [Metrics](docs/metrics.md) ## Contributing -- [Contributing](contributing.md) -- [Code Style](code-style.md) -- [Code Review](code-reviews.md) -- [Versioning](versioning.md) +- [Contributing](docs/contributing.md) +- [Code Style](docs/code-style.md) +- [Code Review](docs/code-reviews.md) +- [Versioning](docs/versioning.md) diff --git a/checkstyle.xml b/checkstyle.xml index 86235beeacf..64f253767e3 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -44,7 +44,7 @@ - + diff --git a/docs/application-settings.md b/docs/application-settings.md index 178ae5c9cc2..a12b46d3ad4 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -1,28 +1,52 @@ # Application Settings + There are two ways to configure application settings: database and file. This document will describe both approaches. ## Account properties + - `id` - identifies publisher account. -- `price-granularity` - defines price granularity types: 'low','med','high','auto','dense','unknown'. -- `banner-cache-ttl` - how long (in seconds) banner will be available via the external Cache Service. -- `video-cache-ttl`- how long (in seconds) video creative will be available via the external Cache Service. -- `events-enabled` - enables events for account if true -- `enforce-ccpa` - enforces ccpa if true. Has higher priority than configuration in application.yaml. -- `gdpr.enabled` - enables gdpr verifications if true. Has higher priority than configuration in application.yaml. -- `gdpr.integration-enabled.web` - overrides `gdpr.enabled` property behaviour for web requests type. -- `gdpr.integration-enabled.amp` - overrides `gdpr.enabled` property behaviour for amp requests type. -- `gdpr.integration-enabled.app` - overrides `gdpr.enabled` property behaviour for app requests type. -- `gdpr.integration-enabled.video` - overrides `gdpr.enabled` property behaviour for video requests type. -- `gdpr.purposes.[p1-p10].enforce-purpose` - define type of enforcement confirmation: `no`/`basic`/`full`. Default `full` -- `gdpr.purposes.[p1-p10].enforce-vendors` - if equals to `true`, user must give consent to use vendors. Purposes will be omitted. Default `true` -- `gdpr.purposes.[p1-p10].vendor-exceptions[]` - bidder names that will be treated opposite to `pN.enforce-vendors` value. -- `gdpr.special-features.[f1-f2].enforce`- if equals to `true`, special feature will be enforced for purpose. Default `true` -- `gdpr.special-features.[f1-f2].vendor-exceptions` - bidder names that will be treated opposite to `sfN.enforce` value. -- `gdpr.purpose-one-treatment-interpretation` - option that allows to skip the Purpose one enforcement workflow. Values: ignore, no-access-allowed, access-allowed. -- `analytics-sampling-factor` - Analytics sampling factor value. -- `truncate-target-attr` - Maximum targeting attributes size. Values between 1 and 255. -- `default-integration` - Default integration to assume. -- `analytics-config.auction-events.` - defines which channels are supported by analytics for this account +- `status` - allows to mark account as `active` or `inactive`. +- `auction.price-granularity` - defines price granularity types: 'low','med','high','auto','dense','unknown'. +- `auction.banner-cache-ttl` - how long (in seconds) banner will be available via the external Cache Service. +- `auction.video-cache-ttl`- how long (in seconds) video creative will be available via the external Cache Service. +- `auction.truncate-target-attr` - Maximum targeting attributes size. Values between 1 and 255. +- `auction.default-integration` - Default integration to assume. +- `auction.bid-validations.banner-creative-max-size` - Overrides creative max size validation for banners. Valid values + are: + - "skip": don't do anything about creative max size for this publisher + - "warn": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, log an + operational warning. + - "enforce": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, reject + the bid and log an operational warning. +- `auction.events.enabled` - enables events for account if true +- `privacy.enforce-ccpa` - enforces ccpa if true. Has higher priority than configuration in application.yaml. +- `privacy.gdpr.enabled` - enables gdpr verifications if true. Has higher priority than configuration in + application.yaml. +- `privacy.gdpr.integration-enabled.web` - overrides `privacy.gdpr.enabled` property behaviour for web requests type. +- `privacy.gdpr.integration-enabled.amp` - overrides `privacy.gdpr.enabled` property behaviour for amp requests type. +- `privacy.gdpr.integration-enabled.app` - overrides `privacy.gdpr.enabled` property behaviour for app requests type. +- `privacy.gdpr.integration-enabled.video` - overrides `privacy.gdpr.enabled` property behaviour for video requests + type. +- `privacy.gdpr.purposes.[p1-p10].enforce-purpose` - define type of enforcement confirmation: `no`/`basic`/`full`. + Default `full` +- `privacy.gdpr.purposes.[p1-p10].enforce-vendors` - if equals to `true`, user must give consent to use vendors. + Purposes will be omitted. Default `true` +- `privacy.gdpr.purposes.[p1-p10].vendor-exceptions[]` - bidder names that will be treated opposite + to `pN.enforce-vendors` value. +- `privacy.gdpr.special-features.[f1-f2].enforce`- if equals to `true`, special feature will be enforced for purpose. + Default `true` +- `privacy.gdpr.special-features.[f1-f2].vendor-exceptions` - bidder names that will be treated opposite + to `sfN.enforce` value. +- `privacy.gdpr.purpose-one-treatment-interpretation` - option that allows to skip the Purpose one enforcement workflow. + Values: ignore, no-access-allowed, access-allowed. +- `analytics.auction-events.` - defines which channels are supported by analytics for this account +- `analytics.modules..*` - space for `module-name` analytics module specific configuration, may be of any shape +- `cookie-sync.default-limit` - if the "limit" isn't specified in the `/cookie_sync` request, this is what to use +- `cookie-sync.max-limit` - if the "limit" is specified in the `/cookie_sync` request, it can't be greater than this + value +- `cookie-sync.default-coop-sync` - if the "coopSync" value isn't specified in the `/cookie_sync` request, use this + +Here are the definitions of the "purposes" that can be defined in the GDPR setting configurations: ``` Purpose | Purpose goal | Purpose meaning for PBS (n\a - not affected) @@ -30,7 +54,7 @@ Purpose | Purpose goal | Purpose meaning for PBS (n\a - not p1 | Access device | Stops usersync for given vendor and stops settings cookie on `/seuid` p2 | Select basic ads | Verify consent for each vendor as appropriate for the enforcement method before calling a bid adapter. If consent is not granted, log a metric and skip it. p3 | Personalized ads profile | n\a -p4 | Select personalized ads | Verify consent for each vendor that passed the Purpose 2. If consent is not granted, remove the bidrequest.userId, user.ext.eids, user.ext.digitrust, device.if attributes and call the adapter. +p4 | Select personalized ads | Verify consent for each vendor that passed the Purpose 2. If consent is not granted, remove the bidrequest.userId, user.ext.eids, device.if attributes and call the adapter. p5 | Personalized content profile | n\a p6 | Select personalized content | n\a p7 | Measure ad performance | Verify consent for each analytics module. If consent is not grantet skip it. @@ -42,277 +66,382 @@ sf1 | Precise geo | Verifies user opt-in. If the user sf2 | Fingerprinting | n\a ``` -## File application setting +## Setting Account Configuration in Files In file based approach all configuration stores in .yaml files, path to which are defined in application properties. ### Configuration in application.yaml -``` +The general idea is that you'll place all the account-specific settings in a separate YAML file and point to that file. + +```yaml settings: filesystem: settings-filename: ``` + ### File format +Here's an example YAML file containing account-specific settings: + +```yaml + accounts: + - id: 1111 + status: active + auction: + price-granularity: low + banner-cache-ttl: 100 + video-cache-ttl: 100 + truncate-target-attr: 40 + default-integration: web + bid-validations: + banner-creative-max-size: enforce + events: + enabled: true + privacy: + enforce-ccpa: true + gdpr: + enabled: true + integration-enabled: + video: true + web: true + app: true + amp: true + purposes: + p1: + enforce-purpose: full + enforce-vendors: true + vendor-exceptions: + - bidder1 + - bidder2 + p2: + enforce-purpose: full + enforce-vendors: true + vendor-exceptions: + - bidder1 + - bidder2 + p3: + enforce-purpose: full + enforce-vendors: true + vendor-exceptions: + - bidder1 + - bidder2 + p4: + enforce-purpose: full + enforce-vendors: true + vendor-exceptions: + - bidder1 + - bidder2 + p5: + enforce-purpose: full + enforce-vendors: true + vendor-exceptions: + - bidder1 + - bidder2 + p6: + enforce-purpose: full + enforce-vendors: true + vendor-exceptions: + - bidder1 + - bidder2 + p7: + enforce-purpose: full + enforce-vendors: true + vendor-exceptions: + - bidder1 + - bidder2 + p8: + enforce-purpose: full + enforce-vendors: true + vendor-exceptions: + - bidder1 + - bidder2 + p9: + enforce-purpose: full + enforce-vendors: true + vendor-exceptions: + - bidder1 + - bidder2 + p10: + enforce-purpose: full + enforce-vendors: true + vendor-exceptions: + - bidder1 + - bidder2 + special-features: + sf1: + enforce: true + vendor-exceptions: + - bidder1 + - bidder2 + sf2: + enforce: true + vendor-exceptions: + - bidder1 + - bidder2 + purpose-one-treatment-interpretation: ignore + analytics: + auction-events: + amp: true + cookie-sync: + default-limit: 5 + max-limit: 8 + default-coop-sync: true ``` -accounts: - - id: 14062 - bannerCacheTtl: 100 - videoCacheTtl: 100 - eventsEnabled: true - priceGranularity: low - enforceCcpa: true - analyticsSamplingFactor: 1 - truncateTargetAttr: 40 - defaultIntegration: web - analytics-config: - auction-events: - amp: true - gdpr: - enabled: true - integration-enabled: - video: true - web: true - app: true - amp: true - purposes: - p1: - enforce-purpose: full - enforce-vendors: true - vendor-exceptions: - - bidder1 - - bidder2 - p2: - enforce-purpose: full - enforce-vendors: true - vendor-exceptions: - - bidder1 - - bidder2 - p3: - enforce-purpose: full - enforce-vendors: true - vendor-exceptions: - - bidder1 - - bidder2 - p4: - enforce-purpose: full - enforce-vendors: true - vendor-exceptions: - - bidder1 - - bidder2 - p5: - enforce-purpose: full - enforce-vendors: true - vendor-exceptions: - - bidder1 - - bidder2 - p6: - enforce-purpose: full - enforce-vendors: true - vendor-exceptions: - - bidder1 - - bidder2 - p7: - enforce-purpose: full - enforce-vendors: true - vendor-exceptions: - - bidder1 - - bidder2 - p8: - enforce-purpose: full - enforce-vendors: true - vendor-exceptions: - - bidder1 - - bidder2 - p9: - enforce-purpose: full - enforce-vendors: true - vendor-exceptions: - - bidder1 - - bidder2 - p10: - enforce-purpose: full - enforce-vendors: true - vendor-exceptions: - - bidder1 - - bidder2 - special-features: - sf1: - enforce: true - vendor-exceptions: - - bidder1 - - bidder2 - sf2: - enforce: true - vendor-exceptions: - - bidder1 - - bidder2 - purpose-one-treatment-interpretation: ignore -``` - -## Database application setting +## Setting Account Configuration in the Database + +In database approach account properties are stored in database table(s). -In database approach account properties are stored in database table with name accounts_account. +SQL query for retrieving account is configurable and can be specified in [application configuration](config-app.md). +Requirements for the SQL query stated below. ### Configuration in application.yaml -``` + +```yaml settings: database: - type: pool-size: 20 - type: mysql + type: host: port: + account-query: ``` -### Table description +### Configurable SQL query for account requirements -Query to create accounts_account table: +The general approach is that each host company can set up their database however they wish, so long as the configurable +query run by Prebid Server returns expected data in the expected order. Here's an example configuration: -``` -'CREATE TABLE `accounts_account` ( -`id` int(10) unsigned NOT NULL AUTO_INCREMENT, -`uuid` varchar(40) NOT NULL, -`price_granularity` enum('low','med','high','auto','dense','unknown') NOT NULL DEFAULT 'unknown', -`granularityMultiplier` decimal(9,3) DEFAULT NULL, -`banner_cache_ttl` int(11) DEFAULT NULL, -`video_cache_ttl` int(11) DEFAULT NULL, -`events_enabled` bit(1) DEFAULT NULL, -`enforce_ccpa` bit(1) DEFAULT NULL, -`enforce_gdpr` bit(1) DEFAULT NULL, -`tcf_config` json DEFAULT NULL, -`analytics_sampling_factor` tinyint(4) DEFAULT NULL, -`truncate_target_attr` tinyint(3) unsigned DEFAULT NULL, -`default_integration` varchar(64) DEFAULT NULL, -`analytics_config` varchar(512) DEFAULT NULL, -`status` enum('active','inactive') DEFAULT 'active', -`updated_by` int(11) DEFAULT NULL, -`updated_by_user` varchar(64) DEFAULT NULL, -`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -PRIMARY KEY (`id`), -UNIQUE KEY `uuid` (`uuid`)) -ENGINE=InnoDB DEFAULT CHARSET=utf8' +```yaml +settings: + database: + type: mysql + account-query: SELECT config FROM accounts_account where uuid = ? LIMIT 1 ``` -where tcf_config column is json with next format +The SQL query for account must: -``` +* return following columns, with specified type, in this order: + * configuration document, JSON string, see below +* specify a special single `%ACCOUNT_ID%` placeholder in the `WHERE` clause that will be replaced with account ID in + runtime + +It is recommended to include `LIMIT 1` clause in the query because only the very first result returned will be taken. + +### Configuration document JSON + +The configuration document JSON returned by the SQL query must conform to the format illustrated with the following +example: + +```json { - "enabled": true, - "integration-enabled": { - "video": true, - "web": true, - "app": true, - "amp": true - } - "purpose-one-treatment-interpretation": "ignore" - "purposes": { - "p1": { - "enforce-purpose": "full", - "enforce-vendors": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] - }, - "p2": { - "enforce-purpose": "full", - "enforce-vendors": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] - }, - "p3": { - "enforce-purpose": "full", - "enforce-vendors": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] - }, - "p4": { - "enforce-purpose": "full", - "enforce-vendors": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] - }, - "p5": { - "enforce-purpose": "full", - "enforce-vendors": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] + "id": "1111", + "status": "active", + "auction": { + "price-granularity": "low", + "banner-cache-ttl": 100, + "video-cache-ttl": 100, + "truncate-target-attr": 40, + "default-integration": "web", + "bid-validations": { + "banner-creative-max-size": "enforce" }, - "p6": { - "enforce-purpose": "full", - "enforce-vendors": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] - }, - "p7": { - "enforce-purpose": "full", - "enforce-vendors": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] - }, - "p8": { - "enforce-purpose": "full", - "enforce-vendors": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] - }, - "p9": { - "enforce-purpose": "full", - "enforce-vendors": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] - }, - "p10": { - "enforce-purpose": "full", - "enforce-vendors": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] + "events": { + "enabled": true } }, - "special-features": { - "sf1": { - "enforce": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] - }, - "sf2": { - "enforce": true, - "vendor-exceptions": [ - "bidder1", - "bidder2" - ] + "privacy": { + "enforce-ccpa": true, + "gdpr": { + "enabled": true, + "integration-enabled": { + "video": true, + "web": true, + "app": true, + "amp": true + }, + "purpose-one-treatment-interpretation": "ignore", + "purposes": { + "p1": { + "enforce-purpose": "full", + "enforce-vendors": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + }, + "p2": { + "enforce-purpose": "full", + "enforce-vendors": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + }, + "p3": { + "enforce-purpose": "full", + "enforce-vendors": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + }, + "p4": { + "enforce-purpose": "full", + "enforce-vendors": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + }, + "p5": { + "enforce-purpose": "full", + "enforce-vendors": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + }, + "p6": { + "enforce-purpose": "full", + "enforce-vendors": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + }, + "p7": { + "enforce-purpose": "full", + "enforce-vendors": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + }, + "p8": { + "enforce-purpose": "full", + "enforce-vendors": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + }, + "p9": { + "enforce-purpose": "full", + "enforce-vendors": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + }, + "p10": { + "enforce-purpose": "full", + "enforce-vendors": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + } + }, + "special-features": { + "sf1": { + "enforce": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + }, + "sf2": { + "enforce": true, + "vendor-exceptions": [ + "bidder1", + "bidder2" + ] + } + } } + }, + "analytics": { + "auction-events": { + // the analytics adapter should log auction events when the channel is web + "web": true, + // the analytics adapter should log auction events when the channel is AMP + "amp": true, + // the analytics adapter should not log auction events when the channel is app + "app": false + } + }, + "cookie-sync": { + "default-limit": 5, + "max-limit": 8, + "default-coop-sync": true } } ``` -Query used to get an account: +At some point this format might be formalized into an +exhaustive [JSON Schema](https://json-schema.org/specification.html). + +#### SQL Query example + +Prebid Server does not impose any rules for the table(s) schema but requires SQL query specified in +configuration to return a single column of JSON type containing the document adhering to the format shown above. + +It might be the case that the host companies store account configuration in a table of arbitrary structure or even in +several tables. MySQL and Postgres provides necessary functions allowing to project practically any table structure to +expected JSON format. + +Let's assume following table schema for example: +```sql +'CREATE TABLE `accounts_account` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(40) NOT NULL, + `price_granularity` enum('low','med','high','auto','dense','unknown') NOT NULL DEFAULT 'unknown', + `banner_cache_ttl` int(11) DEFAULT NULL, + `video_cache_ttl` int(11) DEFAULT NULL, + `events_enabled` bit(1) DEFAULT NULL, + `enforce_ccpa` bit(1) DEFAULT NULL, + `tcf_config` json DEFAULT NULL, + `truncate_target_attr` tinyint(3) unsigned DEFAULT NULL, + `default_integration` varchar(64) DEFAULT NULL, + `analytics_config` json DEFAULT NULL, + `bid_validations` json DEFAULT NULL, + `config` json DEFAULT NULL, + `status` enum('active','inactive') DEFAULT 'active', + `updated_by` int(11) DEFAULT NULL, + `updated_by_user` varchar(64) DEFAULT NULL, + `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +PRIMARY KEY (`id`), +UNIQUE KEY `uuid` (`uuid`)) +ENGINE=InnoDB DEFAULT CHARSET=utf8' ``` -SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config -FROM accounts_account where uuid = ? -LIMIT 1 +The following Mysql SQL query could be used to construct a JSON document of required shape on the fly: + +```sql +SELECT + JSON_MERGE_PATCH(config, JSON_OBJECT( + 'id', uuid, + 'status', status, + 'auction', JSON_OBJECT( + 'price-granularity', price_granularity, + 'banner-cache-ttl', banner_cache_ttl, + 'video-cache-ttl', video_cache_ttl, + 'truncate-target-attr', truncate_target_attr, + 'default-integration', default_integration, + 'bid-validations', bid_validations, + 'events', JSON_OBJECT( + 'enabled', NOT NOT(events_enabled) + ) + ), + 'privacy', JSON_OBJECT( + 'enforce-ccpa', NOT NOT(enforce_ccpa), + 'gdpr', tcf_config + ), + 'analytics', analytics_config + )) as consolidated_config +FROM accounts_account +WHERE uuid = %ACCOUNT_ID% +LIMIT 1; ``` diff --git a/docs/bidders/appnexus.md b/docs/bidders/appnexus.md index 434ba8048f0..2868740d234 100644 --- a/docs/bidders/appnexus.md +++ b/docs/bidders/appnexus.md @@ -15,7 +15,7 @@ The AppNexus endpoint expects `imp.displaymanagerver` to be populated for mobile requests, however not all SDKs will populate this field. If the `imp.displaymanagerver` field is not supplied for an `imp`, but `request.app.ext.prebid.source` and `request.app.ext.prebid.version` are supplied, the adapter will fill in a value for -`diplaymanagerver`. It will concatonate the two `app` fields as `-` to fill in +`diplaymanagerver`. It will concatenate the two `app` fields as `-` to fill in the empty `displaymanagerver` before sending the request to AppNexus. ## Test Request @@ -37,9 +37,13 @@ that would match with the test creative. }] }, "ext": { - "appnexus": { - "placementId": 13144370 - } - } + "prebid": { + "bidder":{ + "appnexus": { + "placementId": 13144370 + } + } + } + } }] ``` \ No newline at end of file diff --git a/docs/bidders/openx.md b/docs/bidders/openx.md index f7fc0dab04f..f8f3b9fd492 100644 --- a/docs/bidders/openx.md +++ b/docs/bidders/openx.md @@ -34,9 +34,13 @@ If you have any questions regarding setting up, please reach out to your account ] }, "ext": { - "openx": { - "delDomain": "mobile-d.openx.net", - "unit": "541028953" + "prebid": { + "bidder":{ + "openx": { + "delDomain": "mobile-d.openx.net", + "unit": "541028953" + } + } } } } @@ -56,10 +60,14 @@ If you have any questions regarding setting up, please reach out to your account ] }, "ext": { - "openx": { - "unit": "540949380", - "delDomain": "sademo-d.openx.net" - }, + "prebid": { + "bidder":{ + "openx": { + "unit": "540949380", + "delDomain": "sademo-d.openx.net" + } + } + } } } ``` \ No newline at end of file diff --git a/docs/bidders/pubmatic.md b/docs/bidders/pubmatic.md index 610108b2e07..503c8e51026 100644 --- a/docs/bidders/pubmatic.md +++ b/docs/bidders/pubmatic.md @@ -23,9 +23,13 @@ and sizes that would match with the test creative. ] }, "ext":{ - "pubmatic":{ - "publisherId":“156276”, - "adSlot":"pubmatic_test" + "prebid": { + "bidder"{ + "pubmatic":{ + "publisherId":“156276”, + "adSlot":"pubmatic_test" + } + } } } } diff --git a/docs/bidders/pubnative.md b/docs/bidders/pubnative.md index ad421069fb2..17704e91305 100644 --- a/docs/bidders/pubnative.md +++ b/docs/bidders/pubnative.md @@ -10,9 +10,9 @@ Please see [documentation](https://developers.pubnative.net/docs/prebid-adding-p ## Configuration -- bidder should be always set to "pubnative" (`imp.ext.pubnative`) -- zone_id (int) should be always set to 1, unless special use case agreed with our account manager. (`imp.ext.pubnative.zone_id`) -- app_auth_token (string) is unique per publisher app. Please contact our account manager to obtain yours. (`imp.ext.pubnative.app_auth_token`) +- bidder should be always set to "pubnative" (`imp.ext.prebid.bidder.pubnative`) +- zone_id (int) should be always set to 1, unless special use case agreed with our account manager. (`imp.ext.prebid.bidder.pubnative.zone_id`) +- app_auth_token (string) is unique per publisher app. Please contact our account manager to obtain yours. (`imp.ext.prebid.bidder.pubnative.app_auth_token`) An example is illustrated in a section below. @@ -44,9 +44,13 @@ The following json can be used to do a request to prebid server for verifying it ] }, "ext": { - "pubnative": { - "zone_id": 1, - "app_auth_token": "b620e282f3c74787beedda34336a4821" + "prebid": { + "bidder": { + "pubnative": { + "zone_id": 1, + "app_auth_token": "b620e282f3c74787beedda34336a4821" + } + } } } } diff --git a/docs/build-aws.md b/docs/build-aws.md index d9b2b9569d7..c6e64d93630 100644 --- a/docs/build-aws.md +++ b/docs/build-aws.md @@ -3,15 +3,19 @@ Follow next steps to create zip which can be deployed to AWS Elastic Beanstalk. Build project as described [here](build.md). +Then get `prebid-server.jar` file from generated `target` directory -Create next configuration files: +Create next configuration file: -- `prebid-config.yaml` - `prebid-logging.xml` -The content of these files can be found [here](config.md). +The content of these file can be found [here](config.md). + +Copy `sample` directory from project root +(it will contain two files) -- `Procfile` +- `prebid-config.yaml` +- `sample-app-settings.yaml` Create `Procfile` which tells how to start application: ```bash @@ -32,15 +36,15 @@ nano run.sh With the next content: ``` -exec java -Dlogging.config=$LOGGING_FILE -jar prebid-server.jar --spring.config.additional-location=$CONFIG_FILE +exec java -jar prebid-server.jar -Dlogging.config=$LOGGING_FILE --spring.config.additional-location=$CONFIG_FILE ``` where - $LOGGING_FILE - file with configuration for logger (`prebid-logging.xml` from example above) -- $CONFIG_FILE - file with prebid server configuration (`prebid-config.yaml` from example above) +- $CONFIG_FILE - file with prebid server configuration (`prebid-config.yaml` from `sample` directory above) If you follow same naming convention, your `run.sh` script should be similar to: ``` -exec java -Dlogging.config=prebid-logging.xml -jar prebid-server.jar --spring.config.additional-location=prebid-config.yaml +exec java -jar prebid-server.jar -Dlogging.config=prebid-logging.xml --spring.config.additional-location=sample/prebid-config.yaml ``` Make run.sh executable using the next command: @@ -93,18 +97,18 @@ It is used to configure tasks for tail logs, bundle logs, and log rotation. Move back to the project root directory and create zip file: ```bash cd .. -zip -j $ZIPFILE target/prebid-server.jar $CONFIG_FILE $PROCFILE $LOGGING_FILE $RUN_SH +zip -j $ZIPFILE target/prebid-server.jar $PROCFILE $LOGGING_FILE $RUN_SH ``` where - $ZIPFILE - name for zip file will be create by the command above - $PROCFILE - path to file describes how to start prebid-server (`Procfile` from example above) - $LOGGING_FILE - path to file with configuration for logger (`prebid-logging.xml` from example above) -- $CONFIG_FILE - path to file with prebid server configuration (`prebid-config.yaml` from example above) +- $CONFIG_FILE - path to file with prebid server configuration (`prebid-config.yaml` from `sample` directory above) - $RUN_SH - script to start application (`run.sh` from example above) If you follow same naming convention, your command should be similar to: ```bash -zip -j prebid-server.zip target/prebid-server.jar prebid-config.yaml Procfile prebid-logging.xml run.sh +zip -j prebid-server.zip prebid-server.jar Procfile prebid-logging.xml run.sh ``` Save project root directory to env variable: @@ -112,7 +116,12 @@ Save project root directory to env variable: export ROOT_DIR=$(pwd) ``` -Add `.ebextensions` to created in previous step zip archive using the command: +Add `sample` to created in previous step zip archive using the command: +```bash +zip -ur "$ROOT_DIR/$ZIPFILE" sample +``` + +Add `.ebextensions` to zip archive using the command: ```bash zip -ur "$ROOT_DIR/$ZIPFILE" .ebextensions ``` diff --git a/docs/build.md b/docs/build.md index 89e3d9a20f9..44b13bee47b 100644 --- a/docs/build.md +++ b/docs/build.md @@ -19,7 +19,7 @@ Follow next steps to create JAR which can be deployed locally. Download or clone a project locally: ```bash -git clone https://github.com/rubicon-project/prebid-server-java.git +git clone https://github.com/prebid/prebid-server-java.git ``` Move to project directory: diff --git a/docs/code-reviews.md b/docs/code-reviews.md index 972597751f5..78728fef18a 100644 --- a/docs/code-reviews.md +++ b/docs/code-reviews.md @@ -1,7 +1,7 @@ # Code Reviews ## Standards -Anyone is free to review and comment on any [open pull requests](https://github.com/rubicon-project/prebid-server-java/pulls). +Anyone is free to review and comment on any [open pull requests](https://github.com/prebid/prebid-server-java/pulls). All pull requests must be reviewed and approved by at least one [core member](https://github.com/orgs/prebid/teams/core/members) before merge. @@ -38,7 +38,7 @@ Some examples include: - Can we improve the user's experience in any way? - Have the relevant [docs]() been added or updated? If not, add the `needs docs` label. - Do you believe that the code works by looking at the unit tests? If not, suggest more tests until you do! -- Is the motivation behind these changes clear? If not, there must be [an issue](https://github.com/rubicon-project/prebid-server-java/issues) +- Is the motivation behind these changes clear? If not, there must be [an issue](https://github.com/prebid/prebid-server-java/issues) explaining it. Are there better ways to achieve those goals? - Does the code use any global, mutable state? [Inject dependencies](https://en.wikipedia.org/wiki/Dependency_injection) instead! - Can the code be organized into smaller, more modular pieces? diff --git a/docs/config-app.md b/docs/config-app.md index d52dcabc519..c9800108063 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -15,6 +15,7 @@ This parameter exists to allow to change the location of the directory Vert.x wi - `vertx.http-server-instances` - how many http server instances should be created. This parameter affects how many CPU cores will be utilized by the application. Rough assumption - one http server instance will keep 1 CPU core busy. - `vertx.init-timeout-ms` - time to wait for asynchronous initialization steps completion before considering them stuck. When exceeded - exception is thrown and Prebid Server stops. +- `vertx.enable-per-client-endpoint-metrics` - enables HTTP client metrics per destination endpoint (`host:port`) ## HTTP - `http.port` - the port to listen on. @@ -62,6 +63,9 @@ Removes and downloads file again if depending service cant process probably corr - `max-timeout-ms` - this setting controls maximum timeout for /auction endpoint. - `timeout-adjustment-ms` - reduces timeout value passed in legacy Auction request so that Prebid Server can handle timeouts from adapters and respond to the request before it times out. +## Default bid request +- `default-request.file.path` - path to a JSON file containing the default request + ## Auction (OpenRTB) - `auction.blacklisted-accounts` - comma separated list of blacklisted account IDs. - `auction.blacklisted-apps` - comma separated list of blacklisted applications IDs, requests from which should not be processed. @@ -74,7 +78,10 @@ Removes and downloads file again if depending service cant process probably corr - `auction.cache.expected-request-time-ms` - approximate value in milliseconds for Cache Service interacting. This time will be subtracted from global timeout. - `auction.cache.only-winning-bids` - if equals to `true` only the winning bids would be cached. Has lower priority than request-specific flags. - `auction.generate-bid-id` - whether to generate seatbid[].bid[].ext.prebid.bidid in the OpenRTB response. -- `auction.id-generator-type` - if generate-bid-id is on, then this defines how the ID should be generated. Currently onlye `uuid` is supported. +- `auction.generate-source-tid` - whether to generate bidrequest.source.tid in the OpenRTB request. +- `auction.validations.banner-creative-max-size` - enables creative max size validation for banners. Possible values: `skip`, `enforce`, `warn`. Default is `skip`. +- `auction.validations.secure-markup` - enables secure markup validation. Possible values: `skip`, `enforce`, `warn`. Default is `skip`. +- `auction.host-schain-node` - defines global schain node that will be appended to `request.source.ext.schain.nodes` passed to bidders ## Amp (OpenRTB) - `amp.default-timeout-ms` - default operation timeout for OpenRTB Amp requests. @@ -82,6 +89,12 @@ Removes and downloads file again if depending service cant process probably corr - `amp.timeout-adjustment-ms` - reduces timeout value passed in Amp request so that Prebid Server can handle timeouts from adapters and respond to the AMP RTC request before it times out. - `amp.custom-targeting` - a list of bidders whose custom targeting should be included in AMP responses. +## Timeout notification +- `auction.timeout-notification.timeout-ms` - HTTP timeout to use when sending notifications about bidder timeouts +- `auction.timeout-notification.log-result` - causes bidder timeout notification result to be logged +- `auction.timeout-notification.log-failure-only` - causes only bidder timeout notification failures to be logged +- `auction.timeout-notification.log-sampling-rate` - instructs apply sampling when logging bidder timeout notification results + ## Video - `auction.video.stored-required` - flag forces to merge with stored request - `auction.blacklisted-accounts` - comma separated list of blacklisted account IDs. @@ -97,7 +110,8 @@ Removes and downloads file again if depending service cant process probably corr - `cookie-sync.coop-sync.pri` - lists of bidders prioritised in groups. ## Vtrack -- `vtrack.allow-unkonwn-bidder` - flag allows servicing requests with bidders who were not configured in Prebid Server. +- `vtrack.allow-unknown-bidder` - flag that allows servicing requests with bidders who were not configured in Prebid Server. +- `vtrack.modify-vast-for-unknown-bidder` - flag that allows modifying the VAST value and adding the impression tag to it, for bidders who were not configured in Prebid Server. ## Adapters - `adapters.*` - the section for bidder specific configuration options. @@ -105,16 +119,30 @@ Removes and downloads file again if depending service cant process probably corr There are several typical keys: - `adapters..enabled` - indicates the bidder should be active and ready for auction. By default all bidders are disabled. - `adapters..endpoint` - the url for submitting bids. -- `adapters..pbs-enforces-gdpr` - indicates if pbs server provides gdpr support for bidder or bidder will handle it itself. +- `adapters..pbs-enforces-gdpr` - indicates if PBS server provides GDPR support for bidder or bidder will handle it itself. +- `adapters..pbs-enforces-ccpa` - indicates if PBS server provides CCPA support for bidder or bidder will handle it itself. +- `adapters..modifying-vast-xml-allowed` - indicates if PBS server is allowed to modify VAST creatives received from this bidder. - `adapters..deprecated-names` - comma separated deprecated names of bidder. -- `adapters..aliases` - comma separated aliases of bidder. +- `adapters..meta-info.maintainer-email` - specifies maintainer e-mail address that will be shown in bidder info endpoint response. +- `adapters..meta-info.app-media-types` - specifies media types supported for app requests that will be shown in bidder info endpoint response. +- `adapters..meta-info.site-media-types` - specifies media types supported for site requests that will be shown in bidder info endpoint response. +- `adapters..meta-info.supported-vendors` - specifies viewability vendors supported by the bidder. +- `adapters..meta-info.vendor-id` - specifies TCF vendor ID. - `adapters..usersync.url` - the url for synchronizing UIDs cookie. - `adapters..usersync.redirect-url` - the redirect part of url for synchronizing UIDs cookie. - `adapters..usersync.cookie-family-name` - the family name by which user ids within adapter's realm are stored in uidsCookie. - `adapters..usersync.type` - usersync type (i.e. redirect, iframe). - `adapters..usersync.support-cors` - flag signals if CORS supported by usersync. -But feel free to add additional bidder's specific options. +In addition, each bidder could have arbitrary aliases configured that will look and act very much the same as the bidder itself. +Aliases are configured by adding child configuration object at `adapters..aliases..`, aliases +support the same configuration options that their bidder counterparts support except `aliases` (i.e. it's not possible +to declare alias of an alias). Another restriction of aliases configuration is that they cannot declare support for media types +not supported by their bidders (however aliases could narrow down media types they support). For example: if the bidder +is written to not support native site requests, then an alias cannot magically decide to change that; however, if a bidder +supports native site requests, and the alias does not want to for some reason, it has the ability to remove that support. + +Also, each bidder could have its own bidder-specific options. ## Logging - `logging.http-interaction.max-limit` - maximum value for the number of interactions to log in one take. @@ -127,6 +155,8 @@ But feel free to add additional bidder's specific options. - `currency-converter.external-rates.url` - the url for Prebid.org’s currency file. [More details](http://prebid.org/dev-docs/modules/currency.html) - `currency-converter.external-rates.default-timeout-ms` - default operation timeout for fetching currency rates. - `currency-converter.external-rates.refresh-period-ms` - default refresh period for currency rates updates. +- `currency-converter.external-rates.stale-after-ms` - how old currency rates should be to become considered stale. +- `currency-converter.external-rates.stale-period-ms` - stale period after which the latest external currency rates get discarded. ## Admin Endpoints - `admin-endpoints.version.enabled` - if equals to `true` the endpoint will be available. @@ -159,6 +189,26 @@ But feel free to add additional bidder's specific options. - `admin-endpoints.logging-httpinteraction.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`. - `admin-endpoints.logging-httpinteraction.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials` +- `admin-endpoints.tracelog.enabled` - if equals to `true` the endpoint will be available. +- `admin-endpoints.tracelog.path` - the server context path where the endpoint will be accessible. +- `admin-endpoints.tracelog.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`. +- `admin-endpoints.tracelog.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials` + +- `admin-endpoints.deals-status.enabled` - if equals to `true` the endpoint will be available. +- `admin-endpoints.deals-status.path` - the server context path where the endpoint will be accessible. +- `admin-endpoints.deals-status.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`. +- `admin-endpoints.deals-status.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials` + +- `admin-endpoints.lineitem-status.enabled` - if equals to `true` the endpoint will be available. +- `admin-endpoints.lineitem-status.path` - the server context path where the endpoint will be accessible. +- `admin-endpoints.lineitem-status.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`. +- `admin-endpoints.lineitem-status.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials` + +- `admin-endpoints.e2eadmin.enabled` - if equals to `true` the endpoint will be available. +- `admin-endpoints.e2eadmin.path` - the server context path where the endpoint will be accessible. +- `admin-endpoints.e2eadmin.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`. +- `admin-endpoints.e2eadmin.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials` + - `admin-endpoints.credentials` - user and password for access to admin endpoints if `admin-endpoints.[NAME].protected` is true`. ## Metrics @@ -191,6 +241,9 @@ For `console` backend type available next options: - `metrics.console.enabled` - if equals to `true` then `console` will be used to submit metrics. - `metrics.console.interval` - interval in seconds between successive sending metrics. +For `prometheus` backend type available next options: +- `metrics.prometheus.port` - if a port is specified a prometheus reporter will start on that port + It is possible to define how many account-level metrics will be submitted on per-account basis. See [metrics documentation](metrics.md) for complete list of metrics submitted at each verbosity level. - `metrics.accounts.default-verbosity` - verbosity for accounts not specified in next sections. Allowed values: `none, basic, detailed`. Default is `none`. @@ -230,6 +283,7 @@ For database data source available next options: - `settings.database.user` - database user. - `settings.database.password` - database password. - `settings.database.pool-size` - set the initial/min/max pool size of database connections. +- `settings.database.account-query` - the SQL query to fetch account. - `settings.database.stored-requests-query` - the SQL query to fetch stored requests. - `settings.database.amp-stored-requests-query` - the SQL query to fetch AMP stored requests. - `settings.database.stored-responses-query` - the SQL query to fetch stored responses. @@ -245,6 +299,29 @@ For HTTP data source available next options: For account processing rules available next options: - `settings.enforce-valid-account` - if equals to `true` then request without account id will be rejected with 401. +- `settings.generate-storedrequest-bidrequest-id` - overrides `bidrequest.id` in amp or app stored request with generated UUID if true. Default value is false. This flag can be overridden by setting `bidrequest.id` as `{{UUID}}` placeholder directly in stored request. + +It is possible to specify default account configuration values that will be assumed if account config have them +unspecified or missing at all. Example: +```yaml +settings: + default-account-config: > + { + "auction": { + "default-integration": "pbjs" + "events": { + "enabled": true + } + }, + "privacy": { + "enforce-ccpa": true, + "gdpr": { + "enabled": true + } + } + } +``` +See [application settings](application-settings.md) for full reference of available configuration parameters. For caching available next options: - `settings.in-memory-cache.ttl-seconds` - how long (in seconds) data will be available in LRU cache. @@ -306,14 +383,18 @@ If not defined in config all other Health Checkers would be disabled and endpoin - `gdpr.special-features.sfN.vendor-exceptions[]` - bidder names that will be treated opposite to `sfN.enforce` value. - `gdpr.purpose-one-treatment-interpretation` - option that allows to skip the Purpose one enforcement workflow. - `gdpr.vendorlist.default-timeout-ms` - default operation timeout for obtaining new vendor list. -- `gdpr.vendorlist.vN.http-endpoint-template` - template string for vendor list url, where `{VERSION}` is used as version number placeholder. -- `gdpr.vendorlist.vN.refresh-missing-list-period-ms` - time to wait between attempts to fetch vendor list version that previously was reported to be missing by origin. Default `3600000` (one hour). -- `gdpr.vendorlist.vN.fallback-vendor-list-path` - location on the file system of the fallback vendor list that will be used in place of missing vendor list versions. Optional. -- `gdpr.vendorlist.vN.cache-dir` - directory for local storage cache for vendor list. Should be with `WRITE` permissions for user application run from. +- `gdpr.vendorlist.v2.http-endpoint-template` - template string for vendor list url version 2. +- `gdpr.vendorlist.v2.refresh-missing-list-period-ms` - time to wait between attempts to fetch vendor list version that previously was reported to be missing by origin. Default `3600000` (one hour). +- `gdpr.vendorlist.v2.fallback-vendor-list-path` - location on the file system of the fallback vendor list that will be used in place of missing vendor list versions. Optional. +- `gdpr.vendorlist.v2.deprecated` - Flag to show is this vendor list is deprecated or not. +- `gdpr.vendorlist.v2.cache-dir` - directory for local storage cache for vendor list. Should be with `WRITE` permissions for user application run from. ## CCPA - `ccpa.enforce` - if equals to `true` enforces to check ccpa policy, otherwise ignore ccpa verification. +## LMT +- `lmt.enforce` - if equals to `true` enforces to check lmt policy, otherwise ignore lmt verification. + ## Geo Location - `geolocation.enabled` - if equals to `true` the geo location service will be used to determine the country for client request. - `geolocation.circuit-breaker.enabled` - if equals to `true` circuit breaker will be used to make geo location client more robust. @@ -323,3 +404,48 @@ If not defined in config all other Health Checkers would be disabled and endpoin - `geolocation.type` - set the geo location service provider, can be `maxmind` or custom provided by hosting company. - `geolocation.maxmind` - section for [MaxMind](https://www.maxmind.com) configuration as geo location service provider. - `geolocation.maxmind.remote-file-syncer` - use RemoteFileSyncer component for downloading/updating MaxMind database file. See [RemoteFileSyncer](#remote-file-syncer) section for its configuration. + +## Analytics +- `analytics.pubstack.enabled` - if equals to `true` the Pubstack analytics module will be enabled. Default value is `false`. +- `analytics.pubstack.endpoint` - url for reporting events and fetching configuration. +- `analytics.pubstack.scopeid` - defined the scope provided by the Pubstack Support Team. +- `analytics.pubstack.configuration-refresh-delay-ms` - delay in milliseconds between remote config updates. +- `analytics.pubstack.timeout-ms` - timeout in milliseconds for report and fetch config requests. +- `analytics.pubstack.buffers.size-bytes` - threshold in bytes for buffer to send events. +- `analytics.pubstack.buffers.count` - threshold in events count for buffer to send events +- `analytics.pubstack.buffers.report-ttl-ms` - max period between two reports. + +## Programmatic Guaranteed Delivery +- `deals.planner.plan-endpoint` - planner endpoint to get plans from. +- `deals.planner.update-period` - cron expression to start job for requesting Line Item metadata updates from the Planner. +- `deals.planner.plan-advance-period` - cron expression to start job for advancing Line Items to the next plan. +- `deals.planner.retry-period-sec` - how long (in seconds) to wait before re-sending a request to the Planner that previously failed with 5xx HTTP error code. +- `deals.planner.timeout-ms` - default operation timeout for requests to planner's endpoints. +- `deals.planner.register-endpoint` - register endpoint to get plans from. +- `deals.planner.register-period-sec` - time period (in seconds) to send register request to the Planner. +- `deals.planner.username` - username for planner BasicAuth. +- `deals.planner.password` - password for planner BasicAuth. +- `deals.delivery-stats.delivery-period` - cron expression to start job for sending delivery progress to planner. +- `deals.delivery-stats.cached-reports-number` - how many reports to cache while planner is unresponsive. +- `deals.delivery-stats.timeout-ms` - default operation timeout for requests to delivery progress endpoints. +- `deals.delivery-stats.username` - username for delivery progress BasicAuth. +- `deals.delivery-stats.password` - password for delivery progress BasicAuth. +- `deals.delivery-stats.line-items-per-report` - max number of line items in each report to split for batching. Default is 25. +- `deals.delivery-stats.reports-interval-ms` - interval in ms between consecutive reports. Default is 0. +- `deals.delivery-stats.batches-interval-ms` - interval in ms between consecutive batches. Default is 1000. +- `deals.delivery-stats.request-compression-enabled` - enables request gzip compression when set to true. +- `deals.delivery-progress.line-item-status-ttl-sec` - how long to store line item's metrics after it was expired. +- `deals.delivery-progress.cached-plans-number` - how many plans to store in metrics per line item. +- `deals.delivery-progress.report-reset-period`- cron expression to start job for closing current delivery progress and starting new one. +- `deals.delivery-progress-report.competitors-number`- number of line items top competitors to send in delivery progress report. +- `deals.user-data.user-details-endpoint` - user Data Store endpoint to get user details from. +- `deals.user-data.win-event-endpoint` - user Data Store endpoint to which win events should be sent. +- `deals.user-data.timeout` - time to wait (in milliseconds) for User Data Service response. +- `deals.user-data.user-ids` - list of Rules for determining user identifiers to send to User Data Store. +- `deals.max-deals-per-bidder` - maximum number of deals to send to each bidder. +- `deals.alert-proxy.enabled` - enable alert proxy service if `true`. +- `deals.alert-proxy.url` - alert service endpoint to send alerts to. +- `deals.alert-proxy.timeout-sec` - default operation timeout for requests to alert service endpoint. +- `deals.alert-proxy.username` - username for alert proxy BasicAuth. +- `deals.alert-proxy.password` - password for alert proxy BasicAuth. +- `deals.alert-proxy.alert-types` - key value pair of alert type and sampling factor to send high priority alert. diff --git a/docs/config.md b/docs/config.md index 9a1966ba5f1..89055c2ca3d 100644 --- a/docs/config.md +++ b/docs/config.md @@ -24,8 +24,8 @@ metrics: settings: database: type: mysql - stored-requests-query: SELECT reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) UNION ALL SELECT impid, impData, 'imp' as dataType FROM stored_imps WHERE impid IN (%IMP_ID_LIST%) - amp-stored-requests-query: SELECT reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) + stored-requests-query: SELECT accountId, reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) UNION ALL SELECT accountId, impid, impData, 'imp' as dataType FROM stored_imps WHERE impid IN (%IMP_ID_LIST%) + amp-stored-requests-query: SELECT accountId, reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) stored-responses-query: SELECT resid, responseData FROM stored_responses WHERE resid IN (%RESPONSE_ID_LIST%) ``` If some property is missed in `prebid-config.yaml` application will look for it in `src/main/resources/application.yaml` file. diff --git a/docs/contributing.md b/docs/contributing.md index 3ca432f3905..ae3f10dc4e9 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -2,7 +2,7 @@ ## Create an issue -[Create an issue](https://github.com/rubicon-project/prebid-server-java/issues/new) describing the motivation for your changes. +[Create an issue](https://github.com/prebid/prebid-server-java/issues/new) describing the motivation for your changes. Are you fixing a bug? Improving documentation? Optimizing some slow code? Pull Requests without associated Issues may still be accepted, if the motivation is obvious. @@ -29,6 +29,6 @@ those updates must be submitted in the same Pull Request as the code changes. ## Open a Pull Request When you're ready, [submit a Pull Request](https://help.github.com/articles/creating-a-pull-request/) -against the `master` branch of [our GitHub repository](https://github.com/rubicon-project/prebid-server-java/compare). +against the `master` branch of [our GitHub repository](https://github.com/prebid/prebid-server-java/compare). If the tests pass locally, but fail on your PR, [update your fork](https://help.github.com/articles/syncing-a-fork/) with the latest code from `master`. diff --git a/docs/deals.md b/docs/deals.md new file mode 100644 index 00000000000..fca8c585e26 --- /dev/null +++ b/docs/deals.md @@ -0,0 +1,152 @@ +# Deals + +## Planner and Register services + +### Planner service + +Periodically request Line Item metadata from the Planner. Line Item metadata includes: +1. Line Item details +2. Targeting +3. Frequency caps +4. Delivery schedule + +### Register service + +Each Prebid Server instance register itself with the General Planner with a health index +(QoS indicator based on its internal counters like circuit breaker trip counters, timeouts, etc.) +and KPI like ad requests per second. + +Also allows planner send command to PBS admin endpoint to stored request caches and tracelogs. + +### Planner and register service configuration + +```yaml +planner: + register-endpoint: + plan-endpoint: + update-period: "0 */1 * * * *" + register-period-sec: 60 + timeout-ms: 8000 + username: + password: +``` + +## Deals stats service + +Supports sending reports to delivery stats serving with following metrics: + +1. Number of client requests seen since start-up +2. For each Line Item +- Number of tokens spent so far at each token class within active and expired plans +- Number of times the account made requests (this will be the same across all LineItem for the account) +- Number of win notifications +- Number of times the domain part of the target matched +- Number of times impressions matched whole target +- Number of times impressions matched the target but was frequency capped +- Number of times impressions matched the target but the fcap lookup failed +- Number of times LineItem was sent to the bidder +- Number of times LineItem was sent to the bidder as the top match +- Number of times LineItem came back from the bidder +- Number of times the LineItem response was invalidated +- Number of times the LineItem was sent to the client +- Number of times the LineItem was sent to the client as the top match +- Array of top 10 competing LineItems sent to client + +### Deals stats service configuration + +```yaml +delivery-stats: + endpoint: + delivery-period: "0 */1 * * * *" + cached-reports-number: 20 + line-item-status-ttl-sec: 3600 + timeout-ms: 8000 + username: + password: +``` + +## Alert service + +Sends out alerts when PBS cannot talk to general planner and other critical situations. Alerts are simply JSON messages +over HTTP sent to a central proxy server. + +```yaml + alert-proxy: + enabled: truew + timeout-sec: 10 + url: + username: + password: + alert-types: + : + pbs-planner-empty-response-error: 15 +``` + +## GeoLocation service + +This service currently has 1 implementation: +- MaxMind + +In order to support targeting by geographical attributes the service will provide the following information: + +1. `continent` - Continent code +2. `region` - Region code using ISO-3166-2 +3. `metro` - Nielsen DMAs +4. `city` - city using provider specific encoding +5. `lat` - latitude from -90.0 to +90.0, where negative is south +6. `lon` - longitude from -180.0 to +180.0, where negative is west + +### GeoLocation service configuration for MaxMind + +```yaml +geolocation: + enabled: true + type: maxmind + maxmind: + remote-file-syncer: + download-url: + save-filepath: + tmp-filepath: + retry-count: 3 + retry-interval-ms: 3000 + timeout-ms: 300000 + update-interval-ms: 0 + http-client: + connect-timeout-ms: 2500 + max-redirects: 3 +``` + +## User Service + +This service is responsible for: +- Requesting user targeting segments and frequency capping status from the User Data Store +- Reporting to User Data Store when users finally see ads to aid in correctly enforcing frequency caps + +### User service configuration + +```yaml + user-data: + win-event-endpoint: + user-details-endpoint: + timeout: 1000 + user-ids: + - location: rubicon + source: uid + type: khaos +``` +1. khaos, adnxs - types of the ids that will be specified in requests to User Data Store +2. source - source of the id, the only supported value so far is “uids” which stands for uids cookie +3. location - where exactly in the source to look for id + +## Device Info Service + +DeviceInfoService returns device-related attributes based on User-Agent for use in targeting: +- deviceClass: desktop, tablet, phone, ctv +- os: windows, ios, android, osx, unix, chromeos +- osVersion +- browser: chrome, firefox, edge, safari +- browserVersion + +## See also + +- [Configuration](config.md) diff --git a/docs/developers/PrebidServerJava_GDPR_Requirements.pdf b/docs/developers/PrebidServerJava_GDPR_Requirements.pdf deleted file mode 100644 index 33792ab1d4c..00000000000 Binary files a/docs/developers/PrebidServerJava_GDPR_Requirements.pdf and /dev/null differ diff --git a/docs/developers/add-new-bidder.md b/docs/developers/add-new-bidder.md index c299642733b..b2361533b49 100644 --- a/docs/developers/add-new-bidder.md +++ b/docs/developers/add-new-bidder.md @@ -16,26 +16,37 @@ Throughout the rest of this document, substitute `{bidder}` with the name you've Bidders may define their own APIs for Publishers pass custom values. It is _strongly encouraged_ that these not duplicate values already present in the [OpenRTB 2.5 spec](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf). -Publishers will send values for these parameters in `request.imp[i].ext.{bidder}` of +Publishers will send values for these parameters in `request.imp[i].ext.prebid.bidder.{bidder}` of [the Auction endpoint](../endpoints/openrtb2/auction.md). Prebid Server will preprocess these so that your bidder will access them at `request.imp[i].ext.bidder`--regardless of what your `{bidder}` name is. ## Configuration Add default configuration properties and metadata(e.g. contact email, platform & media type support) for your Bidder to `src/main/resources/bidder-config/{bidder}.yaml` file. -For more information about application configuration see [here](../config.md) +For more information about application configuration see [here](../config-app.md) ## Implementation Bidder implementations are scattered throughout several files: - `src/main/java/org/prebid/server/bidder/{bidder}/{bidder}Bidder.java`: contains an implementation of [the Bidder interface](../../src/main/java/org/prebid/server/bidder/Bidder.java). -- `src/main/java/org/prebid/server/bidder/{bidder}/{bidder}Adapter.java`: contains an implementation of [the Adapter interface](../../src/main/java/org/prebid/server/bidder/Adapter.java). - `src/main/java/org/prebid/server/bidder/{bidder}/{bidder}Usersyncer.java`: contains an implementation of [the Usersyncer interface](../../src/main/java/org/prebid/server/bidder/Usersyncer.java). - `src/main/java/org/prebid/server/proto/openrtb/ext/{bidder}`: contract classes for your Bidder's params. - `src/main/resources/static/bidder-params/{bidder}.json`: A [draft-4 json-schema](https://spacetelescope.github.io/understanding-json-schema/) which [validates your Bidder's params](https://www.jsonschemavalidator.net/). Bidder implementations may assume that any params have already been validated against the defined json-schema. +### Timeout notification support +This is an optional feature. If you wish to get timeout notifications when a bid request from PBS times out, you can implement the +`org.prebid.server.bidder.Bidder.makeTimeoutNotification` method in your bidder implementation. If you do not wish +timeout notification, do not implement the method. + +`HttpRequest makeTimeoutNotification(HttpRequest httpRequest)` + +Here the `HttpRequest` supplied as an argument is the request returned from `makeHttpRequests` that timed out. If a bidder generates +multiple requests, and more than one of them times out, then there will be a call to `makeTimeoutNotification` for each failed +request. The method should then return a `HttpRequest` object that will be the timeout notification to be sent to the bidder. +Timeout notifications will not generate subsequent timeout notifications if they time out or fail. + ### Generic OpenRTB Bidder There's an option to implement a bidder by using a pre-existing template. @@ -89,7 +100,8 @@ It should be public class with Spring `@Configuration` annotation so that framew This file consists of three main parts: - the constant `BIDDER_NAME` with the name of your Bidder. - injected configuration properties (like `endpoint`, `usersyncUrl`, etc) needed for the Bidder's implementation. -- declaration of `BidderDeps` bean combining _bidder name_, _Usersyncer_, _Adapter_ and _BidderRequester_ in one place as a single point-of-truth for using it in application. +- declaration of `BidderDeps` bean combining _bidder name_, _Usersyncer_, _Adapter_, _Bidder_ and other meta-data +for the bidder and its aliases in one place as a single point-of-truth for using it in application. Also, you can add `@ConditionalOnProperty` annotation on configuration if bidder has no default properties. See `src/main/java/org/prebid/server/spring/config/bidder/FacebookConfiguration.java` as an example. @@ -102,17 +114,16 @@ Assume common rules to write unit tests from [here](unit-tests.md). Bidder tests live in the next files: - `src/test/java/org/prebid/server/bidder/{bidder}/{bidder}BidderTest.java`: unit tests for your Bidder implementation. -- `src/test/java/org/prebid/server/bidder/{bidder}/{bidder}AdapterTest.java`: unit tests for your Adapter implementation. - `src/test/java/org/prebid/server/bidder/{bidder}/{bidder}UsersyncerTest.java`: unit tests for your Usersyncer implementation. Commonly you should write tests for covering: -- creation of your Adapter/Bidder/Usersyncer implementations. +- creation of your Bidder/Usersyncer implementations. - correct Bidder's params filling. - JSON parser errors handling. - specific cases for composing requests to exchange. - specific cases for processing responses from exchange. -Do not forget to add your Bidder to `ApplicationTest.java` tests. +Do not forget to add integration test for your Bidder in `src/test/java/org/prebid/server/it`. We expect to see at least 90% code coverage on each bidder. @@ -122,7 +133,7 @@ We expect to see at least 90% code coverage on each bidder. Then `POST` an OpenRTB Request to `http://localhost:8080/openrtb2/auction`. -If at least one `request.imp[i].ext.{bidder}` is defined in your Request, then your bidder should be called. +If at least one `request.imp[i].ext.prebid.bidder.{bidder}` is defined in your Request, then your bidder should be called. To test user syncs, [save a UID](../endpoints/setuid.md) using the FamilyName of your Bidder. The next time you use `/openrtb2/auction`, the OpenRTB request sent to your Bidder should have diff --git a/docs/developers/default-request.md b/docs/developers/default-request.md new file mode 100644 index 00000000000..08bf1041009 --- /dev/null +++ b/docs/developers/default-request.md @@ -0,0 +1,29 @@ +# Server Based Global Default Request + +This allows a default request to be defined that allows the server to set up some defaults for all incoming +requests. A stored request(s) referenced by a bid request override default request, and of course any options specified +directly in the bid request override both. The default request is only read on server startup, it is meant as an +installation static default rather than a dynamic tuning option. + +## Config Options + +Following config options are exposed to support this feature. +```yaml +default-request: + file: + path : path/to/default/request.json +``` + +The `path` option is the path to a JSON file containing the default request JSON. +```json +{ + "tmax": "2000", + "regs": { + "ext": { + "gdpr": 1 + } + } +} +``` +This will be JSON merged into the incoming requests at the top level. These will be used as fallbacks which can be +overridden by both Stored Requests _and_ the incoming HTTP request payload. diff --git a/docs/developers/stored-requests.md b/docs/developers/stored-requests.md index b8b5bf1ece9..285e16b5771 100644 --- a/docs/developers/stored-requests.md +++ b/docs/developers/stored-requests.md @@ -36,8 +36,12 @@ Add the file `stored_imps/{id}.json` and populate it with some [Imp](https://www ] }, "ext": { - "appnexus": { - "placement_id": 10433394 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 10433394 + } + } } } } @@ -83,8 +87,12 @@ You can also store _part_ of the Imp on the server. For example: ] }, "ext": { - "appnexus": { - "placement_id": 10433394 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 10433394 + } + } } } } @@ -122,7 +130,7 @@ So far, our examples have only used Stored Imp data. However, Stored Requests are also allowed on the [BidRequest](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=15). These work exactly the same way, but support storing properties like timeouts and price granularity. -For example, assume the following `stored-requests/stored-request.json`: +For example, assume the following `stored-requests/{id}.json`: ```json { @@ -137,7 +145,7 @@ For example, assume the following `stored-requests/stored-request.json`: } ``` -Then an HTTP request like: +Then HTTP request like: ```json { @@ -148,7 +156,7 @@ Then an HTTP request like: "ext": { "prebid": { "storedrequest": { - "id": "stored-request.json" + "id": "{id}" } } } @@ -178,8 +186,7 @@ Prebid Server does allow Stored BidRequests and Stored Imps in the same HTTP Req The Stored BidRequest will be applied first, and then the Stored Imps after. **Beware**: Stored Request data will not be applied recursively. -If a Stored BidRequest includes Imps with their own Stored Request IDs, -then the data for those Stored Imps not be resolved. +If a Stored BidRequest includes Imps with their own Stored Request IDs, then the data for those Stored Imps will not be resolved. ## Alternate backends @@ -193,13 +200,6 @@ For PostgreSQL: settings: database: type: postgres - host: localhost - port: 5432 - dbname: database-name - user: username - password: password - stored-requests-query: SELECT reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) UNION ALL SELECT impid, impData, 'imp' as dataType FROM stored_imps WHERE impid IN (%IMP_ID_LIST%) - amp-stored-requests-query: SELECT reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) ``` For MySQL: @@ -209,10 +209,11 @@ settings: type: mysql ``` -The select query columns of `stored-data-query` and `amp-stored-data-query` properties should correspond to the specific format: -- first column: ID of stored data item -- second column: value of stored data item -- third column: type of stored data item. Can be `request` for stored requests or `imp` for stored impressions. +The select query columns of `stored-requests-query` and `amp-stored-requests-query` properties should correspond to the specific format: +- first column: account ID which is searched by. +- second column: ID of stored data item which is searched by. +- third column: value of stored data item. +- forth column: type of stored data item. Can be `request` for stored requests or `imp` for stored impressions. ### HTTP backend @@ -261,8 +262,8 @@ settings: dbname: database-name user: username password: password - stored-requests-query: SELECT reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) UNION ALL SELECT impid, impData, 'imp' as dataType FROM stored_imps WHERE impid IN (%IMP_ID_LIST%) - amp-stored-requests-query: SELECT reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) + stored-requests-query: SELECT accountId, reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) UNION ALL SELECT accountId, impid, impData, 'imp' as dataType FROM stored_imps WHERE impid IN (%IMP_ID_LIST%) + amp-stored-requests-query: SELECT accountId, reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) http: endpoint: http://stored-requests.prebid.com amp-endpoint: http://stored-requests.prebid.com?amp=true diff --git a/docs/differenceBetweenPBSGo-and-Java.md b/docs/differenceBetweenPBSGo-and-Java.md deleted file mode 100644 index 3e6d185f550..00000000000 --- a/docs/differenceBetweenPBSGo-and-Java.md +++ /dev/null @@ -1,46 +0,0 @@ -# Differences Between Prebid Server Go and Java - -January 24, 2019 - -The sister Prebid Server projects are both busy and moving forward at different paces on different features. Sometimes a feature may exist in one implementation -and not the other for an interim period. This page tracks known differences that may persist for longer than a couple of weeks. - -[Feature Checklist Overview](pbs-java-and-go-features-review.md) - -## Feature Differences - -1) PBS-Java supports Stored Responses [issue 861](https://github.com/prebid/prebid-server/issues/861). PBS-Java [PR 354](https://github.com/rubicon-project/prebid-server-java/pull/354). -1) PBS-Java supports Currency conversion. PBS-Go has it implemented, but disabled by default(still under dev) [issue 280](https://github.com/prebid/prebid-server/issues/280), [issue 760](https://github.com/prebid/prebid-server/pull/760). PBS-Java [PR 22](https://github.com/rubicon-project/prebid-server-java/pull/22) -1) PBS-Java Currency conversion supports finding intermediate conversion rate, e.g. if pairs USD : AUD = 1.2 and EUR : AUD = 1.5 are present and EUR to USD conversion is needed, will return (1/1.5) * 1.2 conversion rate. -1) PBS-Go Currency conversion admin debug endpoint exposes following information: Sync source URL, Internal rates, Update frequency, Last update. PBS-Java `/currency-rates` admin endpoint currently supports checking the latest update time only and is not available if currency conversion is disabled. -1) PBS-Java supports IP-address lookup in certain scenarios around GDPR. See https://github.com/rubicon-project/prebid-server-java/blob/master/docs/developers/PrebidServerJava_GDPR_Requirements.pdf -1) PBS-Java supports InfluxDB, Graphite and Prometheus, PBS-Go supports InfluxDB and Prometheus as metrics backend. -1) PBS-Java has Circuit Breaker mechanism for database, http and geolocation requests. This can protect the server in scenarios where an external service becomes unavailable. -1) PBS-Java supports `ext.prebid.cache.{bids,vastxml}.returnCreative` field to control creative presence in response (`true` by default). -1) PBS-Java support caching winning bids only through `auction.cache.only-winning-bids` configuration property or request field `request.ext.prebid.cache.winningonly`. PBS-Java [issue 279](https://github.com/rubicon-project/prebid-server-java/issues/279), [PR 484](https://github.com/rubicon-project/prebid-server-java/pull/484). -1) PBS-Java has a specific `host-cookie` and `uids` cookie processing for all endpoints, that sets `uids.HOST-BIDDER` from `host-cookie` if first is absent or not equal to second. -1) PBS-Java has a specific `/cookie-sync` behaviour, that sets `/setuid` as usersync-url for host-bidder if `host-cookie` specified but `uids.HOST-BIDDER` undefined or differs. -1) PBS-Java has `/event` endpoint to allow Web browsers and mobile applications to notify about different ad events (win, view etc). Filling new bid extensions `response.seatbid.bid.ext.prebid.events.{win,view}` with events url after successful auction completing makes it possible. -1) PBS-Java supports per-account cache TTL and event URLs configuration in the database in columns `banner_cache_ttl`, `video_cache_ttl` and `events_enabled`. -1) PBS-Java does not support passing bidder extensions in `imp[...].ext.prebid.bidder`. PBS-Go [PR 846](https://github.com/prebid/prebid-server/pull/846) -1) PBS-Java responds with active bidders only in `/info/bidders` and `/info/bidders/all` endpoints, although PBS-Go returns all implemented ones. Calling `/info/bidders/{bidderName}` with a disabled bidder name will result in 404 Not Found, which is a desired behaviour, unlike in PBS-Go [issue 988](https://github.com/prebid/prebid-server/issues/988), [PR](https://github.com/prebid/prebid-server/pull/989). -1) PBS-Java supports video impression tracking [issue 1015](https://github.com/prebid/prebid-server/issues/1015). PBS-Java [PR 437](https://github.com/rubicon-project/prebid-server-java/pull/437). - -## Minor differences - -- PBS-Java removes null objects or empty strings (e.g. in Go `/auction` response bid object will have field `hb_cache: ""` whereas in Java it will be absent; also `digitrust: null` in PBS Go is not there in PBS Java). PBS-Go [Issue 476](https://github.com/prebid/prebid-server/issues/476) -- All adapters have been ported to use OpenRTB directly in PBS-Java. PBS-Go Facebook AudienceNetwork adapter [Issue 211](https://github.com/prebid/prebid-server/issues/211) -- Java and Go adapter internal interface returns currency in different ways: - - in PBS-Go, the adapter sets BidResponse.currency, which is outside of each TypedBid. - - in PBS-Java, the adapter set BidderBid[N].currency. -- PBS-Go use "60 seconds buffer + {bid,imp,mediaType}TTL" approach to determine caching TTL period. -- PBS-Java has different names for system metrics. For example instead of `active_connections` it uses `vertx.http.servers.[IP]:[PORT].open-netsockets.count`. See [Metrics](metrics.md) for details. -- PBS-Go `/openrtb2/{auction,amp}` returns HTTP 503 Service Unavailable if request has blacklisted account or app, PBS-Java returns HTTP 403 Forbidden. - -## GDPR differences -- PBS-Java supports geo location service interface to determine the country for incoming client request and provides a default implementation using MaxMind GeoLite2 Country database. -- Different checking of purpose IDs (1 - `Storage and access of information`, 3 - `Ad selection, delivery, reporting`): - - for `/auction` endpoint: in PBS-Java - doesn't support GDPR processing. - - for `/openrtb2/{auction,amp}` endpoint: in PBS-Java - 1 and 3 (for each bidder from request); in PBS-Go - doesn't support GDPR processing. - - for `/cookie_sync` endpoint: in PBS-Java - doesn't support GDPR processing; in PBS-Go - only 1 checked. -- PBS-Java allows bidder to enforce GDPR processing. This information available in bidder meta info. diff --git a/docs/endpoints/admin.md b/docs/endpoints/admin.md deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/docs/endpoints/auction.md b/docs/endpoints/auction.md index ac4f19f1c6a..9eb35f77914 100644 --- a/docs/endpoints/auction.md +++ b/docs/endpoints/auction.md @@ -1,6 +1,6 @@ # Auction The `/auction` endpoint expects a JSON request in to format defined by -[pbs_request.json](https://github.com/rubicon-project/prebid-server-java/blob/master/src/main/resources/static/pbs_request.json). +[pbs_request.json](../../src/main/resources/static/pbs_request.json). This endpoint does not support OpenRTB request, so can be assumed as legacy. diff --git a/docs/endpoints/bidders/params.md b/docs/endpoints/bidders/params.md deleted file mode 100644 index 24fcfcdf333..00000000000 --- a/docs/endpoints/bidders/params.md +++ /dev/null @@ -1,24 +0,0 @@ -## GET /bidders/params - -This endpoint gets information about all the custom bidders params that Prebid Server supports. - -### Returns - -A JSON object whose keys are bidder codes, and values are Draft 4 JSON schemas which describe that bidders' params. - -For example: - -``` -{ - "appnexus": { /* A json-schema describing AppNexus' bidder params */ }, - "rubicon": { /* A json-schema describing Rubicon's bidder params */ } - ... all other bidders will have similar keys & values here ... -} -``` - -The exact contents of the json-schema values can be found [here](../../../src/main/resources/static/bidder-params). - -### See also - -- [JSON schema homepage](http://json-schema.org/specification-links.html#draft-4) -- [Understanding JSON schema](https://spacetelescope.github.io/understanding-json-schema/) diff --git a/docs/endpoints/cookieSync.md b/docs/endpoints/cookieSync.md index 47f028521b6..7115092a334 100644 --- a/docs/endpoints/cookieSync.md +++ b/docs/endpoints/cookieSync.md @@ -9,13 +9,23 @@ This endpoint is used during cookie syncs. For technical details, see the This returns a set of URLs to enable cookie syncs across bidders. (See Prebid.js documentation?) The request must supply a JSON object to define the list of bidders that may need to be synced. -``` +```json { "bidders": ["appnexus", "rubicon"], "coopSync": true, "gdpr": 1, "gdpr_consent": "BONV8oqONXwgmADACHENAO7pqzAAppY", - "limit": 2 + "limit": 2, + "filterSettings": { + "iframe": { + "bidders": ["rubicon"], // only this bidder is excluded from syncing iframe pixels, all other bidders are allowed + "filter": "exclude" + }, + "image": { + "bidders": ["appnexus"], // only this bidder is allowed to sync image pixels + "filter": "include" + } + } } ``` @@ -31,6 +41,8 @@ must supply a JSON object to define the list of bidders that may need to be sync get the count down to limit if more would otherwise have been returned. This is to facilitate clients not overloading a user with syncs the first time they are encountered. +`filterSettings` is optional. It defines which bidders are allowed to use which usersync method. + If `gdpr` is omitted, callers are still encouraged to send `gdpr_consent` if they have it. Depending on how the Prebid Server host company has configured their servers, they may or may not require it for cookie syncs. diff --git a/docs/endpoints/currency-rates.md b/docs/endpoints/currency-rates.md deleted file mode 100644 index f733c88b947..00000000000 --- a/docs/endpoints/currency-rates.md +++ /dev/null @@ -1,29 +0,0 @@ -# Currency Rates - -Unavailable if currency conversion is disabled (`currency-converter.external-rates.enabled` config property). - -This endpoint will return a json with the the following information: -- `active` - true if currency conversion is enabled -- `source` - URL from which rates are fetched -- `fetchingIntervalNs` - fetching interval from source in nanoseconds -- `lastUpdated` - timestamp when the rates where updated (in the ISO-8601 format, using UTC) -- `rates` - internal rates values - -Sample response: - -```json -{ - "active": true, - "source": "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json", - "fetchingIntervalNs": 60000000000, - "lastUpdated":"2018-11-06T19:25:48.085Z", - "rates": { - "GBP": { - "AUD": 1.8611576401 - }, - "USD": { - "AUD": 1.4056048493 - } - } -} -``` \ No newline at end of file diff --git a/docs/endpoints/deals-status.md b/docs/endpoints/deals-status.md new file mode 100644 index 00000000000..eb8a234f4eb --- /dev/null +++ b/docs/endpoints/deals-status.md @@ -0,0 +1,10 @@ +# Deals Status +This endpoint is available on admin port called /pbs-admin/deals-status + +## `GET /pbs-admin/deals-status` + +Giving read-only access to current Line Items status, progress and aggregated metrics. + +### Sample request + +`GET http://prebid.site.com/pbs-admin/deals-status` \ No newline at end of file diff --git a/docs/endpoints/event.md b/docs/endpoints/event.md deleted file mode 100644 index b3074a917bd..00000000000 --- a/docs/endpoints/event.md +++ /dev/null @@ -1,22 +0,0 @@ -# Event Endpoint - -Allows Web browsers and mobile applications to notify about different ad events (win, imp etc). - -## `GET /event` - -This endpoint is used to notify about event and do request for tracking pixel if needed. - -### Query Params - -- `t`: Type of the event. Allowed values: `win` or `imp`. Required parameter. -- `a`: Account ID. Required parameter. -- `b`: Bid ID, expected to be unique at least within single bidder. Required parameter. -- `bidder`: Bidder code. Required parameter. -- `f`: Format of the PBS response. Allowed values: - - `b`: blank, just return HTTP 200 with an empty body - - `i`: image, return HTTP 200 with a blank PNG body -- `ts`: auction timestamp -- `x` : Disables or enables analytics. Allowed values: `1` to enable analytics or `0` to disable. `1` is default. -### Sample request - -`GET http://prebid.site.com/event?type=win&bidid=12345&a=1111&bidder=rubicon&f=b` diff --git a/docs/endpoints/getuids.md b/docs/endpoints/getuids.md deleted file mode 100644 index fcc088aa25e..00000000000 --- a/docs/endpoints/getuids.md +++ /dev/null @@ -1,11 +0,0 @@ -# Getting User Syncs - -This endpoint is used by bidders to obtain user IDs with Prebid Server. - -## Sample request - -`GET http://prebid.site.com/getuids` - -This will response like: -```json -{"buyeruids":{"adnxs":"appnexus-uid","rubicon":"rubicon-uid"}} \ No newline at end of file diff --git a/docs/endpoints/info/bidders.md b/docs/endpoints/info/bidders.md deleted file mode 100644 index a3868782993..00000000000 --- a/docs/endpoints/info/bidders.md +++ /dev/null @@ -1,23 +0,0 @@ -# Bidder List - -## `GET /info/bidders` - -This endpoint returns a list of active Bidders supported by Prebid Server. -These are the core values allowed to be used as `request.imp[i].ext.{bidder}` -keys in [Auction](../openrtb2/auction.md) requests. - -For detailed info about a specific Bidder, use [`/info/bidders/{bidderName}`](bidders/bidderName.md) - -### Sample Response - -This endpoint returns JSON like: - -``` -[ - "appnexus", - "audienceNetwork", - "pubmatic", - "rubicon", - "other-bidders-here" -] -``` diff --git a/docs/endpoints/info/bidders/bidderName.md b/docs/endpoints/info/bidders/bidderName.md deleted file mode 100644 index f3a041635ca..00000000000 --- a/docs/endpoints/info/bidders/bidderName.md +++ /dev/null @@ -1,95 +0,0 @@ -# Bidder Details - -## `GET /info/bidders/{bidderName}` - -This endpoint returns some metadata about the Bidder whose name is `{bidderName}`. -Legal values for `{bidderName}` can be retrieved from the [/info/bidders](../bidders.md) endpoint. - -### Sample Response - -This endpoint returns JSON like: - -```json -{ - "maintainer": { - "email": "info@prebid.org" - }, - "capabilities": { - "app": { - "mediaTypes": [ - "banner", - "native" - ] - }, - "site": { - "mediaTypes": [ - "banner", - "video", - "native" - ] - } - } -} -``` - -The fields hold the following information: - -- `maintainer.email`: A contact email for the Bidder's maintainer. In general, Bidder bugs should be logged as [issues](https://github.com/rubicon-project/prebid-server-java/issues)... but this contact email may be useful in case of emergency. -- `capabilities.app.mediaTypes`: A list of media types this Bidder supports from Mobile Apps. -- `capabilities.site.mediaTypes`: A list of media types this Bidder supports from Web pages. - -If `capabilities.app` or `capabilities.site` do not exist, then this Bidder does not support that platform. -OpenRTB Requests which define a `request.app` or `request.site` property will fail if a -`request.imp[i].ext.{bidderName}` exists for a Bidder which doesn't support them. - - -## `GET /info/bidders/all` - -This will respond with all possible bidders' details. - -### Sample Response - -This endpoint returns JSON like: - -```json -{ - "appnexus": { - "maintainer": { - "email": "info@prebid.org" - }, - "capabilities": { - "app": { - "mediaTypes": [ - "banner", - "native" - ] - }, - "site": { - "mediaTypes": [ - "banner", - "video", - "native" - ] - } - } - }, - "rubicon": { - "maintainer": { - "email": "header-bidding@rubiconproject.com" - }, - "capabilities": { - "app": { - "mediaTypes": [ - "banner" - ] - }, - "site": { - "mediaTypes": [ - "banner", - "video" - ] - } - } - } -} -``` diff --git a/docs/endpoints/lineitem-status.md b/docs/endpoints/lineitem-status.md new file mode 100644 index 00000000000..756161adf71 --- /dev/null +++ b/docs/endpoints/lineitem-status.md @@ -0,0 +1,18 @@ +# Line Item status + +This endpoint is available on admin port called `/pbs-admin/lineitem-status`. + +Giving read-only access to defined in parameters line item. Contains information about active delivery schedule, +ready at timestamp, spent tokens number and pacing frequency in milliseconds. + +## `GET /pbs-admin/lineitem-status?id=` + +### Query parameters: + +This endpoint supports the following query parameters: + +`id` - line item id indicate a the line item about which information is needed. + +### Sample request + +`GET http://prebid.site.com/pbs-admin/lineitem-status?id=lineItemId1` \ No newline at end of file diff --git a/docs/endpoints/logging/changelevel.md b/docs/endpoints/logging/changelevel.md deleted file mode 100644 index 933c53bb7e7..00000000000 --- a/docs/endpoints/logging/changelevel.md +++ /dev/null @@ -1,10 +0,0 @@ -# Change logging level endpoint - -This endpoint has a path `/logging/changelevel` by default (can be configured). - -This endpoint allows changing `org.prebid.server` logger level temporarily, mainly for troubleshooting production issues. - -### Query Params -- `level` - desired logging level to set; must be one of `error`, `warn`, `info`, `debug` -- `duration` - for how long to change level before it gets reset to original; there is an upper threshold for this -value set in [configuration](../../config-app.md) diff --git a/docs/endpoints/logging/httpinteraction.md b/docs/endpoints/logging/httpinteraction.md deleted file mode 100644 index 0f8731618ca..00000000000 --- a/docs/endpoints/logging/httpinteraction.md +++ /dev/null @@ -1,19 +0,0 @@ -# Enable HTTP interaction logging endpoint - -This endpoint has a path `/logging/httpinteraction` by default (can be configured). - -This endpoint turns on temporary logging of raw HTTP requests and responses, mainly for troubleshooting production issues. - -Interaction is logged at `INFO` level using `http-interaction` logback logger so make sure this logger has at least -`INFO` or more verbose level set ([logback configuration](../../../src/main/resources/logback-spring.xml) bundled in JAR -file sets this logger to `INFO` level). - -### Query Params -- `endpoint` - endpoint to be affected; valid values: [auction](../openrtb2/auction.md), [amp](../openrtb2/amp.md); -if omitted all valid endpoints will be affected -- `statusCode` - specifies that only interactions resulting in this response status code should be logged; -valid values: >=200 and <=500 -- `account` - specifies that only interactions involving this account should be logged -- `bidder` - name of the bidder whose adapter request will be logged; -- `limit` - number of interactions to log; there is an upper threshold for this value set in -[configuration](../../config-app.md) diff --git a/docs/endpoints/openrtb2/amp.md b/docs/endpoints/openrtb2/amp.md index 178b68d48e7..1072c52ee4b 100644 --- a/docs/endpoints/openrtb2/amp.md +++ b/docs/endpoints/openrtb2/amp.md @@ -54,12 +54,16 @@ An example Stored Request is given below: "id": "some-impression-id", "banner": {}, // The sizes are defined is set by your AMP tag query params "ext": { - "appnexus": { - // Insert parameters here - }, - "rubicon": { - // Insert parameters here - } + "prebid": { + "bidder": { + "appnexus": { + // Insert parameters here + }, + "rubicon": { + // Insert parameters here + } + } + } } } ] @@ -112,11 +116,14 @@ This endpoint supports the following query parameters: 6. `curl` - the canonical URL of the page 7. `timeout` - the publisher-specified timeout for the RTC callout - A configuration option `amp.default-timeout-ms` may be set to account for estimated latency so that Prebid Server can handle timeouts from adapters and respond to the AMP RTC request before it times out. -8. `us_privacy` - CCPA value for request. -9. `gdpr_consent` - GDPR value for request. +8. `gdpr_consent` - consent string for request. +9. `consent_string` - consent string for request. 10. `debug` - When set to `1`, the responses will contain extra info for debugging. 11. `account` - accountId parameter for Site object. 12. `slot` - tagId parameter for Imp object. +13. `gdpr_applies` - GDPR param for Regs Ext object. +14. `consent_type` - param to define what type of consent_string passed. +15. `attl_consent` - consentedProviders param for User Ext ConsentedProvidersSettings object. For information on how these get from AMP into this endpoint, see [this pull request adding the query params to the Prebid callout](https://github.com/ampproject/amphtml/pull/14155) and [this issue adding support for network-level RTC macros](https://github.com/ampproject/amphtml/issues/12374). @@ -126,11 +133,14 @@ If present, these will override parts of your Stored Request. 1. `ow`, `oh`, `w`, `h`, and/or `ms` will be used to set `request.imp[0].banner.format` if `request.imp[0].banner` is present. 2. `curl` will be used to set `request.site.page` 3. `timeout` will generally be used to set `request.tmax`. However, the Prebid Server host can [configure](../../config.md) their deploy to reduce this timeout for technical reasons. -4. `us_privacy` will be used to set `request.regs.ext.us_privacy` -5. `debug` will be used to set `request.test`, causing the `response.debug` to have extra debugging info in it. -6. `account` - will be used to set `site.publisher.id` parameter for Site object. -7. `slot` - will be used to set `tagId` parameter to overwrite Imp object. -8. `gdpr_consent` will be used to set `user.ext.consent`. +4. `debug` will be used to set `request.test`, causing the `response.debug` to have extra debugging info in it. +5. `account` - will be used to set `site.publisher.id` parameter for Site object. +6. `slot` - will be used to set `tagId` parameter to overwrite Imp object. +7. `gdpr_applies` will be used to set `request.regs.ext.gdpr` +8. `consent_type` will be used to check what should be done with consent string +9. `attl_consent` will be used to set `user.ext.ConsentedProvidersSettings.consented_providers`. +10. `gdpr_consent` will be used to set `request.regs.ext.us_privacy` or `user.ext.consent` +11. `consent_string` will be used to set `request.regs.ext.us_privacy` or `user.ext.consent`. This param has bigger priority then `gdpr_consent`. diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index ed43a951a1a..ea73be96719 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -39,8 +39,12 @@ The following is a "hello world" request which fetches the [Prebid sample ad](ht ] }, "ext": { - "appnexus": { - "placement_id": 10433394 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 10433394 + } + } } } } @@ -107,16 +111,15 @@ These fall under the `ext` property of JSON objects. If `ext` is defined on an object, Prebid Server uses the following conventions: -1. `ext` in "Request objects" uses `ext.prebid` and/or `ext.{anyBidderCode}`. +1. `ext` in "Request objects" uses `ext.prebid` and/or `ext.prebid.bidder.{anyBidderCode}`. 2. `ext` on "Response objects" uses `ext.prebid` and/or `ext.bidder`. The only exception here is the top-level `BidResponse`, because it's bidder-independent. -`ext.{anyBidderCode}` and `ext.bidder` extensions are defined by bidders. +`ext.prebid.bidder.{anyBidderCode}` and `ext.bidder` extensions are defined by bidders. `ext.prebid` extensions are defined by Prebid Server. Exceptions are made for extensions with "standard" recommendations: -- `request.user.ext.digitrust` -- To support Digitrust support - `request.regs.ext.gdpr` and `request.user.ext.consent` -- To support GDPR - `request.site.ext.amp` -- To identify AMP as the request source - `request.app.ext.source` and `request.app.ext.version` -- To support identifying the displaymanager/SDK in mobile apps. If given, we expect these to be strings. @@ -131,7 +134,7 @@ If you find that some bidders use Gross bids, publishers can adjust for it with "ext": { "prebid": { "bidadjustmentfactors": { - "appnexus: 0.8, + "appnexus": 0.8, "rubicon": 0.7 } } @@ -165,6 +168,7 @@ to set these params on the response at `response.seatbid[i].bid[j].ext.prebid.ta }, "includewinners": false, // Optional param defaulting to true "includebidderkeys": false // Optional param defaulting to true + "includeformat": false // Optional param defaulting to false } } } @@ -179,6 +183,8 @@ For backwards compatibility the following strings will also be allowed as price One of "includewinners" or "includebidderkeys" must be true (both default to true if unset). If both were false, then no targeting keys would be set, which is better configured by omitting targeting altogether. +The parameter "includeformat" indicates the type of the bid (banner, video, etc) for multiformat requests. It will add the key `hb_format` and/or `hb_format_{bidderName}` as per "includewinners" and "includebidderkeys" above. + MediaType PriceGranularity - when a single OpenRTB request contains multiple impressions with different mediatypes, or a single impression supports multiple formats, the different mediatypes may need different price granularities. If `mediatypepricegranularity` is present, `pricegranularity` would only be used for any mediatypes not specified. ``` @@ -287,11 +293,15 @@ This can be used to request bids from the same Bidder with different params. For "mimes": ["video/mp4"] }, "ext": { - "appnexus: { - "placement_id": 123 - }, - "districtm": { - "placement_id": 456 + "prebid": { + "bidder": { + "appnexus: { + "placement_id": 123 + }, + "districtm": { + "placement_id": 456 + } + } } } } @@ -322,7 +332,7 @@ For example, if the Request defines an alias like this: } ``` -then any `imp.ext.appnexus` params will actually go to the **rubicon** adapter. +then any `imp.ext.prebid.bidder.appnexus` params will actually go to the **rubicon** adapter. It will become impossible to fetch bids from Appnexus within that Request. #### Bidder Response Times @@ -683,11 +693,7 @@ Prebid Server adapters can support the [Prebid.js User ID modules](http://prebid "source": "pubcommon", "id":"11111111" } - ], - "digitrust": { - "id": "11111111111", - "keyv": 4 - } + ] } } } @@ -759,7 +765,7 @@ This section describes the ways in which Prebid Server **breaks** the OpenRTB sp Prebid Server returns a 400 on requests which define `wseat` or `bseat`. We may add support for these in the future, if there's compelling need. -Instead, an impression is only offered to a bidder if `bidrequest.imp[i].ext.{bidderName}` exists. +Instead, an impression is only offered to a bidder if `bidrequest.imp[i].ext.prebid.bidder.{bidderName}` exists. This supports publishers who want to sell different impressions to different bidders. @@ -768,8 +774,8 @@ This supports publishers who want to sell different impressions to different bid This endpoint returns a 400 if the request contains deprecated properties (e.g. `imp.wmin`, `imp.hmax`). The error message in the response should describe how to "fix" the request to make it legal. -If the message is unclear, please [log an issue](https://github.com/rubicon-project/prebid-server-java/issues) -or [submit a pull request](https://github.com/rubicon-project/prebid-server-java/pulls) to improve it. +If the message is unclear, please [log an issue](https://github.com/prebid/prebid-server-java/issues) +or [submit a pull request](https://github.com/prebid/prebid-server-java/pulls) to improve it. #### Determining Bid Security (http/https) diff --git a/docs/endpoints/setuid.md b/docs/endpoints/setuid.md deleted file mode 100644 index 5d19295f225..00000000000 --- a/docs/endpoints/setuid.md +++ /dev/null @@ -1,27 +0,0 @@ -# Saving User Syncs - -This endpoint is used during cookie syncs. For technical details, see the -[Cookie Sync developer docs](../developers/cookie-syncs.md). - -## `GET /setuid` - -This endpoint saves a UserID for a Bidder in the Cookie. Saved IDs will be recognized for 7 days before being considered "stale" and being re-synced. - -### Query Params - -- `bidder`: The FamilyName of the [Usersyncer](../../usersync/usersync.go) which is being synced. -- `uid`: The ID which the Bidder uses to recognize this user. If undefined, the UID for `bidder` will be deleted. -- `gdpr`: This should be `1` if GDPR is in effect, `0` if not, and undefined if the caller isn't sure -- `gdpr_consent`: This is required if `gdpr` is one, and optional (but encouraged) otherwise. If present, it should be an [unpadded base64-URL](https://tools.ietf.org/html/rfc4648#page-7) encoded [Vendor Consent String](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/Consent%20string%20and%20vendor%20list%20formats%20v1.1%20Final.md#vendor-consent-string-format-). -- `format`: is optional. When `format=img`, response will include `tracking-pixel.png` file. - -If the `gdpr` and `gdpr_consent` params are included, this endpoint will _not_ write a cookie unless: - -1. The Vendor ID set by the Prebid Server host company has permission to save cookies for that user. -2. The Prebid Server host company did not configure it to run with GDPR support. - -If in doubt, contact the company hosting Prebid Server and ask if they're GDPR-ready. - -### Sample request - -`GET http://prebid.site.com/setuid?bidder=adnxs&uid=12345&gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw` diff --git a/docs/endpoints/tracelog.md b/docs/endpoints/tracelog.md new file mode 100644 index 00000000000..47d94a4c943 --- /dev/null +++ b/docs/endpoints/tracelog.md @@ -0,0 +1,29 @@ +# Tracelog Endpoint + +This endpoint is available on admin port called `/pbs-admin/tracelog`. + +## POST `/pbs-admin/tracelog` + + Allows to configure logging level for specific account, line item and bidder code during some defined time period. + +### Query parameters: + +This endpoint supports the following query parameters: + + 1. `account` - specified an account for which logging level should be changed. (Not required, no default value) + 2. `lineItemId` - specified a lineItemId for which logging level should be changed. (Not required, no default value) + 3. `bidderCode`- specified a bidderCode for which logging level should be changed. (Not required, no default value) + 4. `level` - specified a log level to which logs should be updated. Allowed values are `info`, `warn`, `trace`, + `error`, `fatal`, `debug`. Default value if not defined is `error`. (Not required) + 5. `duration` - time in seconds during which changes will be applied. (Required). + +At least one of `account`, `lineItemId` or `bidderCode` should be specified. If more than one specified, +logic conjuction (and operation) is applied to parameters. + +### Request samples + +`GET http://prebid.site.com/pbs-admin/tracelog?account=1234&duration=100` - updates logging level to `error` level for account 1234 +for 100 seconds. + +`GET http://prebid.site.com/pbs-admin/tracelog?account=1234&bidder=rubicon&lineItemId=lineItemId1&level=debug&duration=100` - updates +logging level to warn, for account = 1234 and bidder = rubicon and lineItemId = lineItemId1 for 100 seconds. \ No newline at end of file diff --git a/docs/endpoints/vtrack.md b/docs/endpoints/vtrack.md deleted file mode 100644 index 4d7f0c5fbbd..00000000000 --- a/docs/endpoints/vtrack.md +++ /dev/null @@ -1,32 +0,0 @@ -# Event Endpoint - -This endpoint is used to cache puts in request. - -## `POST /vtrack` - -Inserts `bidId` and `accountId` into json parameter `value`, depending on the `modifyingVastXmlAllowed` bidder flag. -POST the modified JSON to Prebid Cache, and forward the results to the client. - -Response body example: - -```json -{"responses":[{"uuid":"94531ab8-c662-4fc7-904e-6b5d3be43b1a"}]} -``` - -### Query Params - -- `a`: Account id. Required parameter if there are any bidders with `modifyingVastXmlAllowed` equals true. - -### Sample request - -`POST https://prebid-server.rubiconproject.com/vtrack?a=ACCOUNT` - -```json -{"puts":[{ - "bidid": "BIDID", - "bidder": "BIDDER", - "type":"xml", - "value":"", - "ttlseconds":3600 -}]} -``` diff --git a/docs/metrics.md b/docs/metrics.md index 24b720b0bf4..3354a4f45f3 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -3,19 +3,41 @@ This document describes all metrics collected and submitted to configured backends by the Prebid Server. ## System metrics +Other available metrics not mentioned here can found at +[Vert.x Dropwizard Metrics](https://vertx.io/docs/vertx-dropwizard-metrics/java/#_the_metrics) page. + +### HTTP server metrics - `vertx.http.servers.[IP]:[PORT].open-netsockets.count` - current number of open connections where: - `[IP]` should be equal to IP address of bound network interface on cluster node for Prebid Server (for example: `0.0.0.0`) - `[PORT]` should be equal to `http.port` configuration property -Other available metrics can found at [Vert.x Dropwizard Metrics](https://vertx.io/docs/vertx-dropwizard-metrics/java/#_the_metrics) page. +### HTTP client metrics +- `vertx.http.clients.connections.{min,max,mean,p95,p99}` - how long connections live +- `vertx.http.clients.connections.{m1_rate,m5_rate,m15_rate,mean_rate}` - rate of the connection occurrences +- `vertx.http.clients.requests.{min,max,mean,p95,p99}` - request time +- `vertx.http.clients.requests.{m1_rate,m5_rate,m15_rate,mean_rate}` - request rate + +If HTTP client per destination endpoint metrics enabled: +- `vertx.http.clients.endpoint.[ENDPOINT]:[PORT].queue-delay.{min,max,mean,p95,p99}` - wait time of a pending request in the queue +- `vertx.http.clients.endpoint.[ENDPOINT]:[PORT].queue-size.count` - actual queue size +- `vertx.http.clients.endpoint.[ENDPOINT]:[PORT].open-netsockets.count` - actual number of open sockets to the endpoint +- `vertx.http.clients.endpoint.[ENDPOINT]:[PORT].usage.{min,max,mean,p95,p99}` - time of the delay between the request starts and the response ends +- `vertx.http.clients.endpoint.[ENDPOINT]:[PORT].in-use` - actual number of in-flight requests +- `vertx.http.clients.endpoint.[ENDPOINT]:[PORT].ttfb` - wait time between the request ended and its response begins + +### Database pool metrics +- `vertx.pools.datasouce.[DATASOURCE].queue-delay.{min,max,mean,p95,p99}` - duration of the delay to obtain the resource, i.e the wait time in the queue +- `vertx.pools.datasouce.[DATASOURCE].queue-size.counter` - the actual number of waiters in the queue +- `vertx.pools.datasouce.[DATASOURCE].usage.{min,max,mean,p95,p99}` - duration of the usage of the resource +- `vertx.pools.datasouce.[DATASOURCE].in-use.counter` - actual number of resources used + +where `[DATASOURCE]` is a data source name, `DEFAULT_DS` by defaul. ## General auction metrics - `app_requests` - number of requests received from applications - `no_cookie_requests` - number of requests without `uids` cookie or with one that didn't contain at least one live UID -- `safari_requests` - number of requests received from Safari browser -- `safari_no_cookie_requests` - number of requests received from Safari browser without `uids` cookie or with one that didn't contain at least one live UID - `request_time` - timer tracking how long did it take for Prebid Server to serve a request - `imps_requested` - number if impressions requested - `imps_banner` - number of banner impressions @@ -23,12 +45,9 @@ Other available metrics can found at [Vert.x Dropwizard Metrics](https://vertx.i - `imps_native` - number of native impressions - `imps_audio` - number of audio impressions - `requests.(ok|badinput|err|networkerr|blacklisted_account|blacklisted_app).(openrtb2-web|openrtb-app|amp|legacy)` - number of requests broken down by status and type +- `bidder-cardinality..requests` - number of requests targeting `` of bidders - `connection_accept_errors` - number of errors occurred while establishing HTTP connection -- `db_circuitbreaker_opened` - number of times database circuit breaker was opened (database is unavailable) -- `db_circuitbreaker_closed` - number of times database circuit breaker was closed (database is available again) - `db_query_time` - timer tracking how long did it take for database client to obtain the result for a query -- `httpclient_circuitbreaker_opened.` - number of times http client circuit breaker was opened (requested resource is unavailable) for particular host -- `httpclient_circuitbreaker_closed.` - number of times http client circuit breaker was closed (requested resource is available again) for particular host - `stored_requests_found` - number of stored requests that were found - `stored_requests_missing` - number of stored requests that were not found by provided stored request IDs - `stored_imps_found` - number of stored impressions that were found @@ -36,8 +55,16 @@ Other available metrics can found at [Vert.x Dropwizard Metrics](https://vertx.i - `geolocation_requests` - number of times geo location lookup was requested - `geolocation_successful` - number of successful geo location lookup responses - `geolocation_fail` - number of failed geo location lookup responses -- `geolocation_circuitbreaker_opened` - number of times geo location circuit breaker was opened (geo location resource is unavailable) -- `geolocation_circuitbreaker_closed` - number of times geo location circuit breaker was closed (geo location resource is available again) +- `circuit-breaker.http.named..opened` - state of the http client circuit breaker for a particular host: `1` means opened (requested resource is unavailable), `0` - closed +- `circuit.breaker.http.existing` - number of http client circuit breakers existing currently for all hosts +- `circuit-breaker.db.opened` - state of the database circuit breaker: `1` means opened (database is unavailable), `0` - closed +- `circuit-breaker.geo.opened` - state of the geo location circuit breaker: `1` means opened (geo location resource is unavailable), `0` - closed +- `timeout_notification.ok` - number of times bidders were successfully notified about timeouts +- `timeout_notification.failed` - number of unsuccessful attempts to notify bidders about timeouts +- `currency-rates.stale` - a flag indicating if currency rates obtained from external source are fresh (`0`) or stale (`1`) +- `settings.cache.(stored-request|amp-stored-request).refresh.(initialize|update).db_query_time` - timer tracking how long was settings cache population +- `settings.cache.(stored-request|amp-stored-request).refresh.(initialize|update).err` - number of errors during settings cache population +- `settings.cache.account.(hit|miss)` - number of times account was found or was missing in cache ## Auction per-adapter metrics - `adapter..no_cookie_requests` - number of requests made to `` that did not contain UID @@ -51,17 +78,21 @@ Other available metrics can found at [Vert.x Dropwizard Metrics](https://vertx.i - `adapter..(openrtb2-web|openrtb-app|amp|legacy).tcf.geo_masked` - number of requests made to `` that required geo information removed as a result of TCF enforcement for that bidder - `adapter..(openrtb2-web|openrtb-app|amp|legacy).tcf.request_blocked` - number of requests made to `` that were blocked as a result of TCF enforcement for that bidder - `adapter..(openrtb2-web|openrtb-app|amp|legacy).tcf.analytics_blocked` - number of requests made to `` that required analytics blocked as a result of TCF enforcement for that bidder +- `adapter..response.validation.size.(warn|err)` - number of banner bids received from the `` that had invalid size +- `adapter..response.validation.secure.(warn|err)` - number of bids received from the `` that had insecure creative while in secure context ## Auction per-account metrics Following metrics are collected and submitted if account is configured with `basic` verbosity: - `account..requests` - number of requests received from account with `` +- `account..response.validation.size.(warn|err)` - number of banner bids received from account with `` that had invalid size +- `account..response.validation.secure.(warn|err)` - number of bids received from account with `` that had insecure creative while in secure context Following metrics are collected and submitted if account is configured with `detailed` verbosity: - `account..requests.type.(openrtb2-web,openrtb-app,amp,legacy)` - number of requests received from account with `` broken down by type of incoming request -- `account...request_time` - timer tracking how long did it take to make a request to `` when incoming request was from `` -- `account...bids_received` - number of bids received from `` when incoming request was from `` -- `account...requests.(gotbids|nobid)` - number of requests made to `` broken down by result status when incoming request was from `` -- `account..requests.rejected` - number of rejected requests caused by incorrect `accountId` ([UnauthorizedAccountException.java](https://github.com/rubicon-project/prebid-server-java/blob/master/src/main/java/org/prebid/server/exception/UnauthorizedAccountException.java)) +- `account..requests.rejected` - number of rejected requests caused by incorrect `accountId` +- `account..adapter..request_time` - timer tracking how long did it take to make a request to `` when incoming request was from `` +- `account..adapter..bids_received` - number of bids received from `` when incoming request was from `` +- `account..adapter..requests.(gotbids|nobid)` - number of requests made to `` broken down by result status when incoming request was from `` ## General Prebid Cache metrics - `prebid_cache.requests.ok` - timer tracking how long did successful cache requests take @@ -84,12 +115,49 @@ Following metrics are collected and submitted if account is configured with `det - `usersync.bad_requests` - number of requests received with bidder not specified - `usersync..sets` - number of requests received resulted in `uid` cookie update for `` - `usersync..tcf.blocked` - number of requests received that didn't result in `uid` cookie update for `` because of lack of user consent for this action according to TCF +- `usersync..tcf.invalid` - number of requests received that are lacking of a valid consent string for `` in setuid endpoint +- `usersync.all.tcf.invalid` - number of requests received that are lacking of a valid consent string for all requested bidders cookieSync endpoint ## Privacy metrics - `privacy.tcf.(missing|invalid)` - number of requests lacking a valid consent string +- `privacy.tcf.(v1,v2).requests` - number of requests by TCF version - `privacy.tcf.(v1,v2).unknown-geo` - number of requests received from unknown geo region with consent string of particular version - `privacy.tcf.(v1,v2).in-geo` - number of requests received from TCF-concerned geo region with consent string of particular version - `privacy.tcf.(v1,v2).out-geo` - number of requests received outside of TCF-concerned geo region with consent string of particular version - `privacy.tcf.(v1,v2).vendorlist.(missing|ok|err|fallback)` - number of processed vendor lists of particular version - `privacy.usp.specified` - number of requests with a valid US Privacy string (CCPA) - `privacy.usp.opt-out` - number of requests that required privacy enforcement according to CCPA rules +- `privacy.lmt` - number of requests that required privacy enforcement according to LMT flag +- `privacy.coppa` - number of requests that required privacy enforcement according to COPPA rules + +## Analytics metrics +- `analytics..(auction|amp|video|cookie_sync|event|setuid).ok` - number of succeeded processed event requests +- `analytics..(auction|amp|video|cookie_sync|event|setuid).timeout` - number of event requests, failed with timeout cause +- `analytics..(auction|amp|video|cookie_sync|event|setuid).err` - number of event requests, failed with errors +- `analytics..(auction|amp|video|cookie_sync|event|setuid).badinput` - number of event requests, rejected with bad input cause + +## win notifications +- `win_notifications` - total number of win notifications. +- `win_requests` - total number of requests sent to user service for win notifications. +- `win_request_preparation_failed` - number of request failed validation and were not sent. +- `win_request_time` - latency between request to user service and response for win notifications. +- `win_request_failed` - number of failed request sent to user service for win notifications. +- `win_request_successful` - number of successful request sent to user service for win notifications. + +## user details +- `user_details_requests` - total number of requests sent to user service to get user details. +- `user_details_request_preparation_failed` - number of request failed validation and were not sent. +- `user_details_request_time` - latency between request to user service and response to get user details. +- `user_details_request_failed` - number of failed request sent to user service to get user details. +- `user_details_request_successful` - number of successful request sent to user service to get user details. + +## Programmatic guaranteed metrics +- `pg.planner_lineitems_received` - number of line items received from general planner. +- `pg.planner_requests` - total number of requests sent to general planner. +- `pg.planner_request_failed` - number of failed request sent to general planner. +- `pg.planner_request_successful` - number of successful requests sent to general planner. +- `pg.planner_request_time` - latency between request to general planner and its successful (200 OK) response. +- `pg.delivery_requests` - total number of requests to delivery stats service. +- `pg.delivery_request_failed` - number of failed requests to delivery stats service. +- `pg.delivery_request_successful` - number of successful requests to delivery stats service. +- `pg.delivery_request_time` - latency between request to delivery stats and its successful (200 OK) response. diff --git a/docs/pbs-java-and-go-features-review.md b/docs/pbs-java-and-go-features-review.md deleted file mode 100644 index 44eeae86428..00000000000 --- a/docs/pbs-java-and-go-features-review.md +++ /dev/null @@ -1,42 +0,0 @@ -# Feature Differences Overview - -[Detailed Differences Description](differenceBetweenPBSGo-and-Java.md) - - Feature | Java | Go -| --- | :---: | :---:| -GDPR TCF1.1 |+|+ -GDPR TCF2 |+|+ -Geo location (used for GDPR) |+|- -COPPA |+|+ -CCPA |+|+ -AMP |+|+ -Stored Requests |+|+ -Stored Responses |+|- -PBJS First Party data |+|- -Currency Conversion** |+|+ -Multiple root schains |+|- -Price Granularity |+|+ -Price Granularity per MediaType|+|- -User ID Module support |+|+ -Account exclude list |+|+ -Event Notification endpoint |+|- -Video ad support |+|+ -Long-form video endpoint |-|+ -IAB advertiser category mapping |-|+ -Aliases |+|+ -Video Impression Tracking endpoint |+|- -Cooperative Cookie Syncing |+|- -Circuit Breaker (Http, DB) |+|- -Operational metrics |+|+ -Supports both "debug" and "test" flags |+|- -All adapters ported to OpenRTB |+|- -Echo stored request video attributes in response |+|- -Accept account ID on AMP requests |+|- -Cache only-winning-bids flag |+|- -Remote File Downloader |+|- -Bidder Generator |+|- -Passing `request.ext.prebid.bidders.BIDDER` to corresponding bidder |+|- - -** -* PBS-Java Currency conversion supports finding intermediate conversion rate; -* PBS-Go Currency Conversion debug endpoint exposes more information, PBS-Java currently provides last updated time only; diff --git a/extra/bundle/README.md b/extra/bundle/README.md new file mode 100644 index 00000000000..7909b5d8c87 --- /dev/null +++ b/extra/bundle/README.md @@ -0,0 +1,2 @@ +# prebid-server-bundle-java +Combines Prebid Server Java and a host company's chosen modules diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml new file mode 100644 index 00000000000..10744e75ac5 --- /dev/null +++ b/extra/bundle/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.1.1.RELEASE + + + org.prebid + prebid-server-bundle + 1.73.0-SNAPSHOT + + prebid-server-bundle + + + 1.8 + + + + + org.prebid + prebid-server + 1.73.0-SNAPSHOT + + + org.prebid.server.hooks.modules + ortb2-blocking + 1.73.0-SNAPSHOT + + + + + ${project.name} + + + org.springframework.boot + spring-boot-maven-plugin + + org.prebid.server.Application + true + + + + + diff --git a/extra/modules/README.md b/extra/modules/README.md new file mode 100644 index 00000000000..1016a7189ee --- /dev/null +++ b/extra/modules/README.md @@ -0,0 +1 @@ +# Prebid Server modules diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml new file mode 100644 index 00000000000..02359e5ba75 --- /dev/null +++ b/extra/modules/ortb2-blocking/pom.xml @@ -0,0 +1,19 @@ + + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 1.73.0-SNAPSHOT + + + ortb2-blocking + 1.73.0-SNAPSHOT + + ortb2-blocking + OpenRTB2 Blocking module + diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java new file mode 100644 index 00000000000..2f7e0217ea9 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java @@ -0,0 +1,498 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.IterableUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.modules.ortb2.blocking.core.exception.InvalidAccountConfigurationException; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BidAttributeBlockingConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; +import org.prebid.server.hooks.modules.ortb2.blocking.core.util.MergeUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class AccountConfigReader { + + private static final String ATTRIBUTES_FIELD = "attributes"; + private static final String BADV_FIELD = "badv"; + private static final String BCAT_FIELD = "bcat"; + private static final String BAPP_FIELD = "bapp"; + private static final String BTYPE_FIELD = "btype"; + private static final String BATTR_FIELD = "battr"; + private static final String ENFORCE_BLOCKS_FIELD = "enforce-blocks"; + private static final String BLOCK_UNKNOWN_ADOMAIN_FIELD = "block-unknown-adomain"; + private static final String BLOCKED_ADOMAIN_FIELD = "blocked-adomain"; + private static final String ALLOWED_ADOMAIN_FOR_DEALS_FIELD = "allowed-adomain-for-deals"; + private static final String BLOCKED_ADV_CAT_FIELD = "blocked-adv-cat"; + private static final String BLOCK_UNKNOWN_ADV_CAT_FIELD = "block-unknown-adv-cat"; + private static final String ALLOWED_ADV_CAT_FOR_DEALS_FIELD = "allowed-adv-cat-for-deals"; + private static final String BLOCKED_APP_FIELD = "blocked-app"; + private static final String ALLOWED_APP_FOR_DEALS_FIELD = "allowed-app-for-deals"; + private static final String BLOCKED_BANNER_TYPE_FIELD = "blocked-banner-type"; + private static final String BLOCKED_BANNER_ATTR_FIELD = "blocked-banner-attr"; + private static final String ALLOWED_BANNER_ATTR_FOR_DEALS = "allowed-banner-attr-for-deals"; + private static final String ACTION_OVERRIDES_FIELD = "action-overrides"; + private static final String OVERRIDE_FIELD = "override"; + private static final String CONDITIONS_FIELD = "conditions"; + private static final String BIDDERS_FIELD = "bidders"; + private static final String MEDIA_TYPE_FIELD = "media-type"; + private static final String DEALIDS_FIELD = "deal-ids"; + + private static final String AUDIO_MEDIA_TYPE = "audio"; + private static final String VIDEO_MEDIA_TYPE = "video"; + private static final String BANNER_MEDIA_TYPE = "banner"; + private static final String NATIVE_MEDIA_TYPE = "native"; + + private final ObjectNode config; + private final String bidder; + private final boolean debugEnabled; + + private AccountConfigReader(ObjectNode config, String bidder, boolean debugEnabled) { + this.config = config; + this.bidder = bidder; + this.debugEnabled = debugEnabled; + } + + public static AccountConfigReader create(ObjectNode config, String bidder, boolean debugEnabled) { + return new AccountConfigReader(config, bidder, debugEnabled); + } + + public Result blockedAttributesFor(BidRequest bidRequest) { + if (attributes() == null) { + return Result.empty(); + } + + final Set requestMediaTypes = mediaTypesFrom(bidRequest); + + final Result> badv = + blockedAttribute(BADV_FIELD, String.class, BLOCKED_ADOMAIN_FIELD, requestMediaTypes); + final Result> bcat = + blockedAttribute(BCAT_FIELD, String.class, BLOCKED_ADV_CAT_FIELD, requestMediaTypes); + final Result> bapp = + blockedAttribute(BAPP_FIELD, String.class, BLOCKED_APP_FIELD, requestMediaTypes); + final Result>> btype = + blockedAttributesForImps(BTYPE_FIELD, Integer.class, BLOCKED_BANNER_TYPE_FIELD, bidRequest); + final Result>> battr = + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, bidRequest); + + return Result.of( + toBlockedAttributes(badv, bcat, bapp, btype, battr), + MergeUtils.mergeMessages(badv, bcat, bapp, btype, battr)); + } + + public Result responseBlockingConfigFor(BidderBid bidderBid) { + final Set bidMediaTypes = mediaTypesFrom(bidderBid); + final String dealid = bidderBid.getBid().getDealid(); + + final Result> badv = blockingConfigForAttribute( + BADV_FIELD, + String.class, + BLOCK_UNKNOWN_ADOMAIN_FIELD, + ALLOWED_ADOMAIN_FOR_DEALS_FIELD, + bidMediaTypes, + dealid); + final Result> bcat = blockingConfigForAttribute( + BCAT_FIELD, + String.class, + BLOCK_UNKNOWN_ADV_CAT_FIELD, + ALLOWED_ADV_CAT_FOR_DEALS_FIELD, + bidMediaTypes, + dealid); + final Result> bapp = blockingConfigForAttribute( + BAPP_FIELD, + String.class, + ALLOWED_APP_FOR_DEALS_FIELD, + bidMediaTypes, + dealid); + final Result> battr = blockingConfigForAttribute( + BATTR_FIELD, + Integer.class, + ALLOWED_BANNER_ATTR_FOR_DEALS, + bidMediaTypes, + dealid); + + final ResponseBlockingConfig response = ResponseBlockingConfig.builder() + .badv(badv.getValue()) + .bcat(bcat.getValue()) + .bapp(bapp.getValue()) + .battr(battr.getValue()) + .build(); + + final List warnings = MergeUtils.mergeMessages(badv, bcat, bapp, battr); + + return Result.of(response, warnings); + } + + private Result> blockedAttribute( + String attribute, Class attributeType, String fieldName, Set actualMediaTypes) { + + final JsonNode attributeConfig = attributeConfig(attribute); + if (attributeConfig == null) { + return Result.empty(); + } + + final Result override = + overrideFor(attributeConfig, actualMediaTypes, fieldName); + + final List result = mergeTypedArrays(attributeConfig, override.getValue(), attributeType, fieldName); + + return Result.of(result, override.getMessages()); + } + + private Result>> blockedAttributesForImps( + String attribute, Class attributeType, String fieldName, BidRequest bidRequest) { + + final Map> attributeValues = new HashMap<>(); + final List> results = new ArrayList<>(); + + for (final Imp imp : bidRequest.getImp()) { + final Result> attributeForImp = + blockedAttribute(attribute, attributeType, fieldName, mediaTypesFrom(imp)); + + if (attributeForImp.hasValue()) { + attributeValues.put(imp.getId(), attributeForImp.getValue()); + } + results.add(attributeForImp); + } + + return Result.of( + !attributeValues.isEmpty() ? attributeValues : null, + MergeUtils.mergeMessages(results)); + } + + private Result> blockingConfigForAttribute( + String attribute, + Class attributeType, + String blockUnknownField, + String allowedForDealsField, + Set bidMediaTypes, + String dealid) { + + final JsonNode attributeConfig = attributeConfig(attribute); + if (attributeConfig == null) { + return Result.empty(); + } + + final Result enforceBlocksOverrideResult = + overrideFor(attributeConfig, bidMediaTypes, ENFORCE_BLOCKS_FIELD); + final boolean enforceBlocks = + mergeBoolean(attributeConfig, enforceBlocksOverrideResult.getValue(), ENFORCE_BLOCKS_FIELD); + + // for attributes that don't support blocking bids with unknown values + final Result blockUnknownOverrideResult = blockUnknownField != null + ? overrideFor(attributeConfig, bidMediaTypes, blockUnknownField) + : Result.empty(); + final boolean blockUnknown = blockUnknownField != null + && mergeBoolean(attributeConfig, blockUnknownOverrideResult.getValue(), blockUnknownField); + + final Set dealExceptions = + StringUtils.isNotBlank(dealid) + ? mergeDealExceptions( + attributeConfig, + dealExceptionsFor(attributeConfig, dealid, allowedForDealsField), + attributeType, + allowedForDealsField) + : Collections.emptySet(); + + final BidAttributeBlockingConfig blockingConfig = + BidAttributeBlockingConfig.of(enforceBlocks, blockUnknown, dealExceptions); + final List warnings = MergeUtils.mergeMessages(enforceBlocksOverrideResult, blockUnknownOverrideResult); + + return Result.of(blockingConfig, warnings); + } + + private Result> blockingConfigForAttribute( + String attribute, + Class type, + String allowedForDealsField, + Set bidMediaTypes, + String dealid) { + + return blockingConfigForAttribute(attribute, type, null, allowedForDealsField, bidMediaTypes, dealid); + } + + private JsonNode attributes() { + return config != null ? objectNodeFrom(config, ATTRIBUTES_FIELD) : null; + } + + private JsonNode attributeConfig(String attribute) { + final JsonNode attributes = attributes(); + + return attributes != null ? objectNodeFrom(attributes, attribute) : null; + + } + + private static Set mediaTypesFrom(BidRequest bidRequest) { + return bidRequest.getImp().stream() + .flatMap(imp -> mediaTypesFrom(imp).stream()) + .collect(Collectors.toSet()); + } + + private static Set mediaTypesFrom(Imp imp) { + final Set mediaTypes = new HashSet<>(); + + if (imp.getAudio() != null) { + mediaTypes.add(AUDIO_MEDIA_TYPE); + } + if (imp.getVideo() != null) { + mediaTypes.add(VIDEO_MEDIA_TYPE); + } + if (imp.getBanner() != null) { + mediaTypes.add(BANNER_MEDIA_TYPE); + } + if (imp.getXNative() != null) { + mediaTypes.add(NATIVE_MEDIA_TYPE); + } + + return mediaTypes; + } + + private static Set mediaTypesFrom(BidderBid bidderBid) { + return Collections.singleton(bidderBid.getType().getName()); + } + + private Result overrideFor(JsonNode parent, Set actualMediaTypes, String field) { + final JsonNode actionOverrides = objectNodeFrom(parent, ACTION_OVERRIDES_FIELD); + final JsonNode overridesForField = actionOverrides != null ? objectArrayFrom(actionOverrides, field) : null; + if (overridesForField == null) { + return Result.empty(); + } + + final List specificBidderResults = new ArrayList<>(); + final List catchAllBidderResults = new ArrayList<>(); + + for (final JsonNode override : overridesForField) { + final JsonNode conditions = requireNonNull( + objectNodeFrom(override, CONDITIONS_FIELD), CONDITIONS_FIELD); + final List bidders = typedArrayFrom(conditions, String.class, BIDDERS_FIELD); + final List mediaTypes = typedArrayFrom(conditions, String.class, MEDIA_TYPE_FIELD); + + if (bidders == null && mediaTypes == null) { + throw new InvalidAccountConfigurationException( + String.format("%s field in account configuration must contain at least one of %s or %s", + CONDITIONS_FIELD, + BIDDERS_FIELD, + MEDIA_TYPE_FIELD)); + } + + final boolean catchAllBidders = bidders == null; + final boolean matchesBidder = catchAllBidders || bidders.contains(bidder); + final boolean matchesMediaTypes = + mediaTypes == null || !Collections.disjoint(mediaTypes, actualMediaTypes); + + if (matchesBidder && matchesMediaTypes) { + final JsonNode actions = requireNonNull(override.get(OVERRIDE_FIELD), OVERRIDE_FIELD); + + final List results = catchAllBidders ? catchAllBidderResults : specificBidderResults; + results.add(actions); + } + } + + return toResult(specificBidderResults, catchAllBidderResults, actualMediaTypes); + } + + private Result toResult( + List specificBidderResults, + List catchAllBidderResults, + Set actualMediaTypes) { + + final JsonNode value = ObjectUtils.firstNonNull( + specificBidderResults.size() > 0 ? specificBidderResults.get(0) : null, + catchAllBidderResults.size() > 0 ? catchAllBidderResults.get(0) : null); + final List warnings = debugEnabled && specificBidderResults.size() + catchAllBidderResults.size() > 1 + ? Collections.singletonList(String.format( + "More than one conditions matches request. Bidder: %s, request media types: %s", + bidder, + actualMediaTypes)) + : null; + + return Result.of(value, warnings); + } + + private static BlockedAttributes toBlockedAttributes( + Result> badv, + Result> bcat, + Result> bapp, + Result>> btype, + Result>> battr) { + + return badv.hasValue() || bcat.hasValue() || bapp.hasValue() || btype.hasValue() || battr.hasValue() + ? BlockedAttributes.builder() + .badv(badv.getValue()) + .bcat(bcat.getValue()) + .bapp(bapp.getValue()) + .btype(btype.getValue()) + .battr(battr.getValue()) + .build() + : null; + } + + private static List dealExceptionsFor(JsonNode parent, String dealid, String field) { + final JsonNode actionOverrides = objectNodeFrom(parent, ACTION_OVERRIDES_FIELD); + final JsonNode overridesForField = actionOverrides != null ? objectArrayFrom(actionOverrides, field) : null; + if (overridesForField == null) { + return Collections.emptyList(); + } + + final List results = new ArrayList<>(); + for (final JsonNode override : overridesForField) { + final JsonNode conditions = requireNonNull(objectNodeFrom(override, CONDITIONS_FIELD), CONDITIONS_FIELD); + final List dealIds = typedArrayFrom(conditions, String.class, DEALIDS_FIELD); + + if (dealIds == null) { + throw new InvalidAccountConfigurationException(String.format( + "%s field in account configuration must contain %s", + CONDITIONS_FIELD, + DEALIDS_FIELD)); + } + + if (dealIds.contains(dealid)) { + results.add(requireNonNull(override.get(OVERRIDE_FIELD), OVERRIDE_FIELD)); + } + } + + return results; + } + + private static List mergeTypedArrays(JsonNode parent, JsonNode override, Class type, String field) { + return MergeUtils.merge( + typedArrayFrom(parent, type, field), + override != null ? asTypedArray(override, type, OVERRIDE_FIELD) : null); + } + + private static boolean mergeBoolean(JsonNode parent, JsonNode override, String field) { + return BooleanUtils.toBooleanDefaultIfNull( + override != null ? typedAs(override, Boolean.class, OVERRIDE_FIELD) : null, + BooleanUtils.toBooleanDefaultIfNull(typedFieldFrom(parent, Boolean.class, field), false)); + } + + private static Set mergeDealExceptions( + JsonNode parent, List overrides, Class type, String field) { + + final List defaultValue = typedArrayFrom(parent, type, field); + if (defaultValue == null && CollectionUtils.isEmpty(overrides)) { + return Collections.emptySet(); + } + + final Set results = new HashSet<>(CollectionUtils.emptyIfNull(defaultValue)); + for (final JsonNode override : IterableUtils.emptyIfNull(overrides)) { + results.addAll(asTypedArray(override, type, field)); + } + + return results; + } + + private static List typedArrayFrom(JsonNode parent, Class type, String field) { + final JsonNode child = parent.get(field); + if (child == null) { + return null; + } + + return asTypedArray(child, type, field); + } + + private static List asTypedArray(JsonNode node, Class type, String field) { + if (node == null) { + return null; + } + + if (!node.isArray()) { + throw new InvalidAccountConfigurationException( + String.format("%s field in account configuration is not an array", field)); + } + + return StreamUtil.asStream(node.elements()) + .map(element -> typedAs(element, type, field)) + .collect(Collectors.toList()); + } + + private static T typedFieldFrom(JsonNode parent, Class type, String field) { + final JsonNode child = parent.get(field); + if (child == null) { + return null; + } + + return typedAs(child, type, field); + } + + @SuppressWarnings("unchecked") + private static T typedAs(JsonNode node, Class type, String field) { + final Function checker; + final Function converter; + + if (type.isAssignableFrom(String.class)) { + checker = JsonNode::isTextual; + converter = JsonNode::textValue; + } else if (type.isAssignableFrom(Integer.class)) { + checker = JsonNode::isInt; + converter = JsonNode::intValue; + } else if (type.isAssignableFrom(Boolean.class)) { + checker = JsonNode::isBoolean; + converter = JsonNode::booleanValue; + } else { + throw new IllegalArgumentException(String.format("Unsupported type: %s", type)); + } + + final Boolean hasDesiredType = checker.apply(node); + if (!hasDesiredType) { + throw new InvalidAccountConfigurationException( + String.format("%s field in account configuration has unexpected type. Expected %s", field, type)); + } + + return (T) converter.apply(node); + } + + private static JsonNode objectNodeFrom(JsonNode parent, String field) { + final JsonNode child = parent.get(field); + if (child == null) { + return null; + } + + if (!child.isObject()) { + throw new InvalidAccountConfigurationException( + String.format("%s field in account configuration is not an object", field)); + } + + return child; + } + + private static JsonNode objectArrayFrom(JsonNode parent, String field) { + final JsonNode child = parent.get(field); + if (child == null) { + return null; + } + + if (!child.isArray() || !StreamUtil.asStream(child.elements()).allMatch(JsonNode::isObject)) { + throw new InvalidAccountConfigurationException( + String.format("%s field in account configuration is not an array of objects", field)); + } + + return child; + } + + private static T requireNonNull(T object, String field) { + if (object == null) { + throw new InvalidAccountConfigurationException( + String.format("%s field in account configuration is missing", field)); + } + + return object; + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java new file mode 100644 index 00000000000..b2c7d492af4 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java @@ -0,0 +1,349 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.modules.ortb2.blocking.core.exception.InvalidAccountConfigurationException; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.AnalyticsResult; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BidAttributeBlockingConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; +import org.prebid.server.hooks.modules.ortb2.blocking.core.util.MergeUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class BidsBlocker { + + private static final String SUCCESS_BLOCKED_STATUS = "success-blocked"; + private static final String SUCCESS_ALLOW_STATUS = "success-allow"; + private static final String ATTRIBUTES_FIELD = "attributes"; + private static final String ADOMAIN_FIELD = "adomain"; + private static final String BCAT_FIELD = "bcat"; + private static final String BUNDLE_FIELD = "bundle"; + private static final String ATTR_FIELD = "attr"; + + private final List bids; + private final String bidder; + private final ObjectNode accountConfig; + private final BlockedAttributes blockedAttributes; + private final boolean debugEnabled; + + private BidsBlocker( + List bids, + String bidder, + ObjectNode accountConfig, + BlockedAttributes blockedAttributes, + boolean debugEnabled) { + + this.bids = bids; + this.bidder = bidder; + this.accountConfig = accountConfig; + this.blockedAttributes = blockedAttributes; + this.debugEnabled = debugEnabled; + } + + public static BidsBlocker create( + List bids, + String bidder, + ObjectNode accountConfig, + BlockedAttributes blockedAttributes, + boolean debugEnabled) { + + return new BidsBlocker( + Objects.requireNonNull(bids), + Objects.requireNonNull(bidder), + accountConfig, + blockedAttributes, + debugEnabled); + } + + public ExecutionResult block() { + final AccountConfigReader accountConfigReader = AccountConfigReader.create(accountConfig, bidder, debugEnabled); + + try { + final List> blockedBidResults = bids.stream() + .sequential() + .map(bid -> isBlocked(bid, accountConfigReader)) + .collect(Collectors.toList()); + + final Set blockedBidIndexes = IntStream.range(0, bids.size()) + .filter(index -> blockedBidResults.get(index).getValue().isBlocked()) + .boxed() + .collect(Collectors.toSet()); + + final BlockedBids blockedBids = !blockedBidIndexes.isEmpty() ? BlockedBids.of(blockedBidIndexes) : null; + final List warnings = MergeUtils.mergeMessages(blockedBidResults); + + return ExecutionResult.builder() + .value(blockedBids) + .debugMessages(blockedBids != null ? debugMessages(blockedBidIndexes, blockedBidResults) : null) + .warnings(warnings) + .analyticsResults(toAnalyticsResults(blockedBidResults)) + .build(); + } catch (InvalidAccountConfigurationException e) { + return debugEnabled ? ExecutionResult.withError(e.getMessage()) : ExecutionResult.empty(); + } + } + + private Result isBlocked(BidderBid bidderBid, AccountConfigReader accountConfigReader) { + final Result blockingConfigResult = + accountConfigReader.responseBlockingConfigFor(bidderBid); + final ResponseBlockingConfig blockingConfig = blockingConfigResult.getValue(); + + final BlockingResult blockingResult = BlockingResult.of( + bidderBid.getBid().getImpid(), + checkBadv(bidderBid, blockingConfig), + checkBcat(bidderBid, blockingConfig), + checkBapp(bidderBid, blockingConfig), + checkBattr(bidderBid, blockingConfig)); + + return Result.of(blockingResult, blockingConfigResult.getMessages()); + } + + private AttributeCheckResult checkBadv(BidderBid bidderBid, ResponseBlockingConfig blockingConfig) { + return checkAttribute( + bidderBid.getBid().getAdomain(), + blockingConfig.getBadv(), + blockedAttributeValues(BlockedAttributes::getBadv)); + } + + private AttributeCheckResult checkBcat(BidderBid bidderBid, ResponseBlockingConfig blockingConfig) { + return checkAttribute( + bidderBid.getBid().getCat(), + blockingConfig.getBcat(), + blockedAttributeValues(BlockedAttributes::getBcat)); + } + + private AttributeCheckResult checkBapp(BidderBid bidderBid, ResponseBlockingConfig blockingConfig) { + return checkAttribute( + bidderBid.getBid().getBundle(), + blockingConfig.getBapp(), + blockedAttributeValues(BlockedAttributes::getBapp)); + } + + private AttributeCheckResult checkBattr( + BidderBid bidderBid, ResponseBlockingConfig blockingConfig) { + + return checkAttribute( + bidderBid.getBid().getAttr(), + blockingConfig.getBattr(), + blockedAttributeValues(BlockedAttributes::getBattr, bidderBid.getBid().getImpid())); + } + + private AttributeCheckResult checkAttribute( + List attribute, BidAttributeBlockingConfig blockingConfig, List blockedAttributeValues) { + + if (blockingConfig == null || !blockingConfig.isEnforceBlocks()) { + return AttributeCheckResult.succeeded(); + } + + if (CollectionUtils.isEmpty(attribute)) { + return blockingConfig.isBlockUnknownValues() + ? AttributeCheckResult.failed() + : AttributeCheckResult.succeeded(); + } + + if (CollectionUtils.isNotEmpty(blockedAttributeValues)) { + final List blockedBidValues = attribute.stream() + .filter(blockedAttributeValues::contains) + .filter(blockedBidValue -> !blockingConfig.getAllowedValues().contains(blockedBidValue)) + .collect(Collectors.toList()); + + return CollectionUtils.isEmpty(blockedBidValues) + ? AttributeCheckResult.succeeded() + : AttributeCheckResult.failed(blockedBidValues); + } + + return AttributeCheckResult.succeeded(); + } + + private AttributeCheckResult checkAttribute( + String attribute, BidAttributeBlockingConfig blockingConfig, List blockedAttributeValues) { + + if (blockingConfig == null + || !blockingConfig.isEnforceBlocks() + || StringUtils.isEmpty(attribute) + || CollectionUtils.isEmpty(blockedAttributeValues)) { + + return AttributeCheckResult.succeeded(); + } + + final boolean blocked = + blockedAttributeValues.contains(attribute) && !blockingConfig.getAllowedValues().contains(attribute); + + return blocked + ? AttributeCheckResult.failed(Collections.singletonList(attribute)) + : AttributeCheckResult.succeeded(); + } + + private T blockedAttributeValues(Function getter) { + return blockedAttributes != null ? getter.apply(blockedAttributes) : null; + } + + private T blockedAttributeValues(Function> getter, String impId) { + final Map blockedAttributeValues = blockedAttributeValues(getter); + + return blockedAttributeValues != null ? blockedAttributeValues.get(impId) : null; + } + + private List debugMessages( + Set blockedBidIndexes, + List> blockedBidResults) { + + if (!debugEnabled) { + return null; + } + + return blockedBidIndexes.stream() + .map(index -> debugEntryFor(index, blockedBidResults.get(index).getValue())) + .collect(Collectors.toList()); + } + + private String debugEntryFor(int index, BlockingResult blockingResult) { + return String.format( + "Bid %d from bidder %s has been rejected, failed checks: %s", + index, + bidder, + blockingResult.getFailedChecks()); + } + + private List toAnalyticsResults(List> blockedBidResults) { + return blockedBidResults.stream() + .map(Result::getValue) + .map(blockingResult -> AnalyticsResult.of( + blockingResult.isBlocked() ? SUCCESS_BLOCKED_STATUS : SUCCESS_ALLOW_STATUS, + blockingResult.isBlocked() ? toAnalyticsResultValues(blockingResult) : null, + bidder, + blockingResult.getImpId())) + .collect(Collectors.toList()); + } + + private Map toAnalyticsResultValues(BlockingResult blockingResult) { + final Map values = new HashMap<>(); + + values.put(ATTRIBUTES_FIELD, blockingResult.getFailedChecks()); + + final AttributeCheckResult badvResult = blockingResult.getBadvCheckResult(); + if (badvResult.isFailed()) { + values.put(ADOMAIN_FIELD, badvResult.getFailedValues()); + } + final AttributeCheckResult bcatResult = blockingResult.getBcatCheckResult(); + if (bcatResult.isFailed()) { + values.put(BCAT_FIELD, bcatResult.getFailedValues()); + } + final AttributeCheckResult bappResult = blockingResult.getBappCheckResult(); + if (bappResult.isFailed()) { + values.put(BUNDLE_FIELD, bappResult.getFailedValues().get(0)); + } + final AttributeCheckResult battrResult = blockingResult.getBattrCheckResult(); + if (battrResult.isFailed()) { + values.put(ATTR_FIELD, battrResult.getFailedValues()); + } + + return values; + } + + @Value(staticConstructor = "of") + private static class BlockingResult { + + private static final String BADV_ATTRIBUTE = "badv"; + private static final String BCAT_ATTRIBUTE = "bcat"; + private static final String BAPP_ATTRIBUTE = "bapp"; + private static final String BATTR_ATTRIBUTE = "battr"; + + String impId; + + boolean blocked; + + AttributeCheckResult badvCheckResult; + + AttributeCheckResult bcatCheckResult; + + AttributeCheckResult bappCheckResult; + + AttributeCheckResult battrCheckResult; + + public static BlockingResult of( + String impId, + AttributeCheckResult badvCheckResult, + AttributeCheckResult bcatCheckResult, + AttributeCheckResult bappCheckResult, + AttributeCheckResult battrCheckResult) { + + final boolean blocked = + badvCheckResult.isFailed() + || bcatCheckResult.isFailed() + || bappCheckResult.isFailed() + || battrCheckResult.isFailed(); + + return of( + impId, + blocked, + badvCheckResult, + bcatCheckResult, + bappCheckResult, + battrCheckResult); + } + + public List getFailedChecks() { + if (!blocked) { + return null; + } + + final List failedChecks = new ArrayList<>(); + if (badvCheckResult.isFailed()) { + failedChecks.add(BADV_ATTRIBUTE); + } + if (bcatCheckResult.isFailed()) { + failedChecks.add(BCAT_ATTRIBUTE); + } + if (bappCheckResult.isFailed()) { + failedChecks.add(BAPP_ATTRIBUTE); + } + if (battrCheckResult.isFailed()) { + failedChecks.add(BATTR_ATTRIBUTE); + } + + return failedChecks; + } + } + + @Value(staticConstructor = "of") + private static class AttributeCheckResult { + + private static final AttributeCheckResult SUCCEEDED = AttributeCheckResult.of(false, null); + private static final AttributeCheckResult FAILED = AttributeCheckResult.of(true, Collections.emptyList()); + + boolean failed; + + List failedValues; + + @SuppressWarnings("unchecked") + public static AttributeCheckResult succeeded() { + return (AttributeCheckResult) SUCCEEDED; + } + + @SuppressWarnings("unchecked") + public static AttributeCheckResult failed() { + return (AttributeCheckResult) FAILED; + } + + public static AttributeCheckResult failed(List failedValues) { + return AttributeCheckResult.of(true, failedValues); + } + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BlockedAttributesResolver.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BlockedAttributesResolver.java new file mode 100644 index 00000000000..4d0932f958c --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BlockedAttributesResolver.java @@ -0,0 +1,59 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.ortb2.blocking.core.exception.InvalidAccountConfigurationException; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; + +import java.util.Objects; + +public class BlockedAttributesResolver { + + private final BidRequest bidRequest; + private final String bidder; + private final ObjectNode accountConfig; + private final boolean debugEnabled; + + private BlockedAttributesResolver( + BidRequest bidRequest, + String bidder, + ObjectNode accountConfig, + boolean debugEnabled) { + + this.bidRequest = bidRequest; + this.bidder = bidder; + this.accountConfig = accountConfig; + this.debugEnabled = debugEnabled; + } + + public static BlockedAttributesResolver create( + BidRequest bidRequest, + String bidder, + ObjectNode accountConfig, + boolean debugEnabled) { + + return new BlockedAttributesResolver( + Objects.requireNonNull(bidRequest), + Objects.requireNonNull(bidder), + accountConfig, + debugEnabled); + } + + public ExecutionResult resolve() { + final AccountConfigReader accountConfigReader = AccountConfigReader.create(accountConfig, bidder, debugEnabled); + + try { + final Result blockedAttributesResult = + accountConfigReader.blockedAttributesFor(bidRequest); + + return ExecutionResult.builder() + .value(blockedAttributesResult.getValue()) + .warnings(blockedAttributesResult.getMessages()) + .build(); + } catch (InvalidAccountConfigurationException e) { + return debugEnabled ? ExecutionResult.withError(e.getMessage()) : ExecutionResult.empty(); + } + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java new file mode 100644 index 00000000000..e4ecb58b810 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java @@ -0,0 +1,78 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core; + +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class RequestUpdater { + + private final BlockedAttributes blockedAttributes; + + private RequestUpdater(BlockedAttributes blockedAttributes) { + this.blockedAttributes = blockedAttributes; + } + + public static RequestUpdater create(BlockedAttributes blockedAttributes) { + return new RequestUpdater(Objects.requireNonNull(blockedAttributes)); + } + + public BidRequest update(BidRequest bidRequest) { + final List blockedAdomain = blockedAttributes.getBadv(); + final List blockedAdvCat = blockedAttributes.getBcat(); + final List blockedApp = blockedAttributes.getBapp(); + + return bidRequest.toBuilder() + .badv(CollectionUtils.isNotEmpty(blockedAdomain) ? blockedAdomain : bidRequest.getBadv()) + .bcat(CollectionUtils.isNotEmpty(blockedAdvCat) ? blockedAdvCat : bidRequest.getBcat()) + .bapp(CollectionUtils.isNotEmpty(blockedApp) ? blockedApp : bidRequest.getBapp()) + .imp(updateImps(bidRequest.getImp())) + .build(); + } + + private List updateImps(List imps) { + final Map> blockedBannerType = blockedAttributes.getBtype(); + final Map> blockedBannerAttr = blockedAttributes.getBattr(); + + if (MapUtils.isEmpty(blockedBannerType) && MapUtils.isEmpty(blockedBannerAttr)) { + return imps; + } + + return imps.stream() + .map(imp -> updateImp(imp, blockedBannerType, blockedBannerAttr)) + .collect(Collectors.toList()); + } + + private Imp updateImp( + Imp imp, + Map> blockedBannerType, + Map> blockedBannerAttr) { + + final String impId = imp.getId(); + final List btypeForImp = blockedBannerType != null ? blockedBannerType.get(impId) : null; + final List battrForImp = blockedBannerAttr != null ? blockedBannerAttr.get(impId) : null; + + if (CollectionUtils.isEmpty(btypeForImp) && CollectionUtils.isEmpty(battrForImp)) { + return imp; + } + + final Banner banner = imp.getBanner(); + final List existingBtype = banner != null ? banner.getBtype() : null; + final List existingBattr = banner != null ? banner.getBattr() : null; + final Banner.BannerBuilder bannerBuilder = banner != null ? banner.toBuilder() : Banner.builder(); + + return imp.toBuilder() + .banner(bannerBuilder + .btype(CollectionUtils.isNotEmpty(btypeForImp) ? btypeForImp : existingBtype) + .battr(CollectionUtils.isNotEmpty(battrForImp) ? battrForImp : existingBattr) + .build()) + .build(); + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/ResponseUpdater.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/ResponseUpdater.java new file mode 100644 index 00000000000..a23397e1018 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/ResponseUpdater.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core; + +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class ResponseUpdater { + + private final BlockedBids blockedBids; + + private ResponseUpdater(BlockedBids blockedBids) { + this.blockedBids = blockedBids; + } + + public static ResponseUpdater create(BlockedBids blockedBids) { + return new ResponseUpdater(Objects.requireNonNull(blockedBids)); + } + + public List update(List bids) { + return IntStream.range(0, bids.size()) + .filter(index -> !blockedBids.getIndexes().contains(index)) + .mapToObj(bids::get) + .collect(Collectors.toList()); + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/exception/InvalidAccountConfigurationException.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/exception/InvalidAccountConfigurationException.java new file mode 100644 index 00000000000..336405e0a58 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/exception/InvalidAccountConfigurationException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.exception; + +public class InvalidAccountConfigurationException extends RuntimeException { + + public InvalidAccountConfigurationException(String message) { + super(message); + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/AnalyticsResult.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/AnalyticsResult.java new file mode 100644 index 00000000000..5f9f41b267d --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/AnalyticsResult.java @@ -0,0 +1,17 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.model; + +import lombok.Value; + +import java.util.Map; + +@Value(staticConstructor = "of") +public class AnalyticsResult { + + String status; + + Map values; + + String bidder; + + String impId; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BidAttributeBlockingConfig.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BidAttributeBlockingConfig.java new file mode 100644 index 00000000000..6e3bbb404df --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BidAttributeBlockingConfig.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.model; + +import lombok.Value; + +import java.util.Set; + +@Value(staticConstructor = "of") +public class BidAttributeBlockingConfig { + + boolean enforceBlocks; + + boolean blockUnknownValues; + + Set allowedValues; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java new file mode 100644 index 00000000000..20b0cb9aecc --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java @@ -0,0 +1,22 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Builder +@Value +public class BlockedAttributes { + + List badv; + + List bcat; + + List bapp; + + Map> btype; + + Map> battr; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedBids.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedBids.java new file mode 100644 index 00000000000..f012a10097a --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedBids.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.model; + +import lombok.Value; + +import java.util.Set; + +@Value(staticConstructor = "of") +public class BlockedBids { + + Set indexes; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ExecutionResult.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ExecutionResult.java new file mode 100644 index 00000000000..1e8619cd1bb --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ExecutionResult.java @@ -0,0 +1,39 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.Collections; +import java.util.List; + +@Builder +@Value +public class ExecutionResult { + + private static final ExecutionResult EMPTY = ExecutionResult.builder().build(); + + T value; + + List errors; + + List warnings; + + List debugMessages; + + List analyticsResults; + + public boolean hasValue() { + return value != null; + } + + @SuppressWarnings("unchecked") + public static ExecutionResult empty() { + return (ExecutionResult) EMPTY; + } + + public static ExecutionResult withError(String error) { + return ExecutionResult.builder() + .errors(Collections.singletonList(error)) + .build(); + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java new file mode 100644 index 00000000000..5ce04803865 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java @@ -0,0 +1,17 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.model; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class ResponseBlockingConfig { + + BidAttributeBlockingConfig badv; + + BidAttributeBlockingConfig bcat; + + BidAttributeBlockingConfig bapp; + + BidAttributeBlockingConfig battr; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/Result.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/Result.java new file mode 100644 index 00000000000..76b2a8afc28 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/Result.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.model; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class Result { + + private static final Result EMPTY = of(null, null); + + T value; + + List messages; + + @SuppressWarnings("unchecked") + public static Result empty() { + return (Result) EMPTY; + } + + public static Result withValue(T value) { + return Result.of(value, null); + } + + public boolean hasValue() { + return value != null; + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/util/MergeUtils.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/util/MergeUtils.java new file mode 100644 index 00000000000..497bd51cd21 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/util/MergeUtils.java @@ -0,0 +1,44 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.util; + +import org.apache.commons.collections4.ListUtils; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public interface MergeUtils { + + static List mergeMessages(List> results) { + return mergeMessages(results.stream()); + } + + static List mergeMessages(Result... results) { + return mergeMessages(Arrays.stream(results)); + } + + static List mergeMessages(Stream> results) { + final List warnings = results + .map(Result::getMessages) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .distinct() + .collect(Collectors.toList()); + + return !warnings.isEmpty() ? warnings : null; + } + + static List merge(List defaultValues, List overriddenValues) { + if (overriddenValues == null) { + return defaultValues; + } + if (defaultValues == null) { + return overriddenValues; + } + + return ListUtils.union(defaultValues, overriddenValues); + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/model/ModuleContext.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/model/ModuleContext.java new file mode 100644 index 00000000000..fc95f74fc24 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/model/ModuleContext.java @@ -0,0 +1,30 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.model; + +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; + +import java.util.HashMap; +import java.util.Map; + +public class ModuleContext { + + private final Map blockedAttributes = new HashMap<>(); + + public static ModuleContext create(String bidder, BlockedAttributes blockedAttributes) { + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.blockedAttributes.put(bidder, blockedAttributes); + + return moduleContext; + } + + public ModuleContext with(String bidder, BlockedAttributes blockedAttributes) { + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.blockedAttributes.putAll(this.blockedAttributes); + moduleContext.blockedAttributes.put(bidder, blockedAttributes); + + return moduleContext; + } + + public BlockedAttributes blockedAttributesFor(String bidder) { + return blockedAttributes.get(bidder); + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/spring/config/Ortb2BlockingModuleConfiguration.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/spring/config/Ortb2BlockingModuleConfiguration.java new file mode 100644 index 00000000000..6f34e0c2157 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/spring/config/Ortb2BlockingModuleConfiguration.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.spring.config; + +import org.prebid.server.hooks.modules.ortb2.blocking.v1.Ortb2BlockingModule; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@ConditionalOnProperty(prefix = "hooks." + Ortb2BlockingModule.CODE, name = "enabled", havingValue = "true") +@Configuration +public class Ortb2BlockingModuleConfiguration { + + @Bean + Ortb2BlockingModule ortb2BlockingModule() { + return new Ortb2BlockingModule(); + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java new file mode 100644 index 00000000000..555ec0b4a7b --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java @@ -0,0 +1,71 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1; + +import io.vertx.core.Future; +import org.prebid.server.hooks.modules.ortb2.blocking.core.BlockedAttributesResolver; +import org.prebid.server.hooks.modules.ortb2.blocking.core.RequestUpdater; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; +import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; +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.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderRequestHook; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; + +public class Ortb2BlockingBidderRequestHook implements BidderRequestHook { + + private static final String CODE = "ortb2-blocking-bidder-request"; + + @Override + public Future> call( + BidderRequestPayload bidderRequestPayload, + BidderInvocationContext invocationContext) { + + final ExecutionResult blockedAttributesResult = + BlockedAttributesResolver + .create( + bidderRequestPayload.bidRequest(), + invocationContext.bidder(), + invocationContext.accountConfig(), + invocationContext.debugEnabled()) + .resolve(); + + final InvocationResultImpl.InvocationResultImplBuilder resultBuilder = + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(blockedAttributesResult.hasValue() + ? InvocationAction.update + : InvocationAction.no_action) + .warnings(blockedAttributesResult.getWarnings()) + .errors(blockedAttributesResult.getErrors()); + if (blockedAttributesResult.hasValue()) { + final BlockedAttributes blockedAttributes = blockedAttributesResult.getValue(); + final RequestUpdater requestUpdater = RequestUpdater.create(blockedAttributes); + resultBuilder + .payloadUpdate(payload -> + BidderRequestPayloadImpl.of(requestUpdater.update(payload.bidRequest()))) + .moduleContext(moduleContext(invocationContext, blockedAttributes)); + } + + return Future.succeededFuture(resultBuilder.build()); + } + + @Override + public String code() { + return CODE; + } + + private static ModuleContext moduleContext( + BidderInvocationContext invocationContext, BlockedAttributes blockedAttributes) { + + final Object moduleContext = invocationContext.moduleContext(); + final String bidder = invocationContext.bidder(); + + return moduleContext instanceof ModuleContext + ? ((ModuleContext) moduleContext).with(bidder, blockedAttributes) + : ModuleContext.create(bidder, blockedAttributes); + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingModule.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingModule.java new file mode 100644 index 00000000000..715d699ba58 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingModule.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Arrays; +import java.util.Collection; + +public class Ortb2BlockingModule implements Module { + + public static final String CODE = "ortb2-blocking"; + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return Arrays.asList( + new Ortb2BlockingBidderRequestHook(), + new Ortb2BlockingRawBidderResponseHook()); + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java new file mode 100644 index 00000000000..756b660818e --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java @@ -0,0 +1,118 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.modules.ortb2.blocking.core.BidsBlocker; +import org.prebid.server.hooks.modules.ortb2.blocking.core.ResponseUpdater; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.AnalyticsResult; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; +import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl; +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.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Ortb2BlockingRawBidderResponseHook implements RawBidderResponseHook { + + private static final String CODE = "ortb2-blocking-raw-bidder-response"; + + private static final String ENFORCE_BLOCKING_ACTIVITY = "enforce-blocking"; + private static final String SUCCESS_STATUS = "success"; + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + public Future> call( + BidderResponsePayload bidderResponsePayload, + BidderInvocationContext invocationContext) { + + final ExecutionResult blockedBidsResult = BidsBlocker + .create( + bidderResponsePayload.bids(), + invocationContext.bidder(), + invocationContext.accountConfig(), + blockedAttributesFrom(invocationContext), + invocationContext.debugEnabled()) + .block(); + + final InvocationResultImpl.InvocationResultImplBuilder resultBuilder = + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(blockedBidsResult.hasValue() + ? InvocationAction.update + : InvocationAction.no_action) + .errors(blockedBidsResult.getErrors()) + .warnings(blockedBidsResult.getWarnings()) + .debugMessages(blockedBidsResult.getDebugMessages()) + .analyticsTags(toAnalyticsTags(blockedBidsResult.getAnalyticsResults())); + if (blockedBidsResult.hasValue()) { + final ResponseUpdater responseUpdater = ResponseUpdater.create(blockedBidsResult.getValue()); + resultBuilder.payloadUpdate(payload -> + BidderResponsePayloadImpl.of(responseUpdater.update(payload.bids()))); + } + + return Future.succeededFuture(resultBuilder.build()); + } + + @Override + public String code() { + return CODE; + } + + private static BlockedAttributes blockedAttributesFrom(BidderInvocationContext invocationContext) { + final Object moduleContext = invocationContext.moduleContext(); + return moduleContext instanceof ModuleContext + ? ((ModuleContext) moduleContext).blockedAttributesFor(invocationContext.bidder()) + : null; + } + + private Tags toAnalyticsTags(List analyticsResults) { + if (CollectionUtils.isEmpty(analyticsResults)) { + return null; + } + + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ENFORCE_BLOCKING_ACTIVITY, + SUCCESS_STATUS, + toResults(analyticsResults)))); + } + + private List toResults(List analyticsResults) { + return analyticsResults.stream() + .map(this::toResult) + .collect(Collectors.toList()); + } + + private Result toResult(AnalyticsResult analyticsResult) { + return ResultImpl.of( + analyticsResult.getStatus(), + toObjectNode(analyticsResult.getValues()), + AppliedToImpl.builder() + .bidders(Collections.singletonList(analyticsResult.getBidder())) + .impIds(Collections.singletonList(analyticsResult.getImpId())) + .build()); + } + + private ObjectNode toObjectNode(Map values) { + return values != null ? mapper.valueToTree(values) : null; + } +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java new file mode 100644 index 00000000000..bd394217b21 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; + +import com.iab.openrtb.request.BidRequest; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class BidderRequestPayloadImpl implements BidderRequestPayload { + + BidRequest bidRequest; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java new file mode 100644 index 00000000000..72d678c89a5 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; + +import java.util.List; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class BidderResponsePayloadImpl implements BidderResponsePayload { + + List bids; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java new file mode 100644 index 00000000000..48be15fdf37 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; +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.PayloadUpdate; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.List; + +@Accessors(fluent = true) +@Builder +@Value +public class InvocationResultImpl implements InvocationResult { + + InvocationStatus status; + + String message; + + InvocationAction action; + + PayloadUpdate payloadUpdate; + + List errors; + + List warnings; + + List debugMessages; + + ModuleContext moduleContext; + + Tags analyticsTags; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java new file mode 100644 index 00000000000..484489a5e6f --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Result; + +import java.util.List; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ActivityImpl implements Activity { + + String name; + + String status; + + List results; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java new file mode 100644 index 00000000000..2971cc40d6e --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.AppliedTo; + +import java.util.List; + +@Accessors(fluent = true) +@Value +@Builder +public class AppliedToImpl implements AppliedTo { + + List impIds; + + List bidders; + + boolean request; + + boolean response; + + List bidIds; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java new file mode 100644 index 00000000000..5405799e25f --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Result; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ResultImpl implements Result { + + String status; + + ObjectNode values; + + AppliedTo appliedTo; +} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java new file mode 100644 index 00000000000..9f0432b9e2f --- /dev/null +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.List; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class TagsImpl implements Tags { + + List activities; +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java new file mode 100644 index 00000000000..a19edcc9a99 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java @@ -0,0 +1,1179 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Audio; +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.Video; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.Test; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AllowedForDealsOverride; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ArrayOverride; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.BooleanOverride; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Conditions; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.DealsConditions; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ModuleConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.exception.InvalidAccountConfigurationException; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BidAttributeBlockingConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class AccountConfigReaderTest { + + private static final ObjectMapper mapper = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + @Test + public void blockedAttributesForShouldReturnEmptyResultWhenNoAccountConfig() { + // given + final AccountConfigReader reader = AccountConfigReader.create(null, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(emptyRequest())).isEqualTo(Result.empty()); + } + + @Test + public void blockedAttributesForShouldReturnEmptyResultWhenNoAttributesField() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(null)); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(emptyRequest())).isEqualTo(Result.empty()); + } + + @Test + public void blockedAttributesForShouldReturnEmptyResultWhenNoBlockedAttributes() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder().build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(emptyRequest())).isEqualTo(Result.empty()); + } + + @Test + public void blockedAttributesForShouldReturnEmptyResultWhenNoBlockedAdomains() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder().build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(emptyRequest())).isEqualTo(Result.empty()); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenAttributesIsNotObject() { + // given + final ObjectNode accountConfig = mapper.createObjectNode().put("attributes", 1); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("attributes field in account configuration is not an object"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenBadvIsNotObject() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .put("badv", 1)); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("badv field in account configuration is not an object"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenBlockedAdomainsIsNotArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .put("blocked-adomain", 1))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("blocked-adomain field in account configuration is not an array"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenBlockedAdomainsIsNotStringArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(1) + .add("domain2.com")))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("blocked-adomain field in account configuration has unexpected type. " + + "Expected class java.lang.String"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenBadvActionOverridesIsNotObject() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .put("action-overrides", 1))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("action-overrides field in account configuration is not an object"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenBadvActionOverridesBlockedAdomainIsNotObjectArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(1) + .add(mapper.createObjectNode()))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("blocked-adomain field in account configuration is not an array of objects"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenOverridesHasNoConditions() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(mapper.createObjectNode()))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("conditions field in account configuration is missing"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenConditionsIsNotObject() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(mapper.createObjectNode() + .put("conditions", 1)))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("conditions field in account configuration is not an object"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenBadvActionOverridesBlockedAdomainConditionsIsEmpty() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(mapper.createObjectNode() + .set("conditions", mapper.createObjectNode())))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("conditions field in account configuration must contain at least one of bidders or " + + "media-type"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenConditionBiddersIsNotArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(mapper.createObjectNode() + .set("conditions", mapper.createObjectNode() + .put("bidders", 1))))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("bidders field in account configuration is not an array"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenConditionBiddersIsNotStringArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(mapper.createObjectNode() + .set("conditions", mapper.createObjectNode() + .set("bidders", mapper.createArrayNode() + .add(1) + .add("abc")))))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("bidders field in account configuration has unexpected type. " + + "Expected class java.lang.String"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenConditionMediaTypeIsNotArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(mapper.createObjectNode() + .set("conditions", mapper.createObjectNode() + .put("media-type", 1))))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("media-type field in account configuration is not an array"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenConditionMediaTypeIsNotStringArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(mapper.createObjectNode() + .set("conditions", mapper.createObjectNode() + .set("media-type", mapper.createArrayNode() + .add(1) + .add("abc")))))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("media-type field in account configuration has unexpected type. " + + "Expected class java.lang.String"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenActionOverridesHasNoOverride() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(mapper.createObjectNode() + .set("conditions", mapper.createObjectNode() + .set("bidders", mapper.createArrayNode() + .add("bidder1")))))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("override field in account configuration is missing"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenBadvBlockedAdomainOverrideIsNotArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(mapper.createObjectNode() + .set("conditions", mapper.createObjectNode() + .set("bidders", mapper.createArrayNode() + .add("bidder1"))) + .put("override", 1)))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("override field in account configuration is not an array"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenBlockedAdomainOverrideIsNotStringArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("blocked-adomain", mapper.createArrayNode() + .add(mapper.createObjectNode() + .set("conditions", mapper.createObjectNode() + .set("bidders", mapper.createArrayNode() + .add("bidder1"))) + .set("override", mapper.createArrayNode() + .add(1) + .add("abc"))))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("override field in account configuration has unexpected type. " + + "Expected class java.lang.String"); + } + + @Test + public void blockedAttributesForShouldReturnErrorWhenBlockedBannerTypeIsNotIntegerArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("btype", mapper.createObjectNode() + .set("blocked-banner-type", mapper.createArrayNode() + .add(1) + .add("type2")))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("blocked-banner-type field in account configuration has unexpected type. " + + "Expected class java.lang.Integer"); + } + + @Test + public void blockedAttributesForShouldReturnResultWithDefaultBadvWhenNoOverrides() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(emptyRequest())).isEqualTo( + Result.withValue(attributesWithBadv(asList("domain1.com", "domain2.com")))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithDefaultBadvWhenOverridesDoNotMatchRequestByBidder() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder2"), null), + singletonList("domain3.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(emptyRequest())).isEqualTo( + Result.withValue(attributesWithBadv(asList("domain1.com", "domain2.com")))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithDefaultBadvWhenOverridesDoNotMatchRequestByMediaType() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList("domain3.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(emptyRequest())).isEqualTo( + Result.withValue(attributesWithBadv(asList("domain1.com", "domain2.com")))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithBadvFromDefaultAndOverridesWhenOverridesMatchRequest() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain3.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(emptyRequest())).isEqualTo( + Result.withValue(attributesWithBadv(asList("domain1.com", "domain2.com", "domain3.com")))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithBadvFromOverridesWhenMatchRequestByBidderAndMediaType() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(asList("bidder1", "bidder2"), asList("video", "banner")), + singletonList("domain3.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + final BidRequest request = BidRequest.builder() + .imp(asList( + Imp.builder().banner(Banner.builder().build()).build(), + Imp.builder().xNative(Native.builder().build()).build())) + .build(); + + // when and then + assertThat(reader.blockedAttributesFor(request)) + .isEqualTo(Result.withValue(attributesWithBadv(singletonList("domain3.com")))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithBadvFromOverridesWhenMatchRequestByBidder() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain3.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(emptyRequest())).isEqualTo( + Result.withValue(attributesWithBadv(singletonList("domain3.com")))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithBadvFromOverridesWhenMatchRequestByMediaType() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(null, singletonList("video")), + singletonList("domain3.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(request(imp -> imp.video(Video.builder().build())))).isEqualTo( + Result.withValue(attributesWithBadv(singletonList("domain3.com")))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithBadvFromOverridesWhenMatchRequestByDifferentMediaTypes() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), asList("audio", "video", + "banner", + "native")), + singletonList("domain3.com"))))) + .build()) + .build())); + + // when and then + assertThat(AccountConfigReader + .create(accountConfig, "bidder1", true) + .blockedAttributesFor(request(imp -> imp.audio(Audio.builder().build())))) + .isEqualTo(Result.withValue(attributesWithBadv(singletonList("domain3.com")))); + assertThat(AccountConfigReader + .create(accountConfig, "bidder1", true) + .blockedAttributesFor(request(imp -> imp.video(Video.builder().build())))) + .isEqualTo(Result.withValue(attributesWithBadv(singletonList("domain3.com")))); + assertThat(AccountConfigReader + .create(accountConfig, "bidder1", true) + .blockedAttributesFor(request(imp -> imp.banner(Banner.builder().build())))) + .isEqualTo(Result.withValue(attributesWithBadv(singletonList("domain3.com")))); + assertThat(AccountConfigReader + .create(accountConfig, "bidder1", true) + .blockedAttributesFor(request(imp -> imp.xNative(Native.builder().build())))) + .isEqualTo(Result.withValue(attributesWithBadv(singletonList("domain3.com")))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithBadvAndWarningFromOverridesWhenMultipleSpecificMatches() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + asList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList("domain3.com")), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("banner")), + singletonList("domain4.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader + .blockedAttributesFor(request(imp -> imp + .video(Video.builder().build()) + .banner(Banner.builder().build())))) + .isEqualTo(Result.of( + attributesWithBadv(singletonList("domain3.com")), + singletonList("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner, video]"))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithoutWarningWhenMultipleSpecificMatchesAndDebugDisabled() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + asList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList("domain3.com")), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("banner")), + singletonList("domain4.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", false); + + // when and then + assertThat(reader + .blockedAttributesFor(request(imp -> imp + .video(Video.builder().build()) + .banner(Banner.builder().build())))) + .isEqualTo(Result.withValue(attributesWithBadv(singletonList("domain3.com")))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithBadvAndWarningFromOverridesWhenMultipleSpecificAndCatchAllMatches() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + asList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList("domain3.com")), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("banner")), + singletonList("domain4.com")), + ArrayOverride.of( + Conditions.of(null, singletonList("video")), + singletonList("domain5.com")), + ArrayOverride.of( + Conditions.of(null, singletonList("banner")), + singletonList("domain6.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader + .blockedAttributesFor(request(imp -> imp + .video(Video.builder().build()) + .banner(Banner.builder().build())))) + .isEqualTo(Result.of( + attributesWithBadv(singletonList("domain3.com")), + singletonList("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner, video]"))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithBadvAndWarningFromOverridesWhenMultipleCatchAllMatches() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + asList( + ArrayOverride.of( + Conditions.of(null, singletonList("video")), + singletonList("domain5.com")), + ArrayOverride.of( + Conditions.of(null, singletonList("banner")), + singletonList("domain6.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader + .blockedAttributesFor(request(imp -> imp + .video(Video.builder().build()) + .banner(Banner.builder().build())))) + .isEqualTo(Result.of( + attributesWithBadv(singletonList("domain5.com")), + singletonList("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner, video]"))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverrides() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .btype(Attribute.btypeBuilder() + .actionOverrides(AttributeActionOverrides.blocked(asList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList(1)), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList(2)), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("banner")), + singletonList(3)), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("banner")), + singletonList(4))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + final Map> expectedBtype = new HashMap<>(); + expectedBtype.put("impId1", singletonList(1)); + expectedBtype.put("impId2", singletonList(3)); + assertThat(reader + .blockedAttributesFor(BidRequest.builder() + .imp(asList( + Imp.builder().id("impId1").video(Video.builder().build()).build(), + Imp.builder().id("impId2").banner(Banner.builder().build()).build())) + .build())) + .isEqualTo(Result.of( + BlockedAttributes.builder().btype(expectedBtype).build(), + asList( + "More than one conditions matches request. Bidder: bidder1, " + + "request media types: [video]", + "More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner]"))); + } + + @Test + public void blockedAttributesForShouldReturnResultWithAllAttributes() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .blocked(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .blocked(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("app3"))))) + .build()) + .btype(Attribute.btypeBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .battr(Attribute.battrBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo( + Result.withValue(BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com", "domain3.com")) + .bcat(asList("cat1", "cat2", "cat3")) + .bapp(asList("app1", "app2", "app3")) + .btype(singletonMap("impId1", asList(1, 2, 3))) + .battr(singletonMap("impId1", asList(1, 2, 3))) + .build())); + } + + @Test + public void responseBlockingConfigForShouldReturnErrorWhenDefaultEnforceBlocksIsNotBoolean() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .put("enforce-blocks", 1))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.responseBlockingConfigFor(bid())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("enforce-blocks field in account configuration has unexpected type. " + + "Expected class java.lang.Boolean"); + } + + @Test + public void responseBlockingConfigForShouldReturnErrorWhenDefaultAllowedAdomainIsNotArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .put("allowed-adomain-for-deals", 1))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.responseBlockingConfigFor(bid())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("allowed-adomain-for-deals field in account configuration is not an array"); + } + + @Test + public void responseBlockingConfigForShouldReturnErrorWhenDefaultAllowedAdomainIsNotStringArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("allowed-adomain-for-deals", mapper.createArrayNode() + .add(1) + .add("domain1.com")))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.responseBlockingConfigFor(bid())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("allowed-adomain-for-deals field in account configuration has unexpected type. " + + "Expected class java.lang.String"); + } + + @Test + public void responseBlockingConfigForShouldReturnErrorWhenDefaultAllowedBlockedAttrIsNotIntegerArray() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("battr", mapper.createObjectNode() + .set("allowed-banner-attr-for-deals", mapper.createArrayNode() + .add(1) + .add("domain1.com")))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.responseBlockingConfigFor(bid())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("allowed-banner-attr-for-deals field in account configuration has unexpected type. " + + "Expected class java.lang.Integer"); + } + + @Test + public void responseBlockingConfigForShouldReturnErrorWhenDealConditionsIsEmpty() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .set("attributes", mapper.createObjectNode() + .set("badv", mapper.createObjectNode() + .set("action-overrides", mapper.createObjectNode() + .set("allowed-adomain-for-deals", mapper.createArrayNode() + .add(mapper.createObjectNode() + .set("conditions", mapper.createObjectNode())))))); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThatThrownBy(() -> reader.responseBlockingConfigFor(bid())) + .isInstanceOf(InvalidAccountConfigurationException.class) + .hasMessage("conditions field in account configuration must contain deal-ids"); + } + + @Test + public void responseBlockingConfigForShouldReturnDefaultResultWhenNoValues() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder().build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isNotNull(); + assertThat(result.getValue().getBadv()).isEqualTo(BidAttributeBlockingConfig.of(false, false, emptySet())); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnResultWithoutDealExceptionWhenNonDealBid() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .allowedForDeals(asList("domain1.com", "domain2.com")) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + final BidderBid bid = BidderBid.of(Bid.builder().build(), BidType.banner, "USD"); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid)).satisfies(result -> { + assertThat(result.getValue()).isNotNull(); + assertThat(result.getValue().getBadv()).isEqualTo(BidAttributeBlockingConfig.of(false, false, emptySet())); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnResultWithDefaultValuesWhenNoOverrides() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("domain1.com", "domain2.com")) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isNotNull(); + assertThat(result.getValue().getBadv()).isEqualTo(BidAttributeBlockingConfig.of( + true, true, new HashSet<>(asList("domain1.com", "domain2.com")))); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnResultWithDefaultValuesWhenOverridesDoNotMatchRequest() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder2"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder2"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid2")), + singletonList("domain3.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isNotNull(); + assertThat(result.getValue().getBadv()).isEqualTo(BidAttributeBlockingConfig.of( + true, true, new HashSet<>(asList("domain1.com", "domain2.com")))); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnResultWithDefaultAndOverridesWhenOverridesMatchRequest() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + true)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("domain3.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isNotNull(); + assertThat(result.getValue().getBadv()).isEqualTo(BidAttributeBlockingConfig.of( + false, true, new HashSet<>(asList("domain1.com", "domain2.com", "domain3.com")))); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnResultWithOverridesWhenOverridesMatchRequest() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + true)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("domain3.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isNotNull(); + assertThat(result.getValue().getBadv()).isEqualTo(BidAttributeBlockingConfig.of( + false, true, new HashSet<>(singletonList("domain3.com")))); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnResultWithWarningAndOverrideWhenMultipleMatches() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blockFlags( + asList( + BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + true), + BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + asList( + BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false), + BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + true)))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isNotNull(); + assertThat(result.getValue().getBadv()).isEqualTo(BidAttributeBlockingConfig.of(true, false, emptySet())); + assertThat(result.getMessages()).containsExactly( + "More than one conditions matches request. Bidder: bidder1, request media types: [banner]"); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnResultWithMergedDealExceptionsWhenMultipleMatches() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.allowedForDeals( + asList( + AllowedForDealsOverride.of( + DealsConditions.of(asList("dealid1", "dealid2")), + singletonList("domain3.com")), + AllowedForDealsOverride.of( + DealsConditions.of(asList("dealid1", "dealid3")), + singletonList("domain4.com"))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isNotNull(); + assertThat(result.getValue().getBadv()).isEqualTo(BidAttributeBlockingConfig.of( + false, false, new HashSet<>(asList("domain3.com", "domain4.com")))); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnAllAttributes() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .enforceBlocks(true) + .allowedForDeals(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("app3"))))) + .build()) + .battr(Attribute.battrBuilder() + .enforceBlocks(true) + .allowedForDeals(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isEqualTo(ResponseBlockingConfig.builder() + .badv(BidAttributeBlockingConfig.of( + false, false, new HashSet<>(asList("domain1.com", "domain2.com", "domain3.com")))) + .bcat(BidAttributeBlockingConfig.of( + false, false, new HashSet<>(asList("cat1", "cat2", "cat3")))) + .bapp(BidAttributeBlockingConfig.of( + false, false, new HashSet<>(asList("app1", "app2", "app3")))) + .battr(BidAttributeBlockingConfig.of( + false, false, new HashSet<>(asList(1, 2, 3)))) + .build()); + assertThat(result.getMessages()).isNull(); + }); + } + + private static BidRequest emptyRequest() { + return BidRequest.builder() + .imp(singletonList(Imp.builder().build())) + .build(); + } + + private static BidRequest request(UnaryOperator impCustomizer) { + return BidRequest.builder() + .imp(singletonList(impCustomizer.apply(Imp.builder()).build())) + .build(); + } + + private static BidderBid bid() { + return BidderBid.of( + Bid.builder().dealid("dealid1").build(), + BidType.banner, + "USD"); + } + + private static BlockedAttributes attributesWithBadv(List badv) { + return BlockedAttributes.builder().badv(badv).build(); + } + + private static ObjectNode toObjectNode(ModuleConfig config) { + return mapper.valueToTree(config); + } +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java new file mode 100644 index 00000000000..f7b7278fb4a --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java @@ -0,0 +1,460 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.Test; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ModuleConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.AnalyticsResult; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; + +class BidsBlockerTest { + + private static final ObjectMapper mapper = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + @Test + public void shouldReturnEmptyResultWhenNoBlockingResponseConfig() { + // given + final List bids = singletonList(bid()); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", null, null, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } + + @Test + public void shouldReturnEmptyResultWithErrorWhenInvalidAccountConfig() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .put("attributes", 1); + + final List bids = singletonList(bid()); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, null, true); + + // when and then + assertThat(blocker.block()).isEqualTo(ExecutionResult.builder() + .errors(singletonList("attributes field in account configuration is not an object")) + .build()); + } + + @Test + public void shouldReturnEmptyResultWithoutErrorWhenInvalidAccountConfigAndDebugDisabled() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .put("attributes", 1); + + final List bids = singletonList(bid()); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, null, false); + + // when and then + assertThat(blocker.block()).isEqualTo(ExecutionResult.empty()); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithoutAdomainAndBlockUnknownFalse() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(false) + .build()) + .build())); + + // when + final List bids = singletonList(bid()); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, null, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithoutAdomainAndEnforceBlocksFalseAndBlockUnknownTrue() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(false) + .blockUnknown(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid()); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, null, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } + + @Test + public void shouldReturnResultWithBidWhenBidWithoutAdomainAndBlockUnknownTrue() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid()); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, null, false); + + // when and then + assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithBlockedAdomainAndEnforceBlocksFalse() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(false) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, blockedAttributes, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithNotBlockedAdomain() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain2.com")); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, blockedAttributes, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } + + @Test + public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndEnforceBlocksTrue() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, blockedAttributes, false); + + // when and then + assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithAdomainAndNoBlockedAttributes() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, null, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedBannerAttrForImp() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .battr(Attribute.battrBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid + .impid("impId2") + .attr(singletonList(1)))); + final BlockedAttributes blockedAttributes = BlockedAttributes.builder() + .battr(singletonMap("impId1", asList(1, 2))) + .build(); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, blockedAttributes, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithBlockedAdomainAndInDealsExceptions() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .allowedForDeals(singletonList("domain1.com")) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, blockedAttributes, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } + + @Test + public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndNotInDealsExceptions() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .allowedForDeals(singletonList("domain2.com")) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, blockedAttributes, false); + + // when and then + assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + } + + @Test + public void shouldReturnResultWithBidAndDebugMessageWhenBidIsBlocked() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid()); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, null, true); + + // when and then + assertThat(blocker.block()).satisfies(result -> { + assertThat(result.getValue()).isEqualTo(BlockedBids.of(singleton(0))); + assertThat(result.getDebugMessages()).containsOnly( + "Bid 0 from bidder bidder1 has been rejected, failed checks: [bcat]"); + }); + } + + @Test + public void shouldReturnResultWithBidWithoutDebugMessageWhenBidIsBlockedAndDebugDisabled() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid()); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, null, false); + + // when and then + assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + } + + @Test + public void shouldReturnResultWithAnalyticsResults() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .build()) + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .build()) + .bapp(Attribute.bappBuilder() + .enforceBlocks(true) + .build()) + .battr(Attribute.battrBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = asList( + bid(bid -> bid + .impid("impId1") + .adomain(asList("domain2.com", "domain3.com", "domain4.com")) + .bundle("app2")), + bid(bid -> bid + .impid("impId2") + .cat(asList("cat2", "cat3", "cat4")) + .attr(asList(2, 3, 4))), + bid(bid -> bid + .impid("impId1") + .adomain(singletonList("domain5.com")) + .cat(singletonList("cat5")) + .bundle("app5") + .attr(singletonList(5)))); + final BlockedAttributes blockedAttributes = BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com", "domain3.com")) + .bcat(asList("cat1", "cat2", "cat3")) + .bapp(asList("app1", "app2", "app3")) + .battr(singletonMap("impId2", asList(1, 2, 3))) + .build(); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, blockedAttributes, true); + + // when and then + assertThat(blocker.block()).satisfies(result -> { + assertThat(result.getValue()).isEqualTo(BlockedBids.of(new HashSet<>(asList(0, 1)))); + + final Map analyticsResultValues1 = new HashMap<>(); + analyticsResultValues1.put("attributes", asList("badv", "bapp")); + analyticsResultValues1.put("adomain", asList("domain2.com", "domain3.com")); + analyticsResultValues1.put("bundle", "app2"); + final Map analyticsResultValues2 = new HashMap<>(); + analyticsResultValues2.put("attributes", asList("bcat", "battr")); + analyticsResultValues2.put("bcat", asList("cat2", "cat3")); + analyticsResultValues2.put("attr", asList(2, 3)); + + assertThat(result.getAnalyticsResults()).containsOnly( + AnalyticsResult.of("success-blocked", analyticsResultValues1, "bidder1", "impId1"), + AnalyticsResult.of("success-blocked", analyticsResultValues2, "bidder1", "impId2"), + AnalyticsResult.of("success-allow", null, "bidder1", "impId1")); + }); + } + + @Test + public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(singletonList("domain2.com")) + .build()) + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(singletonList("cat2")) + .build()) + .bapp(Attribute.bappBuilder() + .enforceBlocks(true) + .allowedForDeals(singletonList("app2")) + .build()) + .battr(Attribute.battrBuilder() + .enforceBlocks(true) + .allowedForDeals(singletonList(2)) + .build()) + .build())); + + // when + final List bids = asList( + bid(bid -> bid.adomain(singletonList("domain1.com"))), + bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat1"))), + bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2"))), + bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2")).bundle("app1")), + bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2")).bundle("app2")), + bid(bid -> bid + .adomain(singletonList("domain2.com")) + .cat(singletonList("cat2")) + .bundle("app2") + .attr(singletonList(1))), + bid(bid -> bid + .adomain(singletonList("domain2.com")) + .cat(singletonList("cat2")) + .bundle("app2") + .attr(singletonList(2))), + bid()); + final BlockedAttributes blockedAttributes = BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .battr(singletonMap("impId1", asList(1, 2))) + .build(); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", accountConfig, blockedAttributes, true); + + // when and then + assertThat(blocker.block()).satisfies(result -> { + assertThat(result.getValue()).isEqualTo(BlockedBids.of(new HashSet<>(asList(0, 1, 3, 5, 7)))); + assertThat(result.getDebugMessages()).containsOnly( + "Bid 0 from bidder bidder1 has been rejected, failed checks: [badv, bcat]", + "Bid 1 from bidder bidder1 has been rejected, failed checks: [bcat]", + "Bid 3 from bidder bidder1 has been rejected, failed checks: [bapp]", + "Bid 5 from bidder bidder1 has been rejected, failed checks: [battr]", + "Bid 7 from bidder bidder1 has been rejected, failed checks: [badv, bcat]"); + }); + } + + private static BidderBid bid() { + return bid(identity()); + } + + private static BidderBid bid(UnaryOperator bidCustomizer) { + return BidderBid.of( + bidCustomizer.apply(Bid.builder() + .impid("impId1") + .dealid("dealid1")) + .build(), + BidType.banner, + "USD"); + } + + private static BlockedAttributes attributesWithBadv(List badv) { + return BlockedAttributes.builder().badv(badv).build(); + } + + private static ObjectNode toObjectNode(ModuleConfig config) { + return mapper.valueToTree(config); + } + + private static void isEmpty(ExecutionResult result) { + assertThat(result.hasValue()).isFalse(); + assertThat(result.getErrors()).isNull(); + assertThat(result.getWarnings()).isNull(); + assertThat(result.getDebugMessages()).isNull(); + } + + private static void hasValue(ExecutionResult result, Integer... indexes) { + assertThat(result.getValue()).isEqualTo(BlockedBids.of(new HashSet<>(asList(indexes)))); + assertThat(result.getErrors()).isNull(); + assertThat(result.getWarnings()).isNull(); + assertThat(result.getDebugMessages()).isNull(); + } +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BlockedAttributesResolverTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BlockedAttributesResolverTest.java new file mode 100644 index 00000000000..f51df9f45da --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BlockedAttributesResolverTest.java @@ -0,0 +1,131 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ArrayOverride; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Conditions; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ModuleConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; + +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +class BlockedAttributesResolverTest { + + private static final ObjectMapper mapper = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + @Test + public void shouldReturnEmptyResultWhenInvalidAccountConfigurationAndDebugDisabled() { + // given + final ObjectNode accountConfig = mapper.createObjectNode().put("block-lists", 1); + final BlockedAttributesResolver resolver = BlockedAttributesResolver.create( + emptyRequest(), "bidder1", accountConfig, false); + + // when and then + assertThat(resolver.resolve()).isEqualTo(ExecutionResult.empty()); + } + + @Test + public void shouldReturnResultWithErrorWhenInvalidAccountConfiguration() { + // given + final ObjectNode accountConfig = mapper.createObjectNode().put("attributes", 1); + final BlockedAttributesResolver resolver = BlockedAttributesResolver.create( + emptyRequest(), "bidder1", accountConfig, true); + + // when and then + assertThat(resolver.resolve()).isEqualTo( + ExecutionResult.withError("attributes field in account configuration is not an object")); + } + + @Test + public void shouldReturnResultWithValueAndWarnings() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + asList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList("domain3.com")), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("banner")), + singletonList("domain4.com"))))) + .build()) + .build())); + final BlockedAttributesResolver resolver = BlockedAttributesResolver.create( + request(imp -> imp + .video(Video.builder().build()) + .banner(Banner.builder().build())), + "bidder1", + accountConfig, + true); + + // when and then + assertThat(resolver.resolve()).isEqualTo(ExecutionResult.builder() + .value(BlockedAttributes.builder().badv(singletonList("domain3.com")).build()) + .warnings(singletonList("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner, video]")) + .build()); + } + + @Test + public void shouldReturnResultWithValueAndWarningsWhenDebugDisabled() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + asList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList("domain3.com")), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("banner")), + singletonList("domain4.com"))))) + .build()) + .build())); + final BlockedAttributesResolver resolver = BlockedAttributesResolver.create( + request(imp -> imp + .video(Video.builder().build()) + .banner(Banner.builder().build())), + "bidder1", + accountConfig, + false); + + // when and then + assertThat(resolver.resolve()).isEqualTo(ExecutionResult.builder() + .value(BlockedAttributes.builder().badv(singletonList("domain3.com")).build()) + .build()); + } + + private static BidRequest emptyRequest() { + return BidRequest.builder() + .imp(singletonList(Imp.builder().build())) + .build(); + } + + private static BidRequest request(UnaryOperator impCustomizer) { + return BidRequest.builder() + .imp(singletonList(impCustomizer.apply(Imp.builder()).build())) + .build(); + } + + private static ObjectNode toObjectNode(ModuleConfig config) { + return mapper.valueToTree(config); + } +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java new file mode 100644 index 00000000000..99da36297b6 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java @@ -0,0 +1,274 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core; + +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; + +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +class RequestUpdaterTest { + + @Test + public void shouldReplaceBadv() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder().badv(asList("domain1.com", "domain2.com")).build()); + final BidRequest request = BidRequest.builder() + .badv(singletonList("overriddendomain1.com")) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .build()); + } + + @Test + public void shouldKeepBadvWhenNoBlockedBadv() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder().build()); + final BidRequest request = BidRequest.builder() + .badv(singletonList("domain1.com")) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(singletonList("domain1.com")) + .build()); + } + + @Test + public void shouldReplaceBcat() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder().bcat(asList("cat1", "cat2")).build()); + final BidRequest request = BidRequest.builder() + .bcat(singletonList("overriddencat1")) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .bcat(asList("cat1", "cat2")) + .build()); + } + + @Test + public void shouldKeepBcatWhenNoBlockedBcat() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder().build()); + final BidRequest request = BidRequest.builder() + .bcat(singletonList("cat1")) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .bcat(singletonList("cat1")) + .build()); + } + + @Test + public void shouldReplaceBapp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder().bapp(asList("app1", "app2")).build()); + final BidRequest request = BidRequest.builder() + .bapp(singletonList("overriddenapp1")) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .bapp(asList("app1", "app2")) + .build()); + } + + @Test + public void shouldKeepBappWhenNoBlockedBapp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder().build()); + final BidRequest request = BidRequest.builder() + .bapp(singletonList("app1")) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .bapp(singletonList("app1")) + .build()); + } + + @Test + public void shouldReplaceImpBtype() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder().btype(singletonMap("impId1", asList(1, 2))).build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .btype(singletonList(123)) + .build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .btype(asList(1, 2)) + .build()) + .build())) + .build()); + } + + @Test + public void shouldReplaceImpBattr() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder().battr(singletonMap("impId1", asList(1, 2))).build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .battr(singletonList(123)) + .build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .battr(asList(1, 2)) + .build()) + .build())) + .build()); + } + + @Test + public void shouldNotChangeImpsWhenNoBlockedBannerTypeAndBlockedBannerAttr() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder().build()); + final List imps = singletonList(Imp.builder().build()); + final BidRequest request = BidRequest.builder() + .imp(imps) + .build(); + + // when and then + assertThat(updater.update(request).getImp()).isSameAs(imps); + } + + @Test + public void shouldNotChangeImpWhenNoBlockedBannerTypeAndBlockedBannerAttrForImp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .btype(singletonMap("impId2", singletonList(1))) + .battr(singletonMap("impId2", singletonList(1))) + .build()); + final Imp imp = Imp.builder().build(); + final BidRequest request = BidRequest.builder() + .imp(singletonList(imp)) + .build(); + + // when and then + final BidRequest updatedRequest = updater.update(request); + assertThat(updatedRequest.getImp()).hasSize(1); + assertThat(updatedRequest.getImp().get(0)).isSameAs(imp); + } + + @Test + public void shouldKeepImpBtypeWhenNoBlockedBannerTypeAndPresentBlockedBannerAttrForImp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap("impId1", singletonList(1))) + .build()); + final Imp imp = Imp.builder() + .id("impId1") + .banner(Banner.builder().btype(singletonList(1000)).build()) + .build(); + final BidRequest request = BidRequest.builder() + .imp(singletonList(imp)) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .btype(singletonList(1000)) + .battr(singletonList(1)) + .build()) + .build())) + .build()); + } + + @Test + public void shouldKeepImpBattrWhenNoBlockedBannerAttrAndPresentBlockedBannerTypeForImp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .btype(singletonMap("impId1", singletonList(1))) + .build()); + final Imp imp = Imp.builder() + .id("impId1") + .banner(Banner.builder().battr(singletonList(1000)).build()) + .build(); + final BidRequest request = BidRequest.builder() + .imp(singletonList(imp)) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .btype(singletonList(1)) + .battr(singletonList(1000)) + .build()) + .build())) + .build()); + } + + @Test + public void shouldUpdateAllAttributes() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .btype(singletonMap("impId1", asList(1, 2))) + .battr(singletonMap("impId1", asList(1, 2))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder().id("impId1").build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .btype(asList(1, 2)) + .battr(asList(1, 2)) + .build()) + .build())) + .build()); + } +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/ResponseUpdaterTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/ResponseUpdaterTest.java new file mode 100644 index 00000000000..08320e953b8 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/ResponseUpdaterTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core; + +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.Test; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.HashSet; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +class ResponseUpdaterTest { + + @Test + public void shouldRemoveSpecifiedBids() { + // given + final ResponseUpdater updater = ResponseUpdater.create(BlockedBids.of(new HashSet<>(asList(1, 2, 4)))); + final List bids = asList( + bid("bid1", BidType.banner, "USD"), + bid("bid2", BidType.video, "USD"), + bid("bid3", BidType.audio, "EUR"), + bid("bid4", BidType.xNative, "JPY"), + bid("bid5", BidType.video, "UAH")); + + // when and then + assertThat(updater.update(bids)).isEqualTo(asList( + bid("bid1", BidType.banner, "USD"), + bid("bid4", BidType.xNative, "JPY"))); + } + + private static BidderBid bid(String id, BidType type, String currency) { + return BidderBid.of(Bid.builder().id(id).build(), type, currency); + } +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/AllowedForDealsOverride.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/AllowedForDealsOverride.java new file mode 100644 index 00000000000..94fe11486e6 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/AllowedForDealsOverride.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.config; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class AllowedForDealsOverride { + + DealsConditions conditions; + + List override; +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/ArrayOverride.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/ArrayOverride.java new file mode 100644 index 00000000000..0b804c780e4 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/ArrayOverride.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.config; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ArrayOverride { + + Conditions conditions; + + List override; +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java new file mode 100644 index 00000000000..6e7d3257a33 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java @@ -0,0 +1,70 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.config; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Builder; +import lombok.Value; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Builder +@Value +public class Attribute { + + @JsonIgnore + String field; + + Boolean enforceBlocks; + + Boolean blockUnknown; + + List blocked; + + List allowedForDeals; + + @JsonIgnore + AttributeActionOverrides actionOverrides; + + @JsonAnyGetter + public Map getProperties() { + final Map properties = new HashMap<>(); + properties.computeIfAbsent("block-unknown-" + field, key -> blockUnknown); + properties.computeIfAbsent("blocked-" + field, key -> blocked); + properties.computeIfAbsent("allowed-" + field + "-for-deals", key -> allowedForDeals); + + properties.computeIfAbsent("action-overrides", key -> actionOverrides != null + ? actionOverrides.toBuilder() + .field(field) + .build() + : null); + + return properties; + } + + public static AttributeBuilder badvBuilder() { + return Attribute.builder() + .field("adomain"); + } + + public static AttributeBuilder bcatBuilder() { + return Attribute.builder() + .field("adv-cat"); + } + + public static AttributeBuilder bappBuilder() { + return Attribute.builder() + .field("app"); + } + + public static AttributeBuilder btypeBuilder() { + return Attribute.builder() + .field("banner-type"); + } + + public static AttributeBuilder battrBuilder() { + return Attribute.builder() + .field("banner-attr"); + } +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/AttributeActionOverrides.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/AttributeActionOverrides.java new file mode 100644 index 00000000000..5080c35330f --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/AttributeActionOverrides.java @@ -0,0 +1,77 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.config; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Builder; +import lombok.Value; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Builder(toBuilder = true) +@Value +public class AttributeActionOverrides { + + @JsonIgnore + String field; + + List> blocked; + + List enforceBlocks; + + List blockUnknown; + + List> allowedForDeals; + + @JsonAnyGetter + public Map getProperties() { + final Map properties = new HashMap<>(); + properties.computeIfAbsent("block-unknown-" + field, key -> blockUnknown); + properties.computeIfAbsent("blocked-" + field, key -> blocked); + properties.computeIfAbsent("allowed-" + field + "-for-deals", key -> allowedForDeals); + + return properties; + } + + public static AttributeActionOverrides blocked(List> blocked) { + return AttributeActionOverrides.builder() + .blocked(blocked) + .build(); + } + + public static AttributeActionOverrides blockUnknown(List blockUnknown) { + return AttributeActionOverrides.builder() + .blockUnknown(blockUnknown) + .build(); + } + + public static AttributeActionOverrides blockFlags( + List enforceBlocks, + List blockUnknown) { + + return AttributeActionOverrides.builder() + .enforceBlocks(enforceBlocks) + .blockUnknown(blockUnknown) + .build(); + } + + public static AttributeActionOverrides response( + List enforceBlocks, + List blockUnknown, + List> allowedForDeals) { + + return AttributeActionOverrides.builder() + .enforceBlocks(enforceBlocks) + .blockUnknown(blockUnknown) + .allowedForDeals(allowedForDeals) + .build(); + } + + public static AttributeActionOverrides allowedForDeals(List> allowedForDeals) { + + return AttributeActionOverrides.builder() + .allowedForDeals(allowedForDeals) + .build(); + } +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attributes.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attributes.java new file mode 100644 index 00000000000..bed73bc2554 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attributes.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.config; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class Attributes { + + Attribute badv; + + Attribute bcat; + + Attribute bapp; + + Attribute btype; + + Attribute battr; +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/BooleanOverride.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/BooleanOverride.java new file mode 100644 index 00000000000..c19dc7227fb --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/BooleanOverride.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.config; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class BooleanOverride { + + Conditions conditions; + + Boolean override; +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Conditions.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Conditions.java new file mode 100644 index 00000000000..974627a2ac0 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Conditions.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.config; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class Conditions { + + List bidders; + + List mediaType; +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/DealsConditions.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/DealsConditions.java new file mode 100644 index 00000000000..9318f39a721 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/DealsConditions.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.config; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class DealsConditions { + + List dealIds; +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/ModuleConfig.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/ModuleConfig.java new file mode 100644 index 00000000000..de57a4f12ea --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/ModuleConfig.java @@ -0,0 +1,9 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.core.config; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ModuleConfig { + + Attributes attributes; +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/model/ModuleContextTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/model/ModuleContextTest.java new file mode 100644 index 00000000000..1a96bbd1b16 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/model/ModuleContextTest.java @@ -0,0 +1,66 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.model; + +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; + +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +class ModuleContextTest { + + @Test + public void shouldCreateContextWithMapping() { + // given + final BlockedAttributes blockedAttributes = attributes(singletonList("domain1.com")); + + // when + final ModuleContext context = ModuleContext.create("bidder1", blockedAttributes); + + // then + assertThat(context.blockedAttributesFor("bidder1")).isSameAs(blockedAttributes); + } + + @Test + public void shouldCreateContextWithAdditionalMapping() { + // given + final BlockedAttributes bidder1BlockedAttributes = attributes(singletonList("domain1.com")); + final ModuleContext initialContext = ModuleContext.create("bidder1", bidder1BlockedAttributes); + + final BlockedAttributes bidder2BlockedAttributes = attributes(singletonList("domain2.com")); + + // when + final ModuleContext context = initialContext.with("bidder2", bidder2BlockedAttributes); + + // then + assertThat(context.blockedAttributesFor("bidder1")).isSameAs(bidder1BlockedAttributes); + assertThat(context.blockedAttributesFor("bidder2")).isSameAs(bidder2BlockedAttributes); + + // initial context shouldn't change + assertThat(initialContext.blockedAttributesFor("bidder1")).isSameAs(bidder1BlockedAttributes); + assertThat(initialContext.blockedAttributesFor("bidder2")).isNull(); + } + + @Test + public void shouldCreateContextWithReplacedMapping() { + // given + final BlockedAttributes initialBlockedAttributes = attributes(singletonList("domain1.com")); + final ModuleContext initialContext = ModuleContext.create("bidder1", initialBlockedAttributes); + + final BlockedAttributes replacementBlockedAttributes = attributes(singletonList("domain2com")); + + // when + final ModuleContext context = initialContext.with("bidder1", replacementBlockedAttributes); + + // then + assertThat(context.blockedAttributesFor("bidder1")).isSameAs(replacementBlockedAttributes); + + // initial context shouldn't change + assertThat(initialContext.blockedAttributesFor("bidder1")).isSameAs(initialBlockedAttributes); + } + + private static BlockedAttributes attributes(List badv) { + return BlockedAttributes.builder().badv(badv).build(); + } +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java new file mode 100644 index 00000000000..c0850518604 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java @@ -0,0 +1,228 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import io.vertx.core.Future; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ArrayOverride; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Conditions; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ModuleConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; +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.PayloadUpdate; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class Ortb2BlockingBidderRequestHookTest { + + private static final ObjectMapper mapper = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + private final Ortb2BlockingBidderRequestHook hook = new Ortb2BlockingBidderRequestHook(); + + @Test + public void shouldReturnResultWithNoActionWhenNoBlockingAttributes() { + // when + final Future> result = hook.call( + BidderRequestPayloadImpl.of(emptyRequest()), + BidderInvocationContextImpl.of("bidder1", null, true)); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isEqualTo(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + @Test + public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .put("attributes", 1); + + // when + final Future> result = hook.call( + BidderRequestPayloadImpl.of(emptyRequest()), + BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isEqualTo(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .errors(singletonList("attributes field in account configuration is not an object")) + .build()); + } + + @Test + public void shouldReturnResultWithNoActionAndNoErrorWhenInvalidAccountConfigAndDebugDisabled() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .put("attributes", 1); + + // when + final Future> result = hook.call( + BidderRequestPayloadImpl.of(emptyRequest()), + BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isEqualTo(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + @Test + public void shouldReturnResultWithModuleContextAndPayloadUpdate() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(singletonList("domain1.com")) + .build()) + .bcat(Attribute.bcatBuilder() + .blocked(singletonList("cat1")) + .build()) + .build())); + + // when + final Future> result = hook.call( + BidderRequestPayloadImpl.of(emptyRequest()), + BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + + // then + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertSoftly(softly -> { + softly.assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + softly.assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + softly.assertThat(invocationResult.moduleContext()) + .isNotNull() + .isInstanceOf(ModuleContext.class) + .asInstanceOf(InstanceOfAssertFactories.type(ModuleContext.class)) + .satisfies(context -> assertThat(context.blockedAttributesFor("bidder1")) + .isEqualTo(BlockedAttributes.builder() + .badv(singletonList("domain1.com")) + .bcat(singletonList("cat1")) + .build())); + softly.assertThat(invocationResult.warnings()).isNull(); + softly.assertThat(invocationResult.errors()).isNull(); + }); + + final PayloadUpdate payloadUpdate = invocationResult.payloadUpdate(); + final BidderRequestPayloadImpl payloadToUpdate = BidderRequestPayloadImpl.of(BidRequest.builder() + .badv(singletonList("overriddendomain1.com")) + .bcat(singletonList("overriddencat1")) + .build()); + assertThat(payloadUpdate.apply(payloadToUpdate)).isEqualTo(BidderRequestPayloadImpl.of( + BidRequest.builder() + .badv(singletonList("domain1.com")) + .bcat(singletonList("cat1")) + .build())); + } + + @Test + public void shouldReturnResultWithUpdateActionAndWarning() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + asList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain1.com")), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain2.com"))))) + .build()) + .build())); + + // when + final Future> result = hook.call( + BidderRequestPayloadImpl.of(emptyRequest()), + BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + + // then + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertSoftly(softly -> { + softly.assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + softly.assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + softly.assertThat(invocationResult.moduleContext()) + .asInstanceOf(InstanceOfAssertFactories.type(ModuleContext.class)) + .satisfies(context -> assertThat(context.blockedAttributesFor("bidder1")) + .isEqualTo(BlockedAttributes.builder().badv(singletonList("domain1.com")).build())); + softly.assertThat(invocationResult.warnings()).isEqualTo(singletonList( + "More than one conditions matches request. Bidder: bidder1, request media types: [video]")); + softly.assertThat(invocationResult.errors()).isNull(); + }); + } + + @Test + public void shouldReturnResultWithUpdateActionAndNoWarningWhenDebugDisabled() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .actionOverrides(AttributeActionOverrides.blocked( + asList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain1.com")), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain2.com"))))) + .build()) + .build())); + + // when + final Future> result = hook.call( + BidderRequestPayloadImpl.of(emptyRequest()), + BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + + // then + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + + assertSoftly(softly -> { + softly.assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + softly.assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + softly.assertThat(invocationResult.moduleContext()) + .asInstanceOf(InstanceOfAssertFactories.type(ModuleContext.class)) + .satisfies(context -> assertThat(context.blockedAttributesFor("bidder1")) + .isEqualTo(BlockedAttributes.builder().badv(singletonList("domain1.com")).build())); + softly.assertThat(invocationResult.warnings()).isNull(); + softly.assertThat(invocationResult.errors()).isNull(); + }); + } + + private static BidRequest emptyRequest() { + return BidRequest.builder() + .imp(singletonList(Imp.builder().video(Video.builder().build()).build())) + .build(); + } + + private static ObjectNode toObjectNode(ModuleConfig config) { + return mapper.valueToTree(config); + } +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java new file mode 100644 index 00000000000..761129aeac0 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java @@ -0,0 +1,321 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import io.vertx.core.Future; +import org.junit.jupiter.api.Test; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attributes; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.BooleanOverride; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Conditions; +import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ModuleConfig; +import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl; +import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl; +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.PayloadUpdate; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +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.SoftAssertions.assertSoftly; + +class Ortb2BlockingRawBidderResponseHookTest { + + private static final ObjectMapper mapper = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + private final Ortb2BlockingRawBidderResponseHook hook = new Ortb2BlockingRawBidderResponseHook(); + + @Test + public void shouldReturnResultWithNoActionWhenNoBidsBlocked() { + // when + final Future> result = hook.call( + BidderResponsePayloadImpl.of(singletonList(bid())), + BidderInvocationContextImpl.of("bidder1", null, true)); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isEqualTo(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .analyticsTags(TagsImpl.of(singletonList(ActivityImpl.of( + "enforce-blocking", + "success", + singletonList(ResultImpl.of( + "success-allow", + null, + AppliedToImpl.builder() + .bidders(singletonList("bidder1")) + .impIds(singletonList("impId1")) + .build())))))) + .build()); + } + + @Test + public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .put("attributes", 1); + + // when + final Future> result = hook.call( + BidderResponsePayloadImpl.of(singletonList(bid())), + BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isEqualTo(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .errors(singletonList("attributes field in account configuration is not an object")) + .build()); + } + + @Test + public void shouldReturnResultWithNoActionAndNoErrorWhenInvalidAccountConfigAndDebugDisabled() { + // given + final ObjectNode accountConfig = mapper.createObjectNode() + .put("attributes", 1); + + // when + final Future> result = hook.call( + BidderResponsePayloadImpl.of(singletonList(bid())), + BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isEqualTo(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + @Test + public void shouldReturnResultWithPayloadUpdateAndAnalyticsTags() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .build()) + .build())); + + // when + final Future> result = hook.call( + BidderResponsePayloadImpl.of(asList( + bid(), + bid(bid -> bid.adomain(singletonList("domain1.com"))), + bid(bid -> bid.adomain(singletonList("domain2.com"))))), + BidderInvocationContextImpl.builder() + .bidder("bidder1") + .accountConfig(accountConfig) + .moduleContext(ModuleContext.create( + "bidder1", BlockedAttributes.builder().badv(singletonList("domain2.com")).build())) + .debugEnabled(true) + .build()); + + // then + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertSoftly(softly -> { + softly.assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + softly.assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + softly.assertThat(invocationResult.warnings()).isNull(); + softly.assertThat(invocationResult.errors()).isNull(); + }); + + final PayloadUpdate payloadUpdate = invocationResult.payloadUpdate(); + final BidderResponsePayloadImpl payloadToUpdate = BidderResponsePayloadImpl.of(asList( + bid(), + bid(bid -> bid.adomain(singletonList("domain1.com"))), + bid(bid -> bid.adomain(singletonList("domain2.com"))))); + assertThat(payloadUpdate.apply(payloadToUpdate)).isEqualTo(BidderResponsePayloadImpl.of( + singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))))); + + assertThat(invocationResult.analyticsTags()).isEqualTo(TagsImpl.of(singletonList(ActivityImpl.of( + "enforce-blocking", + "success", + asList( + ResultImpl.of( + "success-blocked", + mapper.createObjectNode() + .set("adomain", mapper.createArrayNode()) + .set("attributes", mapper.createArrayNode() + .add("badv")), + AppliedToImpl.builder() + .bidders(singletonList("bidder1")) + .impIds(singletonList("impId1")) + .build()), + ResultImpl.of( + "success-allow", + null, + AppliedToImpl.builder() + .bidders(singletonList("bidder1")) + .impIds(singletonList("impId1")) + .build()), + ResultImpl.of( + "success-blocked", + mapper.createObjectNode() + .set("adomain", mapper.createArrayNode() + .add("domain2.com")) + .set("attributes", mapper.createArrayNode() + .add("badv")), + AppliedToImpl.builder() + .bidders(singletonList("bidder1")) + .impIds(singletonList("impId1")) + .build())))))); + } + + @Test + public void shouldReturnResultWithUpdateActionAndWarning() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .actionOverrides(AttributeActionOverrides.blockUnknown( + asList( + BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + true), + BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)))) + .build()) + .build())); + + // when + final Future> result = hook.call( + BidderResponsePayloadImpl.of(singletonList(bid())), + BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + + // then + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertSoftly(softly -> { + softly.assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + softly.assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + softly.assertThat(invocationResult.warnings()).containsOnly( + "More than one conditions matches request. Bidder: bidder1, request media types: [banner]"); + softly.assertThat(invocationResult.errors()).isNull(); + }); + } + + @Test + public void shouldReturnResultWithUpdateActionAndNoWarningWhenDebugDisabled() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .actionOverrides(AttributeActionOverrides.blockUnknown( + asList( + BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + true), + BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)))) + .build()) + .build())); + + // when + final Future> result = hook.call( + BidderResponsePayloadImpl.of(singletonList(bid())), + BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + + // then + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertSoftly(softly -> { + softly.assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + softly.assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + softly.assertThat(invocationResult.warnings()).isNull(); + softly.assertThat(invocationResult.errors()).isNull(); + }); + } + + @Test + public void shouldReturnResultWithUpdateActionAndDebugMessage() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .build()) + .build())); + + // when + final Future> result = hook.call( + BidderResponsePayloadImpl.of(singletonList(bid())), + BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + + // then + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertSoftly(softly -> { + softly.assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + softly.assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + softly.assertThat(invocationResult.debugMessages()).containsOnly( + "Bid 0 from bidder bidder1 has been rejected, failed checks: [badv]"); + }); + } + + @Test + public void shouldReturnResultWithUpdateActionAndNoDebugMessageWhenDebugDisabled() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .build()) + .build())); + + // when + final Future> result = hook.call( + BidderResponsePayloadImpl.of(singletonList(bid())), + BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + + // then + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertSoftly(softly -> { + softly.assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + softly.assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + softly.assertThat(invocationResult.debugMessages()).isNull(); + }); + } + + private static BidderBid bid() { + return bid(identity()); + } + + private static BidderBid bid(UnaryOperator bidCustomizer) { + return BidderBid.of( + bidCustomizer.apply(Bid.builder().impid("impId1")).build(), + BidType.banner, + "USD"); + } + + private static ObjectNode toObjectNode(ModuleConfig config) { + return mapper.valueToTree(config); + } +} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java new file mode 100644 index 00000000000..ccc69f48d78 --- /dev/null +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java @@ -0,0 +1,35 @@ +package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.execution.Timeout; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.model.Endpoint; + +@Accessors(fluent = true) +@Builder +@Value +public class BidderInvocationContextImpl implements BidderInvocationContext { + + Timeout timeout; + + Endpoint endpoint; + + Object moduleContext; + + boolean debugEnabled; + + ObjectNode accountConfig; + + String bidder; + + public static BidderInvocationContext of(String bidder, ObjectNode accountConfig, boolean debugEnabled) { + return BidderInvocationContextImpl.builder() + .bidder(bidder) + .accountConfig(accountConfig) + .debugEnabled(debugEnabled) + .build(); + } +} diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml new file mode 100644 index 00000000000..ca802a420b8 --- /dev/null +++ b/extra/modules/pom.xml @@ -0,0 +1,100 @@ + + + + 4.0.0 + + org.prebid.server.hooks.modules + all-modules + 1.73.0-SNAPSHOT + pom + + + ortb2-blocking + + + + UTF-8 + UTF-8 + + 1.8 + 1.8 + + 1.73.0-SNAPSHOT + + 1.18.4 + + 5.7.2 + 3.20.2 + + 3.8.0 + 2.22.1 + + + + + + org.prebid + prebid-server + ${prebid-server.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.junit + junit-bom + ${junit-bom.version} + pom + import + + + org.assertj + assertj-core + ${assertj.version} + + + + + + + org.prebid + prebid-server + + + org.projectlombok + lombok + provided + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + + diff --git a/extra/pom.xml b/extra/pom.xml new file mode 100644 index 00000000000..d445d0123b2 --- /dev/null +++ b/extra/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + org.prebid + prebid-server-aggregator + 1.73.0-SNAPSHOT + pom + + + scm:git:git@github.com:prebid/prebid-server-java.git + HEAD + + + + .. + modules + bundle + + + diff --git a/pom.xml b/pom.xml index 9413528fd75..ce420f770c8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server - 1.46.0-SNAPSHOT + 1.73.0-SNAPSHOT prebid-server Prebid Server (Server-side Header Bidding) @@ -14,7 +14,7 @@ - scm:git:git@github.com:rubicon-project/prebid-server-java.git + scm:git:git@github.com:prebid/prebid-server-java.git HEAD @@ -28,13 +28,13 @@ 2.1.1.RELEASE 1.3.1 2.0.1.Final - 6.1.0.Final + 6.1.5.Final 3.8.3 1.18.4 3.6 4.1 - 1.19 - 4.5.5 + 1.21 + 4.5.13 5.3.1 2.10.0 0.1.7 @@ -55,7 +55,8 @@ 2.23.4 3.8.0 2.26.3 - 9.4.31.v20200723 + 4.0.1 + 9.4.43.v20210629 3.0.6 1.4.196 @@ -88,6 +89,10 @@ spring-boot-starter ${spring.boot.version} + + org.springframework.boot + spring-boot-starter-aop + javax.annotation javax.annotation-api @@ -235,6 +240,11 @@ metrics-core ${metrics.version} + + io.dropwizard.metrics + metrics-jvm + ${metrics.version} + io.dropwizard.metrics metrics-graphite @@ -299,6 +309,12 @@ ${assertj.version} test + + org.awaitility + awaitility + ${awaitility.version} + test + org.springframework.boot spring-boot-starter-test @@ -312,10 +328,16 @@ com.github.tomakehurst - wiremock + wiremock-jre8 ${wiremock.version} test + + com.iabtcf + iabtcf-encoder + ${iabtcf.version} + test + org.eclipse.jetty jetty-server @@ -547,6 +569,7 @@ spring-boot-maven-plugin ${spring.boot.version} + false true diff --git a/sample/prebid-config.yaml b/sample/prebid-config.yaml index ba288015f75..4f06889f290 100644 --- a/sample/prebid-config.yaml +++ b/sample/prebid-config.yaml @@ -14,12 +14,11 @@ cache: settings: filesystem: settings-filename: sample/sample-app-settings.yaml - stored-requests-dir: /var/tmp - stored-imps-dir: /var/tmp - stored-responses-dir: /var/tmp + stored-requests-dir: + stored-imps-dir: + stored-responses-dir: gdpr: + default-value: 1 vendorlist: - v1: - cache-dir: /var/tmp/vendor1 v2: cache-dir: /var/tmp/vendor2 diff --git a/src/main/java/com/iab/openrtb/request/Content.java b/src/main/java/com/iab/openrtb/request/Content.java index 041056e1db4..e8f63edcf75 100644 --- a/src/main/java/com/iab/openrtb/request/Content.java +++ b/src/main/java/com/iab/openrtb/request/Content.java @@ -1,5 +1,6 @@ package com.iab.openrtb.request; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Builder; import lombok.Value; @@ -19,6 +20,8 @@ @Value public class Content { + private static final Content EMPTY = Content.builder().build(); + /** ID uniquely identifying the content. */ String id; @@ -114,4 +117,9 @@ public class Content { /** Placeholder for exchange-specific extensions to OpenRTB. */ ObjectNode ext; + + @JsonIgnore + public boolean isEmpty() { + return this.equals(EMPTY); + } } diff --git a/src/main/java/com/iab/openrtb/response/Bid.java b/src/main/java/com/iab/openrtb/response/Bid.java index 3e7922afa72..54ea0bbe945 100644 --- a/src/main/java/com/iab/openrtb/response/Bid.java +++ b/src/main/java/com/iab/openrtb/response/Bid.java @@ -1,9 +1,8 @@ package com.iab.openrtb.response; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; +import lombok.Builder; +import lombok.Value; import java.math.BigDecimal; import java.util.List; @@ -13,22 +12,19 @@ * relates to a specific impression in the bid request via the {@code impid} * attribute and constitutes an offer to buy that impression for a given * {@code price}. - *

- * IMPORTANT: unlike other data classes this one is mutable (annotated with {@link Data} instead of - * {@link lombok.Value}). Motivation: during the course of processing bids could be altered several times (price - * adjustment, post-processing). Creating new instance of the bid in each of these cases seems to cause unnecessary - * memory pressure. In order to avoid unnecessary allocations this class is made mutable (as an exception) i.e. this - * decision could be seen as a performance optimisation. */ -@SuperBuilder(toBuilder = true) -@NoArgsConstructor -@Data +@Builder(toBuilder = true) +@Value public class Bid { - /** Bidder generated bid ID to assist with logging/tracking. (required) */ + /** + * Bidder generated bid ID to assist with logging/tracking. (required) + */ String id; - /** ID of the Imp object in the related bid request. (required) */ + /** + * ID of the Imp object in the related bid request. (required) + */ String impid; /** @@ -72,7 +68,9 @@ public class Bid { */ String adm; - /** ID of a preloaded ad to be served if the bid wins. */ + /** + * ID of a preloaded ad to be served if the bid wins. + */ String adid; /** @@ -110,19 +108,29 @@ public class Bid { */ String crid; - /** IAB content categories of the creative. Refer to List 5.1. */ + /** + * IAB content categories of the creative. Refer to List 5.1. + */ List cat; - /** Set of attributes describing the creative. Refer to List 5.3. */ + /** + * Set of attributes describing the creative. Refer to List 5.3. + */ List attr; - /** API required by the markup if applicable. Refer to List 5.6. */ + /** + * API required by the markup if applicable. Refer to List 5.6. + */ Integer api; - /** Video response protocol of the markup if applicable. Refer to List 5.8. */ + /** + * Video response protocol of the markup if applicable. Refer to List 5.8. + */ Integer protocol; - /** Creative media rating per IQG guidelines. Refer to List 5.19. */ + /** + * Creative media rating per IQG guidelines. Refer to List 5.19. + */ Integer qagmediarating; /** @@ -138,10 +146,14 @@ public class Bid { */ String dealid; - /** Width of the creative in device independent pixels (DIPS). */ + /** + * Width of the creative in device independent pixels (DIPS). + */ Integer w; - /** Height of the creative in device independent pixels (DIPS). */ + /** + * Height of the creative in device independent pixels (DIPS). + */ Integer h; /** @@ -162,6 +174,8 @@ public class Bid { */ Integer exp; - /** Placeholder for bidder-specific extensions to OpenRTB. */ + /** + * Placeholder for bidder-specific extensions to OpenRTB. + */ ObjectNode ext; } diff --git a/src/main/java/com/iab/openrtb/response/BidResponse.java b/src/main/java/com/iab/openrtb/response/BidResponse.java index 1473e269b16..55384e99cf6 100644 --- a/src/main/java/com/iab/openrtb/response/BidResponse.java +++ b/src/main/java/com/iab/openrtb/response/BidResponse.java @@ -1,12 +1,10 @@ package com.iab.openrtb.response; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Builder; import lombok.Value; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; -import java.util.Comparator; import java.util.List; -import java.util.Objects; /** * This object is the top-level bid response object (i.e., the unnamed outer @@ -21,7 +19,7 @@ * for not bidding, just a {@link BidResponse} object is returned with a reason * code in the {@code nbr} attribute. */ -@Builder +@Builder(toBuilder = true) @Value public class BidResponse { @@ -61,19 +59,5 @@ public class BidResponse { /** * Placeholder for bidder-specific extensions to OpenRTB. */ - ObjectNode ext; - - public static final Comparator COMPARATOR = (left, right) -> { - if (Objects.isNull(left) - || left.getSeatbid().isEmpty() - || left.getSeatbid().get(0).getBid().isEmpty() - || Objects.isNull(right)) { - return -1; - } - if (right.getSeatbid().isEmpty() || right.getSeatbid().get(0).getBid().isEmpty()) { - return 1; - } - return left.getSeatbid().get(0).getBid().get(0).getPrice() - .compareTo(right.getSeatbid().get(0).getBid().get(0).getPrice()); - }; + ExtBidResponse ext; } diff --git a/src/main/java/com/iab/openrtb/response/Response.java b/src/main/java/com/iab/openrtb/response/Response.java index 7f90aaf4219..d1a3df2bcd1 100644 --- a/src/main/java/com/iab/openrtb/response/Response.java +++ b/src/main/java/com/iab/openrtb/response/Response.java @@ -13,7 +13,7 @@ * field “native” that would contain the object above as its value. * The Native Object specified above is now the root object. */ -@Builder +@Builder(toBuilder = true) @Value public class Response { diff --git a/src/main/java/org/prebid/server/analytics/AnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/AnalyticsReporter.java index 58888228a99..6b1768f3a24 100644 --- a/src/main/java/org/prebid/server/analytics/AnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/AnalyticsReporter.java @@ -1,9 +1,10 @@ package org.prebid.server.analytics; +import io.vertx.core.Future; + /** * Type of component that does transactional logging. */ -@FunctionalInterface public interface AnalyticsReporter { /** @@ -12,5 +13,15 @@ public interface AnalyticsReporter { *

* Implementation note: this method is executed on Vert.x event loop thread so it must never use blocking API. */ - void processEvent(T event); + Future processEvent(T event); + + /** + * Method for defining analytics reporter ID for TCF checks. + */ + int vendorId(); + + /** + * Method for defining name of the related to this analytic adapter. + */ + String name(); } diff --git a/src/main/java/org/prebid/server/analytics/AnalyticsReporterDelegator.java b/src/main/java/org/prebid/server/analytics/AnalyticsReporterDelegator.java new file mode 100644 index 00000000000..5ab765aceb4 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/AnalyticsReporterDelegator.java @@ -0,0 +1,235 @@ +package org.prebid.server.analytics; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Site; +import io.netty.channel.ConnectTimeoutException; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.analytics.model.AmpEvent; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.model.CookieSyncEvent; +import org.prebid.server.analytics.model.NotificationEvent; +import org.prebid.server.analytics.model.SetuidEvent; +import org.prebid.server.analytics.model.VideoEvent; +import org.prebid.server.auction.PrivacyEnforcementService; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.util.StreamUtil; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +/** + * Class dispatches event processing to all enabled reporters. + */ +public class AnalyticsReporterDelegator { + + private static final Logger logger = LoggerFactory.getLogger(AnalyticsReporterDelegator.class); + private static final ConditionalLogger UNKNOWN_ADAPTERS_LOGGER = new ConditionalLogger(logger); + private static final Set ADAPTERS_PERMITTED_FOR_FULL_DATA = Collections.singleton("logAnalytics"); + + private final List delegates; + private final Vertx vertx; + private final PrivacyEnforcementService privacyEnforcementService; + private final Metrics metrics; + + private final Set reporterVendorIds; + private final Set reporterNames; + + public AnalyticsReporterDelegator(List delegates, + Vertx vertx, + PrivacyEnforcementService privacyEnforcementService, + Metrics metrics) { + this.delegates = Objects.requireNonNull(delegates); + this.vertx = Objects.requireNonNull(vertx); + this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); + this.metrics = Objects.requireNonNull(metrics); + + reporterVendorIds = delegates.stream().map(AnalyticsReporter::vendorId).collect(Collectors.toSet()); + reporterNames = delegates.stream().map(AnalyticsReporter::name).collect(Collectors.toSet()); + } + + public void processEvent(T event) { + for (AnalyticsReporter analyticsReporter : delegates) { + vertx.runOnContext(ignored -> processEventByReporter(analyticsReporter, event)); + } + } + + public void processEvent(T event, TcfContext tcfContext) { + privacyEnforcementService.resultForVendorIds(reporterVendorIds, tcfContext) + .setHandler(privacyEnforcementMap -> delegateEvent(event, tcfContext, privacyEnforcementMap)); + } + + private void delegateEvent(T event, + TcfContext tcfContext, + AsyncResult> privacyEnforcementMapResult) { + if (privacyEnforcementMapResult.succeeded()) { + final Map privacyEnforcementActionMap = + privacyEnforcementMapResult.result(); + checkUnknownAdaptersForAuctionEvent(event); + for (AnalyticsReporter analyticsReporter : delegates) { + final T updatedEvent = updateEvent(event, analyticsReporter.name()); + final int reporterVendorId = analyticsReporter.vendorId(); + // resultForVendorIds is guaranteed returning for each provided value except null, + // but to be sure lets use getOrDefault + final PrivacyEnforcementAction reporterPrivacyAction = privacyEnforcementActionMap + .getOrDefault(reporterVendorId, PrivacyEnforcementAction.restrictAll()); + if (!reporterPrivacyAction.isBlockAnalyticsReport()) { + vertx.runOnContext(ignored -> processEventByReporter(analyticsReporter, updatedEvent)); + } + } + } else { + final Throwable privacyEnforcementException = privacyEnforcementMapResult.cause(); + logger.error("Analytics TCF enforcement check failed for consentString: {0} and " + + "delegates with vendorIds {1}", privacyEnforcementException, + tcfContext.getConsentString(), delegates); + } + } + + private void checkUnknownAdaptersForAuctionEvent(T event) { + if (event instanceof AuctionEvent) { + logUnknownAdapters((AuctionEvent) event); + } + } + + private void logUnknownAdapters(AuctionEvent auctionEvent) { + final AuctionContext auctionContext = auctionEvent.getAuctionContext(); + final BidRequest bidRequest = auctionContext != null ? auctionContext.getBidRequest() : null; + final ExtRequest extRequest = bidRequest != null ? bidRequest.getExt() : null; + final ExtRequestPrebid extPrebid = extRequest != null ? extRequest.getPrebid() : null; + final JsonNode analytics = extPrebid != null ? extPrebid.getAnalytics() : null; + final Iterator analyticsFieldNames = isNotEmptyObjectNode(analytics) ? analytics.fieldNames() : null; + + if (analyticsFieldNames != null) { + final List unknownAdapterNames = StreamUtil.asStream(analyticsFieldNames) + .filter(adapter -> !reporterNames.contains(adapter)) + .collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(unknownAdapterNames)) { + final Site site = bidRequest.getSite(); + final String refererUrl = site != null ? site.getPage() : null; + UNKNOWN_ADAPTERS_LOGGER.warn( + String.format("Unknown adapters in ext.prebid.analytics[].adapter: %s, referrer: '%s'", + unknownAdapterNames, refererUrl), 0.01); + } + } + } + + private static boolean isNotEmptyObjectNode(JsonNode analytics) { + return analytics != null && analytics.isObject() && !analytics.isEmpty(); + } + + private static T updateEvent(T event, String adapter) { + if (!ADAPTERS_PERMITTED_FOR_FULL_DATA.contains(adapter) && event instanceof AuctionEvent) { + final AuctionEvent auctionEvent = (AuctionEvent) event; + final AuctionContext updatedAuctionContext = + updateAuctionContextAdapter(auctionEvent.getAuctionContext(), adapter); + return updatedAuctionContext != null + ? (T) auctionEvent.toBuilder().auctionContext(updatedAuctionContext).build() + : event; + } + + return event; + } + + private static AuctionContext updateAuctionContextAdapter(AuctionContext context, String adapter) { + final BidRequest bidRequest = context != null ? context.getBidRequest() : null; + final BidRequest updatedBidRequest = updateBidRequest(bidRequest, adapter); + + return updatedBidRequest != null ? context.toBuilder().bidRequest(updatedBidRequest).build() : null; + } + + private static BidRequest updateBidRequest(BidRequest bidRequest, String adapterName) { + final ExtRequest requestExt = bidRequest != null ? bidRequest.getExt() : null; + final ExtRequestPrebid extPrebid = requestExt != null ? requestExt.getPrebid() : null; + final JsonNode analytics = extPrebid != null ? extPrebid.getAnalytics() : null; + ObjectNode preparedAnalytics = null; + if (isNotEmptyObjectNode(analytics)) { + preparedAnalytics = prepareAnalytics((ObjectNode) analytics, adapterName); + } + final ExtRequest updatedExtRequest = preparedAnalytics != null + ? ExtRequest.of(extPrebid.toBuilder().analytics(preparedAnalytics).build()) + : null; + + if (updatedExtRequest != null) { + updatedExtRequest.addProperties(requestExt.getProperties()); + return bidRequest.toBuilder().ext(updatedExtRequest).build(); + } + + return null; + } + + private static ObjectNode prepareAnalytics(ObjectNode analytics, String adapterName) { + final ObjectNode analyticsNodeCopy = analytics.deepCopy(); + final JsonNode adapterNode = analyticsNodeCopy.get(adapterName); + analyticsNodeCopy.removeAll(); + if (adapterNode != null) { + analyticsNodeCopy.set(adapterName, adapterNode); + } + + return !analyticsNodeCopy.isEmpty() ? analyticsNodeCopy : null; + } + + private void processEventByReporter(AnalyticsReporter analyticsReporter, T event) { + final String reporterName = analyticsReporter.name(); + analyticsReporter.processEvent(event) + .map(ignored -> processSuccess(event, reporterName)) + .otherwise(exception -> processFail(exception, event, reporterName)); + } + + private Future processSuccess(T event, String reporterName) { + updateMetricsByEventType(event, reporterName, MetricName.ok); + return Future.succeededFuture(); + } + + private Future processFail(Throwable exception, T event, String reporterName) { + final MetricName failedResult; + if (exception instanceof TimeoutException || exception instanceof ConnectTimeoutException) { + failedResult = MetricName.timeout; + } else if (exception instanceof InvalidRequestException) { + failedResult = MetricName.badinput; + } else { + failedResult = MetricName.err; + } + updateMetricsByEventType(event, reporterName, failedResult); + return Future.failedFuture(exception); + } + + private void updateMetricsByEventType(T event, String analyticsCode, MetricName result) { + final MetricName eventType; + if (event instanceof AuctionEvent) { + eventType = MetricName.event_auction; + } else if (event instanceof AmpEvent) { + eventType = MetricName.event_amp; + } else if (event instanceof VideoEvent) { + eventType = MetricName.event_video; + } else if (event instanceof SetuidEvent) { + eventType = MetricName.event_setuid; + } else if (event instanceof CookieSyncEvent) { + eventType = MetricName.event_cookie_sync; + } else if (event instanceof NotificationEvent) { + eventType = MetricName.event_notification; + } else { + eventType = MetricName.event_unknown; + } + metrics.updateAnalyticEventMetric(analyticsCode, eventType, result); + } +} diff --git a/src/main/java/org/prebid/server/analytics/CompositeAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/CompositeAnalyticsReporter.java deleted file mode 100644 index 2a9578b3820..00000000000 --- a/src/main/java/org/prebid/server/analytics/CompositeAnalyticsReporter.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.prebid.server.analytics; - -import io.vertx.core.Vertx; - -import java.util.List; -import java.util.Objects; - -/** - * Implementation of the Composite design pattern that dispatches event processing to all enabled reporters. - */ -public class CompositeAnalyticsReporter implements AnalyticsReporter { - - private final List delegates; - private final Vertx vertx; - - public CompositeAnalyticsReporter(List delegates, Vertx vertx) { - this.delegates = Objects.requireNonNull(delegates); - this.vertx = Objects.requireNonNull(vertx); - } - - @Override - public void processEvent(T event) { - for (final AnalyticsReporter reporter : delegates) { - vertx.runOnContext(ignored -> reporter.processEvent(event)); - } - } -} diff --git a/src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java index 7fc108f1d42..f947e226178 100644 --- a/src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java @@ -1,6 +1,7 @@ package org.prebid.server.analytics; import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.vertx.core.Future; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import lombok.AllArgsConstructor; @@ -28,23 +29,39 @@ public LogAnalyticsReporter(JacksonMapper mapper) { } @Override - public void processEvent(T event) { - final String type; + public Future processEvent(T event) { + final LogEvent logEvent; + if (event instanceof AuctionEvent) { - type = "/openrtb2/auction"; + logEvent = new LogEvent<>("/openrtb2/auction", ((AuctionEvent) event).getBidResponse()); } else if (event instanceof AmpEvent) { - type = "/openrtb2/amp"; + logEvent = new LogEvent<>("/openrtb2/amp", ((AmpEvent) event).getBidResponse()); } else if (event instanceof VideoEvent) { - type = "/openrtb2/video"; + logEvent = new LogEvent<>("/openrtb2/video", ((VideoEvent) event).getBidResponse()); } else if (event instanceof SetuidEvent) { - type = "/setuid"; + final SetuidEvent setuidEvent = (SetuidEvent) event; + logEvent = new LogEvent<>( + "/setuid", + setuidEvent.getBidder() + ":" + setuidEvent.getUid() + ":" + setuidEvent.getSuccess()); } else if (event instanceof CookieSyncEvent) { - type = "/cookie_sync"; + logEvent = new LogEvent<>("/cookie_sync", ((CookieSyncEvent) event).getBidderStatus()); } else { - type = "unknown"; + logEvent = new LogEvent<>("unknown", null); } - logger.debug(mapper.encode(new LogEvent<>(type, event))); + logger.debug(mapper.encode(logEvent)); + + return Future.succeededFuture(); + } + + @Override + public int vendorId() { + return 0; + } + + @Override + public String name() { + return "logAnalytics"; } @AllArgsConstructor diff --git a/src/main/java/org/prebid/server/analytics/model/AuctionEvent.java b/src/main/java/org/prebid/server/analytics/model/AuctionEvent.java index 6c6eb656864..5773ab30ffe 100644 --- a/src/main/java/org/prebid/server/analytics/model/AuctionEvent.java +++ b/src/main/java/org/prebid/server/analytics/model/AuctionEvent.java @@ -10,7 +10,7 @@ /** * Represents a transaction at /openrtb2/auction endpoint. */ -@Builder +@Builder(toBuilder = true) @Value public class AuctionEvent { diff --git a/src/main/java/org/prebid/server/analytics/model/NotificationEvent.java b/src/main/java/org/prebid/server/analytics/model/NotificationEvent.java index 85f5c813a98..49bf7fb7d50 100644 --- a/src/main/java/org/prebid/server/analytics/model/NotificationEvent.java +++ b/src/main/java/org/prebid/server/analytics/model/NotificationEvent.java @@ -17,6 +17,8 @@ public class NotificationEvent { Account account; + String lineItemId; + String bidder; Long timestamp; diff --git a/src/main/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporter.java new file mode 100644 index 00000000000..0c3c81f1d08 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporter.java @@ -0,0 +1,188 @@ +package org.prebid.server.analytics.pubstack; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.model.AmpEvent; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.model.CookieSyncEvent; +import org.prebid.server.analytics.model.SetuidEvent; +import org.prebid.server.analytics.model.VideoEvent; +import org.prebid.server.analytics.pubstack.model.EventType; +import org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties; +import org.prebid.server.analytics.pubstack.model.PubstackConfig; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.Initializable; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class PubstackAnalyticsReporter implements AnalyticsReporter, Initializable { + + private static final Logger logger = LoggerFactory.getLogger(PubstackAnalyticsReporter.class); + + private static final String EVENT_REPORT_ENDPOINT_PATH = "/intake"; + private static final String CONFIG_URL_SUFFIX = "/bootstrap?scopeId="; + private static final Map CLASS_TO_EVENT_TYPE; + + static { + CLASS_TO_EVENT_TYPE = new HashMap<>(); + CLASS_TO_EVENT_TYPE.put(AuctionEvent.class.getName(), EventType.auction); + CLASS_TO_EVENT_TYPE.put(AmpEvent.class.getName(), EventType.amp); + CLASS_TO_EVENT_TYPE.put(VideoEvent.class.getName(), EventType.video); + CLASS_TO_EVENT_TYPE.put(SetuidEvent.class.getName(), EventType.setuid); + CLASS_TO_EVENT_TYPE.put(CookieSyncEvent.class.getName(), EventType.cookiesync); + } + + private final long configurationRefreshDelay; + private final long timeout; + private final HttpClient httpClient; + private final JacksonMapper jacksonMapper; + private final Vertx vertx; + + private final Map eventHandlers; + private PubstackConfig pubstackConfig; + + public PubstackAnalyticsReporter(PubstackAnalyticsProperties pubstackAnalyticsProperties, + HttpClient httpClient, + JacksonMapper jacksonMapper, + Vertx vertx) { + this.configurationRefreshDelay = + Objects.requireNonNull(pubstackAnalyticsProperties.getConfigurationRefreshDelayMs()); + this.timeout = Objects.requireNonNull(pubstackAnalyticsProperties.getTimeoutMs()); + this.httpClient = Objects.requireNonNull(httpClient); + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.vertx = Objects.requireNonNull(vertx); + + this.eventHandlers = createEventHandlers(pubstackAnalyticsProperties, httpClient, jacksonMapper, vertx); + this.pubstackConfig = PubstackConfig.of(pubstackAnalyticsProperties.getScopeId(), + pubstackAnalyticsProperties.getEndpoint(), Collections.emptyMap()); + } + + private static Map createEventHandlers( + PubstackAnalyticsProperties pubstackAnalyticsProperties, + HttpClient httpClient, + JacksonMapper jacksonMapper, + Vertx vertx) { + return Arrays.stream(EventType.values()) + .collect(Collectors.toMap(Function.identity(), + eventType -> new PubstackEventHandler( + pubstackAnalyticsProperties, + false, + buildEventEndpointUrl(pubstackAnalyticsProperties.getEndpoint(), eventType), + jacksonMapper, + httpClient, + vertx))); + } + + private static String buildEventEndpointUrl(String endpoint, EventType eventType) { + return HttpUtil.validateUrl(endpoint + EVENT_REPORT_ENDPOINT_PATH + eventType.name()); + } + + public Future processEvent(T event) { + final EventType eventType = CLASS_TO_EVENT_TYPE.get(event.getClass().getName()); + if (eventType != null) { + eventHandlers.get(eventType).handle(event); + } + return Future.succeededFuture(); + } + + @Override + public int vendorId() { + return 0; + } + + @Override + public String name() { + return "pubstack"; + } + + @Override + public void initialize() { + vertx.setPeriodic(configurationRefreshDelay, id -> fetchRemoteConfig()); + fetchRemoteConfig(); + } + + void shutdown() { + eventHandlers.values().forEach(PubstackEventHandler::reportEvents); + } + + private void fetchRemoteConfig() { + logger.info("[pubstack] Updating config: {0}", pubstackConfig); + httpClient.get(makeEventEndpointUrl(pubstackConfig.getEndpoint(), pubstackConfig.getScopeId()), timeout) + .map(this::processRemoteConfigurationResponse) + .setHandler(this::updateConfigsOnChange); + } + + private PubstackConfig processRemoteConfigurationResponse(HttpClientResponse response) { + final int statusCode = response.getStatusCode(); + if (statusCode != 200) { + throw new PreBidException(String.format("[pubstack] Failed to fetch config, reason: HTTP status code %d", + statusCode)); + } + final String body = response.getBody(); + try { + return jacksonMapper.decodeValue(body, PubstackConfig.class); + } catch (DecodeException e) { + throw new PreBidException(String.format("[pubstack] Failed to fetch config, reason: failed to parse" + + " response: %s", body), e); + } + } + + private void updateConfigsOnChange(AsyncResult asyncConfigResult) { + if (asyncConfigResult.failed()) { + logger.error("[pubstask] Fail to fetch remote configuration: {0}", asyncConfigResult.cause().getMessage()); + } else if (!Objects.equals(pubstackConfig, asyncConfigResult.result())) { + final PubstackConfig pubstackConfig = asyncConfigResult.result(); + eventHandlers.values().forEach(PubstackEventHandler::reportEvents); + this.pubstackConfig = pubstackConfig; + updateHandlers(pubstackConfig); + } + } + + private void updateHandlers(PubstackConfig pubstackConfig) { + final Map handlersEnabled = MapUtils.emptyIfNull(pubstackConfig.getFeatures()); + + eventHandlers.forEach((eventType, eventHandler) -> eventHandler.updateConfig( + BooleanUtils.toBooleanDefaultIfNull(handlersEnabled.get(eventType), false), + makeEventHandlerEndpoint(pubstackConfig.getEndpoint(), eventType), + pubstackConfig.getScopeId())); + } + + private static String makeEventEndpointUrl(String endpoint, String scopeId) { + try { + return HttpUtil.validateUrl(endpoint + CONFIG_URL_SUFFIX + scopeId); + } catch (IllegalArgumentException e) { + final String message = String.format("[pubstack] Failed to create remote config server url for endpoint:" + + " %s", endpoint); + logger.error(message); + throw new PreBidException(message); + } + } + + private String makeEventHandlerEndpoint(String endpoint, EventType eventType) { + try { + return HttpUtil.validateUrl(endpoint + EVENT_REPORT_ENDPOINT_PATH + "/" + eventType.name()); + } catch (IllegalArgumentException e) { + final String message = String.format("[pubstack] Failed to create event report url for endpoint: %s", + endpoint); + logger.error(message); + throw new PreBidException(message); + } + } +} diff --git a/src/main/java/org/prebid/server/analytics/pubstack/PubstackEventHandler.java b/src/main/java/org/prebid/server/analytics/pubstack/PubstackEventHandler.java new file mode 100644 index 00000000000..35491f22046 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/pubstack/PubstackEventHandler.java @@ -0,0 +1,201 @@ +package org.prebid.server.analytics.pubstack; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import java.util.zip.GZIPOutputStream; + +public class PubstackEventHandler { + + private static final Logger logger = LoggerFactory.getLogger(PubstackEventHandler.class); + private static final String SCOPE_FIELD_NAME = "scope"; + private static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; + private static final String GZIP = "gzip"; + private static final String NEW_LINE = "\n"; + + private volatile boolean enabled; + private volatile String endpoint; + private volatile String scopeId; + private final long maxByteSize; + private final long maxEventCount; + private final long reportTtlMillis; + private final long timeoutMs; + private final Vertx vertx; + private final JacksonMapper jacksonMapper; + private final HttpClient httpClient; + + private final ReentrantLock lockOnSend; + private final AtomicReference> events; + private final MultiMap headers; + private final AtomicLong byteSize; + private volatile long reportTimerId; + + public PubstackEventHandler(PubstackAnalyticsProperties pubstackAnalyticsProperties, + boolean enabled, + String endpoint, + JacksonMapper jacksonMapper, + HttpClient httpClient, + Vertx vertx) { + this.enabled = enabled; + this.endpoint = HttpUtil.validateUrl(endpoint); + this.scopeId = pubstackAnalyticsProperties.getScopeId(); + this.maxByteSize = pubstackAnalyticsProperties.getSizeBytes(); + this.maxEventCount = pubstackAnalyticsProperties.getCount(); + this.reportTtlMillis = pubstackAnalyticsProperties.getReportTtlMs(); + this.timeoutMs = pubstackAnalyticsProperties.getTimeoutMs(); + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.httpClient = Objects.requireNonNull(httpClient); + this.vertx = Objects.requireNonNull(vertx); + + this.lockOnSend = new ReentrantLock(); + this.events = new AtomicReference<>(new ConcurrentLinkedQueue<>()); + this.headers = makeHeaders(); + this.byteSize = new AtomicLong(); + if (enabled) { + this.reportTimerId = setReportTtlTimer(); + } + } + + public void handle(T event) { + if (enabled) { + buffer(event); + reportEventsOnCondition(byteSize -> byteSize.get() > maxByteSize, byteSize); + reportEventsOnCondition(eventsReference -> eventsReference.get().size() > maxEventCount, events); + } + } + + public void reportEvents() { + if (enabled) { + reportEventsOnCondition(events -> events.get().size() > 0, events); + } + } + + public void updateConfig(boolean enabled, String endpoint, String scopeId) { + updateTimerOnEnabling(enabled); + this.enabled = enabled; + this.endpoint = endpoint; + this.scopeId = scopeId; + } + + private void buffer(T event) { + final ObjectNode eventNode = jacksonMapper.mapper().valueToTree(event); + eventNode.put(SCOPE_FIELD_NAME, scopeId); + final String jsonEvent = jacksonMapper.encode(eventNode); + events.get().add(jsonEvent); + byteSize.getAndAdd(jsonEvent.getBytes().length); + } + + private boolean reportEventsOnCondition(Predicate conditionToSend, T conditionValue) { + boolean requestWasSent = false; + if (conditionToSend.test(conditionValue)) { + lockOnSend.lock(); + try { + if (conditionToSend.test(conditionValue)) { + requestWasSent = true; + sendEvents(events); + } + } catch (Exception exception) { + logger.error("[pubstack] Failed to send analytics report to endpoint {0} with a reason {1}", + endpoint, exception.getMessage()); + } finally { + lockOnSend.unlock(); + } + } + return requestWasSent; + } + + private void sendEvents(AtomicReference> events) { + final String url = HttpUtil.validateUrl(endpoint); + final Queue copyToSend = events.getAndSet(new ConcurrentLinkedQueue<>()); + + resetReportEventsConditions(); + + httpClient.request(HttpMethod.POST, url, headers, toGzippedBytes(copyToSend), timeoutMs) + .setHandler(this::handleReportResponse); + } + + private void resetReportEventsConditions() { + byteSize.set(0); + vertx.cancelTimer(reportTimerId); + reportTimerId = setReportTtlTimer(); + } + + private static byte[] toGzippedBytes(Queue events) { + return gzip(String.join(NEW_LINE, events)); + } + + private static byte[] gzip(String value) { + try (ByteArrayOutputStream obj = new ByteArrayOutputStream(); GZIPOutputStream gzip = new GZIPOutputStream( + obj)) { + + gzip.write(value.getBytes(StandardCharsets.UTF_8)); + gzip.finish(); + + return obj.toByteArray(); + } catch (IOException e) { + throw new PreBidException(String.format("[pubstack] failed to compress, skip the events : %s", + e.getMessage())); + } + } + + private void handleReportResponse(AsyncResult result) { + if (result.failed()) { + logger.error("[pubstack] Failed to send events to endpoint {0} with a reason: {1}", + endpoint, result.cause().getMessage()); + } else { + final HttpClientResponse httpClientResponse = result.result(); + final int statusCode = httpClientResponse.getStatusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + logger.error("[pubstack] Wrong code received {0} instead of 200", statusCode); + } + } + } + + private long setReportTtlTimer() { + return vertx.setTimer(reportTtlMillis, timerId -> sendOnTimer()); + } + + private void sendOnTimer() { + final boolean requestWasSent = reportEventsOnCondition(events -> events.get().size() > 0, events); + if (!requestWasSent) { + setReportTtlTimer(); + } + } + + private void updateTimerOnEnabling(boolean enabled) { + if (this.enabled && !enabled) { + vertx.cancelTimer(reportTimerId); + } else if (!this.enabled && enabled) { + reportTimerId = setReportTtlTimer(); + } + } + + private static MultiMap makeHeaders() { + return MultiMap.caseInsensitiveMultiMap() + .add(HttpHeaders.CONTENT_TYPE, APPLICATION_OCTET_STREAM) + .add(HttpHeaders.CONTENT_ENCODING, GZIP); + } +} diff --git a/src/main/java/org/prebid/server/analytics/pubstack/model/EventType.java b/src/main/java/org/prebid/server/analytics/pubstack/model/EventType.java new file mode 100644 index 00000000000..f4b1cbe0460 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/pubstack/model/EventType.java @@ -0,0 +1,7 @@ +package org.prebid.server.analytics.pubstack.model; + +public enum EventType { + + auction, cookiesync, amp, setuid, video +} + diff --git a/src/main/java/org/prebid/server/analytics/pubstack/model/PubstackAnalyticsProperties.java b/src/main/java/org/prebid/server/analytics/pubstack/model/PubstackAnalyticsProperties.java new file mode 100644 index 00000000000..8987202c56e --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/pubstack/model/PubstackAnalyticsProperties.java @@ -0,0 +1,25 @@ +package org.prebid.server.analytics.pubstack.model; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class PubstackAnalyticsProperties { + + String endpoint; + + String scopeId; + + Boolean enabled; + + Long configurationRefreshDelayMs; + + Integer sizeBytes; + + Integer count; + + Long reportTtlMs; + + Long timeoutMs; +} diff --git a/src/main/java/org/prebid/server/analytics/pubstack/model/PubstackConfig.java b/src/main/java/org/prebid/server/analytics/pubstack/model/PubstackConfig.java new file mode 100644 index 00000000000..9815fbf5047 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/pubstack/model/PubstackConfig.java @@ -0,0 +1,19 @@ +package org.prebid.server.analytics.pubstack.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.Map; + +@Value +@AllArgsConstructor(staticName = "of") +public class PubstackConfig { + + @JsonProperty("scopeId") + String scopeId; + + String endpoint; + + Map features; +} diff --git a/src/main/java/org/prebid/server/auction/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/AmpRequestFactory.java deleted file mode 100644 index adcb24a701c..00000000000 --- a/src/main/java/org/prebid/server/auction/AmpRequestFactory.java +++ /dev/null @@ -1,645 +0,0 @@ -package org.prebid.server.auction; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Banner; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Publisher; -import com.iab.openrtb.request.Regs; -import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.User; -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.web.RoutingContext; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.auction.model.Tuple2; -import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.MetricName; -import org.prebid.server.privacy.ccpa.Ccpa; -import org.prebid.server.privacy.gdpr.TcfDefinerService; -import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; -import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; -import org.prebid.server.proto.openrtb.ext.request.ExtRegs; -import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAmp; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheBids; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheVastxml; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; -import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.request.Targeting; -import org.prebid.server.util.HttpUtil; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -public class AmpRequestFactory { - - private static final Logger logger = LoggerFactory.getLogger(AmpRequestFactory.class); - - private static final String TAG_ID_REQUEST_PARAM = "tag_id"; - private static final String TARGETING_REQUEST_PARAM = "targeting"; - private static final String DEBUG_REQUEST_PARAM = "debug"; - private static final String OW_REQUEST_PARAM = "ow"; - private static final String OH_REQUEST_PARAM = "oh"; - private static final String W_REQUEST_PARAM = "w"; - private static final String H_REQUEST_PARAM = "h"; - private static final String MS_REQUEST_PARAM = "ms"; - private static final String CURL_REQUEST_PARAM = "curl"; - private static final String ACCOUNT_REQUEST_PARAM = "account"; - private static final String SLOT_REQUEST_PARAM = "slot"; - private static final String TIMEOUT_REQUEST_PARAM = "timeout"; - private static final String GDPR_CONSENT_PARAM = "gdpr_consent"; - private static final String CONSENT_PARAM = "consent_string"; - - private static final int NO_LIMIT_SPLIT_MODE = -1; - private static final String AMP_CHANNEL = "amp"; - - private final StoredRequestProcessor storedRequestProcessor; - private final AuctionRequestFactory auctionRequestFactory; - private final OrtbTypesResolver ortbTypesResolver; - private final ImplicitParametersExtractor implicitParametersExtractor; - private final FpdResolver fpdResolver; - private final TimeoutResolver timeoutResolver; - private final JacksonMapper mapper; - - public AmpRequestFactory(StoredRequestProcessor storedRequestProcessor, - AuctionRequestFactory auctionRequestFactory, - OrtbTypesResolver ortbTypesResolver, - ImplicitParametersExtractor implicitParametersExtractor, - FpdResolver fpdResolver, - TimeoutResolver timeoutResolver, - JacksonMapper mapper) { - this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); - this.auctionRequestFactory = Objects.requireNonNull(auctionRequestFactory); - this.ortbTypesResolver = Objects.requireNonNull(ortbTypesResolver); - this.implicitParametersExtractor = Objects.requireNonNull(implicitParametersExtractor); - this.fpdResolver = Objects.requireNonNull(fpdResolver); - this.timeoutResolver = Objects.requireNonNull(timeoutResolver); - this.mapper = Objects.requireNonNull(mapper); - } - - /** - * Creates {@link AuctionContext} based on {@link RoutingContext}. - */ - public Future fromRequest(RoutingContext routingContext, long startTime) { - final String tagId = routingContext.request().getParam(TAG_ID_REQUEST_PARAM); - if (StringUtils.isBlank(tagId)) { - return Future.failedFuture(new InvalidRequestException("AMP requests require an AMP tag_id")); - } - return createBidRequest(routingContext, tagId) - .compose(bidRequestWithErrors -> auctionRequestFactory.toAuctionContext( - routingContext, - bidRequestWithErrors.getLeft(), - MetricName.amp, - bidRequestWithErrors.getRight(), - startTime, - timeoutResolver)); - } - - /** - * Creates {@link BidRequest} and sets properties which were not set explicitly by the client, but can be - * updated by values derived from headers and other request attributes. - */ - private Future>> createBidRequest(RoutingContext context, String tagId) { - final List errors = new ArrayList<>(); - return storedRequestProcessor.processAmpRequest(tagId) - .map(bidRequest -> validateStoredBidRequest(tagId, bidRequest)) - .map(bidRequest -> fillExplicitParameters(bidRequest, context)) - .map(bidRequest -> overrideParameters(bidRequest, context.request(), errors)) - .map(bidRequest -> auctionRequestFactory.fillImplicitParameters(bidRequest, context, timeoutResolver)) - .map(auctionRequestFactory::validateRequest) - .map(bidRequest -> Tuple2.of(bidRequest, errors)); - } - - /** - * Throws {@link InvalidRequestException} in case of invalid {@link BidRequest}. - */ - private static BidRequest validateStoredBidRequest(String tagId, BidRequest bidRequest) { - final List imps = bidRequest.getImp(); - if (CollectionUtils.isEmpty(imps)) { - throw new InvalidRequestException( - String.format("data for tag_id='%s' does not define the required imp array.", tagId)); - } - - final int impSize = imps.size(); - if (impSize > 1) { - throw new InvalidRequestException( - String.format("data for tag_id '%s' includes %d imp elements. Only one is allowed", tagId, - impSize)); - } - - if (bidRequest.getApp() != null) { - throw new InvalidRequestException("request.app must not exist in AMP stored requests."); - } - - if (bidRequest.getExt() == null) { - throw new InvalidRequestException("AMP requests require Ext to be set"); - } - return bidRequest; - } - - /** - * - Updates {@link BidRequest}.ext.prebid.targeting and {@link BidRequest}.ext.prebid.cache.bids with default - * values if it was not included by user - * - Updates {@link Imp} security if required to ensure that amp always uses - * https protocol - * - Sets {@link BidRequest}.test = 1 if it was passed in {@link RoutingContext} - * - Updates {@link BidRequest}.ext.prebid.amp.data with all query parameters - */ - private BidRequest fillExplicitParameters(BidRequest bidRequest, RoutingContext context) { - final List imps = bidRequest.getImp(); - // Force HTTPS as AMP requires it, but pubs can forget to set it. - final Imp imp = imps.get(0); - final Integer secure = imp.getSecure(); - final boolean setSecure = secure == null || secure != 1; - - final ExtRequestPrebid prebid = bidRequest.getExt().getPrebid(); - - // AMP won't function unless ext.prebid.targeting and ext.prebid.cache.bids are defined. - // If the user didn't include them, default those here. - final boolean setDefaultTargeting; - final boolean setDefaultCache; - - final boolean setChannel; - - if (prebid == null) { - setDefaultTargeting = true; - setDefaultCache = true; - setChannel = true; - } else { - final ExtRequestTargeting targeting = prebid.getTargeting(); - setDefaultTargeting = targeting == null - || targeting.getIncludewinners() == null - || targeting.getIncludebidderkeys() == null - || targeting.getPricegranularity() == null || targeting.getPricegranularity().isNull(); - - final ExtRequestPrebidCache cache = prebid.getCache(); - setDefaultCache = cache == null || cache.equals(ExtRequestPrebidCache.EMPTY); - - setChannel = prebid.getChannel() == null; - } - - final Integer debugQueryParam = debugFromQueryStringParam(context); - - final Integer test = bidRequest.getTest(); - final Integer updatedTest = debugQueryParam != null && !Objects.equals(debugQueryParam, test) - ? debugQueryParam - : null; - - final Integer debug = prebid != null ? prebid.getDebug() : null; - final Integer updatedDebug = debugQueryParam != null && !Objects.equals(debugQueryParam, debug) - ? debugQueryParam - : null; - - final Map updatedAmpData = updateAmpData(prebid, context.request()); - - final BidRequest result; - if (setSecure - || setDefaultTargeting - || setDefaultCache - || setChannel - || updatedTest != null - || updatedDebug != null - || updatedAmpData != null) { - - result = bidRequest.toBuilder() - .imp(setSecure ? Collections.singletonList(imps.get(0).toBuilder().secure(1).build()) : imps) - .test(ObjectUtils.defaultIfNull(updatedTest, test)) - .ext(extRequest( - bidRequest, - setDefaultTargeting, - setDefaultCache, - setChannel, - updatedDebug, - updatedAmpData)) - .build(); - } else { - result = bidRequest; - } - return result; - } - - /** - * Returns debug flag from request query string if it is equal to either 0 or 1, or null if otherwise. - */ - private static Integer debugFromQueryStringParam(RoutingContext context) { - final String debug = context.request().getParam(DEBUG_REQUEST_PARAM); - return Objects.equals(debug, "1") ? Integer.valueOf(1) : Objects.equals(debug, "0") ? 0 : null; - } - - /** - * Extracts parameters from http request and overrides corresponding attributes in {@link BidRequest}. - */ - private BidRequest overrideParameters(BidRequest bidRequest, HttpServerRequest request, - List errors) { - final String requestConsentParam = request.getParam(CONSENT_PARAM); - final String requestGdprConsentParam = request.getParam(GDPR_CONSENT_PARAM); - final String consentString = ObjectUtils.firstNonNull(requestConsentParam, requestGdprConsentParam); - - String gdprConsent = null; - String ccpaConsent = null; - if (StringUtils.isNotBlank(consentString)) { - gdprConsent = TcfDefinerService.isConsentStringValid(consentString) ? consentString : null; - ccpaConsent = Ccpa.isValid(consentString) ? consentString : null; - - if (StringUtils.isAllBlank(gdprConsent, ccpaConsent)) { - final String message = String.format( - "Amp request parameter consent_string or gdpr_consent have invalid format: %s", consentString); - logger.debug(message); - errors.add(message); - } - } - - final String requestTargeting = request.getParam(TARGETING_REQUEST_PARAM); - final ObjectNode targetingNode = readTargeting(requestTargeting); - ortbTypesResolver.normalizeTargeting(targetingNode, errors, implicitParametersExtractor.refererFrom(request)); - final Targeting targeting = parseTargeting(targetingNode); - - final Site updatedSite = overrideSite(bidRequest.getSite(), request); - final Imp updatedImp = overrideImp(bidRequest.getImp().get(0), request, targetingNode); - final Long updatedTimeout = overrideTimeout(bidRequest.getTmax(), request); - final User updatedUser = overrideUser(bidRequest.getUser(), gdprConsent); - final Regs updatedRegs = overrideRegs(bidRequest.getRegs(), ccpaConsent); - final ExtRequest updatedExtBidRequest = overrideExtBidRequest(bidRequest.getExt(), targeting); - - final BidRequest result; - if (updatedSite != null || updatedImp != null || updatedTimeout != null || updatedUser != null - || updatedRegs != null || updatedExtBidRequest != null) { - result = bidRequest.toBuilder() - .site(updatedSite != null ? updatedSite : bidRequest.getSite()) - .imp(updatedImp != null ? Collections.singletonList(updatedImp) : bidRequest.getImp()) - .tmax(updatedTimeout != null ? updatedTimeout : bidRequest.getTmax()) - .user(updatedUser != null ? updatedUser : bidRequest.getUser()) - .regs(updatedRegs != null ? updatedRegs : bidRequest.getRegs()) - .ext(updatedExtBidRequest != null ? updatedExtBidRequest : bidRequest.getExt()) - .build(); - } else { - result = bidRequest; - } - return result; - } - - private ObjectNode readTargeting(String jsonTargeting) { - try { - final String decodedJsonTargeting = HttpUtil.decodeUrl(jsonTargeting); - final JsonNode jsonNodeTargeting = decodedJsonTargeting != null - ? mapper.mapper().readTree(decodedJsonTargeting) - : null; - return jsonNodeTargeting != null ? validateAndGetTargeting(jsonNodeTargeting) : null; - } catch (JsonProcessingException | IllegalArgumentException e) { - throw new InvalidRequestException(String.format("Error reading targeting json %s", e.getMessage())); - } - } - - private ObjectNode validateAndGetTargeting(JsonNode jsonNodeTargeting) { - if (jsonNodeTargeting.isObject()) { - return (ObjectNode) jsonNodeTargeting; - } else { - throw new InvalidRequestException(String.format("Error decoding targeting, expected type is `object` " - + "but was %s", jsonNodeTargeting.getNodeType().name())); - } - } - - private Targeting parseTargeting(ObjectNode targetingNode) { - try { - return targetingNode == null - ? Targeting.empty() - : mapper.mapper().treeToValue(targetingNode, Targeting.class); - } catch (JsonProcessingException e) { - throw new InvalidRequestException(String.format("Error decoding targeting from url: %s", e.getMessage())); - } - } - - private Site overrideSite(Site site, HttpServerRequest request) { - final String canonicalUrl = canonicalUrl(request); - final String accountId = request.getParam(ACCOUNT_REQUEST_PARAM); - - final boolean hasSite = site != null; - final ExtSite siteExt = hasSite ? site.getExt() : null; - final boolean shouldSetExtAmp = siteExt == null || siteExt.getAmp() == null; - - if (StringUtils.isNotBlank(canonicalUrl) || StringUtils.isNotBlank(accountId) || shouldSetExtAmp) { - final Site.SiteBuilder siteBuilder = hasSite ? site.toBuilder() : Site.builder(); - if (StringUtils.isNotBlank(canonicalUrl)) { - siteBuilder.page(canonicalUrl); - } - if (StringUtils.isNotBlank(accountId)) { - final Publisher publisher = hasSite ? site.getPublisher() : null; - final Publisher.PublisherBuilder publisherBuilder = publisher != null - ? publisher.toBuilder() : Publisher.builder(); - - siteBuilder.publisher(publisherBuilder.id(accountId).build()); - } - if (shouldSetExtAmp) { - final ObjectNode data = siteExt != null ? siteExt.getData() : null; - siteBuilder.ext(ExtSite.of(1, data)); - } - return siteBuilder.build(); - } - return null; - } - - private static String canonicalUrl(HttpServerRequest request) { - try { - return HttpUtil.decodeUrl(request.getParam(CURL_REQUEST_PARAM)); - } catch (IllegalArgumentException e) { - return null; - } - } - - private Imp overrideImp(Imp imp, HttpServerRequest request, ObjectNode targetingNode) { - final String tagId = request.getParam(SLOT_REQUEST_PARAM); - final Banner banner = imp.getBanner(); - final List overwrittenFormats = banner != null - ? createOverrideBannerFormats(request, banner.getFormat()) - : null; - if (StringUtils.isNotBlank(tagId) || CollectionUtils.isNotEmpty(overwrittenFormats) || targetingNode != null) { - return imp.toBuilder() - .tagid(StringUtils.isNotBlank(tagId) ? tagId : imp.getTagid()) - .banner(overrideBanner(imp.getBanner(), overwrittenFormats)) - .ext(fpdResolver.resolveImpExt(imp.getExt(), targetingNode)) - .build(); - } - return null; - } - - /** - * Creates formats from request parameters to override origin amp banner formats. - */ - private static List createOverrideBannerFormats(HttpServerRequest request, List formats) { - final int overrideWidth = parseIntParamOrZero(request, OW_REQUEST_PARAM); - final int width = parseIntParamOrZero(request, W_REQUEST_PARAM); - final int overrideHeight = parseIntParamOrZero(request, OH_REQUEST_PARAM); - final int height = parseIntParamOrZero(request, H_REQUEST_PARAM); - final String multiSizeParam = request.getParam(MS_REQUEST_PARAM); - - final List paramsFormats = createFormatsFromParams(overrideWidth, width, overrideHeight, height, - multiSizeParam); - - return CollectionUtils.isNotEmpty(paramsFormats) - ? paramsFormats - : updateFormatsFromParams(formats, width, height); - } - - private static Integer parseIntParamOrZero(HttpServerRequest request, String name) { - return parseIntOrZero(request.getParam(name)); - } - - private static Integer parseIntOrZero(String param) { - try { - return Integer.parseInt(param); - } catch (NumberFormatException e) { - return 0; - } - } - - /** - * Create new formats from request parameters. - */ - private static List createFormatsFromParams(Integer overrideWidth, Integer width, Integer overrideHeight, - Integer height, String multiSizeParam) { - final List formats = new ArrayList<>(); - - if (overrideWidth != 0 && overrideHeight != 0) { - formats.add(Format.builder().w(overrideWidth).h(overrideHeight).build()); - } else if (overrideWidth != 0 && height != 0) { - formats.add(Format.builder().w(overrideWidth).h(height).build()); - } else if (width != 0 && overrideHeight != 0) { - formats.add(Format.builder().w(width).h(overrideHeight).build()); - } else if (width != 0 && height != 0) { - formats.add(Format.builder().w(width).h(height).build()); - } - - // Append formats from multi-size param if exist - final List multiSizeFormats = StringUtils.isNotBlank(multiSizeParam) - ? parseMultiSizeParam(multiSizeParam) - : Collections.emptyList(); - if (!multiSizeFormats.isEmpty()) { - formats.addAll(multiSizeFormats); - } - - return formats; - } - - /** - * Updates origin amp banner formats from parameters. - */ - private static List updateFormatsFromParams(List formats, Integer width, Integer height) { - final List updatedFormats; - if (width != 0) { - updatedFormats = formats.stream() - .map(format -> Format.builder().w(width).h(format.getH()).build()) - .collect(Collectors.toList()); - } else if (height != 0) { - updatedFormats = formats.stream() - .map(format -> Format.builder().w(format.getW()).h(height).build()) - .collect(Collectors.toList()); - } else { - updatedFormats = Collections.emptyList(); - } - return updatedFormats; - } - - private static Banner overrideBanner(Banner banner, List formats) { - return banner != null && CollectionUtils.isNotEmpty(formats) - ? banner.toBuilder().format(formats).build() - : banner; - } - - private static Long overrideTimeout(Long tmax, HttpServerRequest request) { - final String timeoutQueryParam = request.getParam(TIMEOUT_REQUEST_PARAM); - if (timeoutQueryParam == null) { - return null; - } - - final long timeout; - try { - timeout = Long.parseLong(timeoutQueryParam); - } catch (NumberFormatException e) { - return null; - } - - return timeout > 0 && !Objects.equals(timeout, tmax) ? timeout : null; - } - - private User overrideUser(User user, String gdprConsent) { - if (StringUtils.isBlank(gdprConsent)) { - return null; - } - - final boolean hasUser = user != null; - final ExtUser extUser = hasUser ? user.getExt() : null; - - final ExtUser.ExtUserBuilder extUserBuilder = extUser != null - ? extUser.toBuilder() - : ExtUser.builder(); - - if (StringUtils.isNotBlank(gdprConsent)) { - extUserBuilder.consent(gdprConsent); - } - - final User.UserBuilder userBuilder = hasUser ? user.toBuilder() : User.builder(); - - return userBuilder - .ext(extUserBuilder.build()) - .build(); - } - - private Regs overrideRegs(Regs regs, String ccpaConsent) { - if (StringUtils.isBlank(ccpaConsent)) { - return null; - } - - Integer coppa = null; - Integer gdpr = null; - if (regs != null) { - coppa = regs.getCoppa(); - gdpr = regs.getExt().getGdpr(); - } - - return Regs.of(coppa, ExtRegs.of(gdpr, ccpaConsent)); - } - - /** - * Overrides {@link ExtRequest} with first party data. - */ - private ExtRequest overrideExtBidRequest(ExtRequest extRequest, Targeting targeting) { - return fpdResolver.resolveBidRequestExt(extRequest, targeting); - } - - private static List parseMultiSizeParam(String ms) { - final String[] formatStrings = ms.split(",", NO_LIMIT_SPLIT_MODE); - final List formats = new ArrayList<>(); - for (String format : formatStrings) { - final String[] widthHeight = format.split("x", NO_LIMIT_SPLIT_MODE); - if (widthHeight.length != 2) { - return Collections.emptyList(); - } - - final Integer width = parseIntOrZero(widthHeight[0]); - final Integer height = parseIntOrZero(widthHeight[1]); - - if (width == 0 && height == 0) { - return Collections.emptyList(); - } - - formats.add(Format.builder() - .w(width) - .h(height) - .build()); - } - return formats; - } - - private static Map updateAmpData(ExtRequestPrebid prebid, HttpServerRequest request) { - final ExtRequestPrebidAmp amp = prebid != null ? prebid.getAmp() : null; - final Map existingAmpData = amp != null ? amp.getData() : null; - - final MultiMap queryParams = request.params(); - if (queryParams.isEmpty()) { - return null; - } - - final Map ampQueryData = queryParams.entries().stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (value1, value2) -> value1)); - - final Map updatedAmpData = - new HashMap<>(ObjectUtils.defaultIfNull(existingAmpData, Collections.emptyMap())); - updatedAmpData.putAll(ampQueryData); - - return updatedAmpData; - } - - /** - * Creates updated bidrequest.ext {@link ObjectNode}. - */ - private ExtRequest extRequest(BidRequest bidRequest, - boolean setDefaultTargeting, - boolean setDefaultCache, - boolean setChannel, - Integer updatedDebug, - Map updatedAmpData) { - - final ExtRequest result; - if (setDefaultTargeting || setDefaultCache || setChannel || updatedDebug != null || updatedAmpData != null) { - final ExtRequest requestExt = bidRequest.getExt(); - final ExtRequestPrebid prebid = requestExt != null ? requestExt.getPrebid() : null; - final ExtRequestPrebid.ExtRequestPrebidBuilder prebidBuilder = prebid != null - ? prebid.toBuilder() - : ExtRequestPrebid.builder(); - - if (setDefaultTargeting) { - prebidBuilder.targeting(createTargetingWithDefaults(prebid)); - } - if (setDefaultCache) { - prebidBuilder.cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(null, null), - ExtRequestPrebidCacheVastxml.of(null, null), null)); - } - if (setChannel) { - prebidBuilder.channel(ExtRequestPrebidChannel.of(AMP_CHANNEL)); - } - if (updatedDebug != null) { - prebidBuilder.debug(updatedDebug); - } - - if (updatedAmpData != null) { - prebidBuilder.amp(ExtRequestPrebidAmp.of(updatedAmpData)); - } - - result = ExtRequest.of(prebidBuilder.build()); - } else { - result = bidRequest.getExt(); - } - return result; - } - - /** - * Creates updated with default values bidrequest.ext.targeting {@link ExtRequestTargeting} if at least one of it's - * child properties is missed or entire targeting does not exist. - */ - private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) { - final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; - final boolean isTargetingNull = targeting == null; - - final JsonNode priceGranularityNode = isTargetingNull ? null : targeting.getPricegranularity(); - final boolean isPriceGranularityNull = priceGranularityNode == null || priceGranularityNode.isNull(); - final JsonNode outgoingPriceGranularityNode = isPriceGranularityNull - ? mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)) - : priceGranularityNode; - - final ExtMediaTypePriceGranularity mediaTypePriceGranularity = isTargetingNull - ? null : targeting.getMediatypepricegranularity(); - - final boolean includeWinners = isTargetingNull || targeting.getIncludewinners() == null - || targeting.getIncludewinners(); - - final boolean includeBidderKeys = isTargetingNull || targeting.getIncludebidderkeys() == null - || targeting.getIncludebidderkeys(); - - return ExtRequestTargeting.builder() - .pricegranularity(outgoingPriceGranularityNode) - .mediatypepricegranularity(mediaTypePriceGranularity) - .includewinners(includeWinners) - .includebidderkeys(includeBidderKeys) - .build(); - } -} diff --git a/src/main/java/org/prebid/server/auction/AmpResponsePostProcessor.java b/src/main/java/org/prebid/server/auction/AmpResponsePostProcessor.java index 1fce5be513e..7928cccd259 100644 --- a/src/main/java/org/prebid/server/auction/AmpResponsePostProcessor.java +++ b/src/main/java/org/prebid/server/auction/AmpResponsePostProcessor.java @@ -16,14 +16,14 @@ public interface AmpResponsePostProcessor { /** * This method is called prior sending the response back to the client. * - * @param bidRequest original auction request - * @param bidResponse auction result - * @param ampResponse AMP RTC response - * @param context request's context + * @param bidRequest original auction request + * @param bidResponse auction result + * @param ampResponse AMP RTC response + * @param routingContext request's context * @return a {@link Future} with (possibly modified) amp response result */ Future postProcess(BidRequest bidRequest, BidResponse bidResponse, AmpResponse ampResponse, - RoutingContext context); + RoutingContext routingContext); /** * Returns {@link NoOpAmpResponsePostProcessor} instance that just does nothing. @@ -39,7 +39,7 @@ class NoOpAmpResponsePostProcessor implements AmpResponsePostProcessor { @Override public Future postProcess(BidRequest bidRequest, BidResponse bidResponse, AmpResponse ampResponse, - RoutingContext context) { + RoutingContext routingContext) { return Future.succeededFuture(ampResponse); } } diff --git a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java deleted file mode 100644 index 6fd98ecfd1c..00000000000 --- a/src/main/java/org/prebid/server/auction/AuctionRequestFactory.java +++ /dev/null @@ -1,935 +0,0 @@ -package org.prebid.server.auction; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Geo; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Publisher; -import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.Source; -import com.iab.openrtb.request.User; -import io.netty.buffer.ByteBufInputStream; -import io.vertx.core.Future; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.web.RoutingContext; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.cookie.UidsCookieService; -import org.prebid.server.exception.BlacklistedAccountException; -import org.prebid.server.exception.BlacklistedAppException; -import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.geolocation.model.GeoInfo; -import org.prebid.server.identity.IdGenerator; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.log.ConditionalLogger; -import org.prebid.server.metric.MetricName; -import org.prebid.server.privacy.model.PrivacyContext; -import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; -import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; -import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; -import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; -import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; -import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.settings.ApplicationSettings; -import org.prebid.server.settings.model.Account; -import org.prebid.server.util.HttpUtil; -import org.prebid.server.validation.RequestValidator; -import org.prebid.server.validation.model.ValidationResult; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Currency; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -/** - * Used in OpenRTB request processing. - */ -public class AuctionRequestFactory { - - private static final Logger logger = LoggerFactory.getLogger(AuctionRequestFactory.class); - private static final ConditionalLogger EMPTY_ACCOUNT_LOGGER = new ConditionalLogger("empty_account", logger); - private static final ConditionalLogger UNKNOWN_ACCOUNT_LOGGER = new ConditionalLogger("unknown_account", logger); - - public static final String WEB_CHANNEL = "web"; - public static final String APP_CHANNEL = "app"; - - private final long maxRequestSize; - private final boolean enforceValidAccount; - private final boolean shouldCacheOnlyWinningBids; - private final String adServerCurrency; - private final List blacklistedApps; - private final List blacklistedAccounts; - private final StoredRequestProcessor storedRequestProcessor; - private final ImplicitParametersExtractor paramsExtractor; - private final IpAddressHelper ipAddressHelper; - private final UidsCookieService uidsCookieService; - private final BidderCatalog bidderCatalog; - private final RequestValidator requestValidator; - private final InterstitialProcessor interstitialProcessor; - private final TimeoutResolver timeoutResolver; - private final TimeoutFactory timeoutFactory; - private final ApplicationSettings applicationSettings; - private final IdGenerator idGenerator; - private final PrivacyEnforcementService privacyEnforcementService; - private final JacksonMapper mapper; - private final OrtbTypesResolver ortbTypesResolver; - - public AuctionRequestFactory(long maxRequestSize, - boolean enforceValidAccount, - boolean shouldCacheOnlyWinningBids, - String adServerCurrency, - List blacklistedApps, - List blacklistedAccounts, - StoredRequestProcessor storedRequestProcessor, - ImplicitParametersExtractor paramsExtractor, - IpAddressHelper ipAddressHelper, - UidsCookieService uidsCookieService, - BidderCatalog bidderCatalog, - RequestValidator requestValidator, - InterstitialProcessor interstitialProcessor, - OrtbTypesResolver ortbTypesResolver, - TimeoutResolver timeoutResolver, - TimeoutFactory timeoutFactory, - ApplicationSettings applicationSettings, - IdGenerator idGenerator, - PrivacyEnforcementService privacyEnforcementService, - JacksonMapper mapper) { - - this.maxRequestSize = maxRequestSize; - this.enforceValidAccount = enforceValidAccount; - this.shouldCacheOnlyWinningBids = shouldCacheOnlyWinningBids; - this.adServerCurrency = validateCurrency(Objects.requireNonNull(adServerCurrency)); - this.blacklistedApps = Objects.requireNonNull(blacklistedApps); - this.blacklistedAccounts = Objects.requireNonNull(blacklistedAccounts); - this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); - this.paramsExtractor = Objects.requireNonNull(paramsExtractor); - this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); - this.uidsCookieService = Objects.requireNonNull(uidsCookieService); - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - this.requestValidator = Objects.requireNonNull(requestValidator); - this.interstitialProcessor = Objects.requireNonNull(interstitialProcessor); - this.ortbTypesResolver = Objects.requireNonNull(ortbTypesResolver); - this.timeoutResolver = Objects.requireNonNull(timeoutResolver); - this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.idGenerator = Objects.requireNonNull(idGenerator); - this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); - this.mapper = Objects.requireNonNull(mapper); - } - - /** - * Validates ISO-4217 currency code. - */ - private static String validateCurrency(String code) { - try { - Currency.getInstance(code); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(String.format("Currency code supplied is not valid: %s", code), e); - } - return code; - } - - /** - * Creates {@link AuctionContext} based on {@link RoutingContext}. - */ - public Future fromRequest(RoutingContext routingContext, long startTime) { - final List errors = new ArrayList<>(); - final BidRequest incomingBidRequest; - try { - incomingBidRequest = parseRequest(routingContext, errors); - } catch (InvalidRequestException e) { - return Future.failedFuture(e); - } - - return updateBidRequest(routingContext, incomingBidRequest) - .compose(bidRequest -> toAuctionContext( - routingContext, - bidRequest, - requestTypeMetric(bidRequest), - errors, - startTime, - timeoutResolver)); - } - - /** - * Returns filled out {@link AuctionContext} based on given arguments. - *

- * Note: {@link TimeoutResolver} used here as argument because this method is utilized in AMP processing. - */ - Future toAuctionContext(RoutingContext routingContext, - BidRequest bidRequest, - MetricName requestTypeMetric, - List errors, - long startTime, - TimeoutResolver timeoutResolver) { - - final Timeout timeout = timeout(bidRequest, startTime, timeoutResolver); - - return accountFrom(bidRequest, timeout, routingContext) - .compose(account -> privacyEnforcementService.contextFromBidRequest( - bidRequest, account, requestTypeMetric, timeout) - .map(privacyContext -> AuctionContext.builder() - .routingContext(routingContext) - .uidsCookie(uidsCookieService.parseFromRequest(routingContext)) - .bidRequest(enrichBidRequestWithAccountAndPrivacyData( - bidRequest, account, privacyContext)) - .requestTypeMetric(requestTypeMetric) - .timeout(timeout) - .account(account) - .prebidErrors(errors) - .privacyContext(privacyContext) - .geoInfo(privacyContext.getTcfContext().getGeoInfo()) - .build())); - } - - /** - * Parses request body to {@link BidRequest}. - *

- * Throws {@link InvalidRequestException} if body is empty, exceeds max request size or couldn't be deserialized. - */ - private BidRequest parseRequest(RoutingContext context, List errors) { - final Buffer body = context.getBody(); - if (body == null) { - throw new InvalidRequestException("Incoming request has no body"); - } - - if (body.length() > maxRequestSize) { - throw new InvalidRequestException( - String.format("Request size exceeded max size of %d bytes.", maxRequestSize)); - } - - final JsonNode bidRequestNode; - try (ByteBufInputStream inputStream = new ByteBufInputStream(body.getByteBuf())) { - bidRequestNode = mapper.mapper().readTree(inputStream); - } catch (IOException e) { - throw new InvalidRequestException(String.format("Error decoding bidRequest: %s", e.getMessage())); - } - - final String referer = paramsExtractor.refererFrom(context.request()); - ortbTypesResolver.normalizeBidRequest(bidRequestNode, errors, referer); - - try { - return mapper.mapper().treeToValue(bidRequestNode, BidRequest.class); - } catch (JsonProcessingException e) { - throw new InvalidRequestException(String.format("Error decoding bidRequest: %s", e.getMessage())); - } - } - - /** - * Sets {@link BidRequest} properties which were not set explicitly by the client, but can be - * updated by values derived from headers and other request attributes. - */ - private Future updateBidRequest(RoutingContext context, BidRequest bidRequest) { - return storedRequestProcessor.processStoredRequests(bidRequest) - .map(resolvedBidRequest -> fillImplicitParameters(resolvedBidRequest, context, timeoutResolver)) - .map(this::validateRequest) - .map(interstitialProcessor::process); - } - - /** - * If needed creates a new {@link BidRequest} which is a copy of original but with some fields set with values - * derived from request parameters (headers, cookie etc.). - *

- * Note: {@link TimeoutResolver} used here as argument because this method is utilized in AMP processing. - */ - BidRequest fillImplicitParameters(BidRequest bidRequest, RoutingContext context, TimeoutResolver timeoutResolver) { - checkBlacklistedApp(bidRequest); - - final BidRequest result; - final HttpServerRequest request = context.request(); - - final Device device = bidRequest.getDevice(); - final Device populatedDevice = populateDevice(device, request); - - final Site site = bidRequest.getSite(); - final Site populatedSite = bidRequest.getApp() != null ? null : populateSite(site, request); - - final User user = bidRequest.getUser(); - final User populatedUser = populateUser(user); - - final Source source = bidRequest.getSource(); - final Source populatedSource = populateSource(source); - - final List imps = bidRequest.getImp(); - final List populatedImps = populateImps(imps, request); - - final Integer at = bidRequest.getAt(); - final Integer resolvedAt = resolveAt(at); - - final List cur = bidRequest.getCur(); - final List resolvedCurrencies = resolveCurrencies(cur); - - final Long tmax = bidRequest.getTmax(); - final Long resolvedTmax = resolveTmax(tmax, timeoutResolver); - - final ExtRequest ext = bidRequest.getExt(); - final ExtRequest populatedExt = populateRequestExt( - ext, bidRequest, ObjectUtils.defaultIfNull(populatedImps, imps)); - - if (populatedDevice != null || populatedSite != null || populatedUser != null || populatedSource != null - || populatedImps != null || resolvedAt != null || resolvedCurrencies != null || resolvedTmax != null - || populatedExt != null) { - - result = bidRequest.toBuilder() - .device(populatedDevice != null ? populatedDevice : device) - .site(populatedSite != null ? populatedSite : site) - .user(populatedUser != null ? populatedUser : user) - .source(populatedSource != null ? populatedSource : source) - .imp(populatedImps != null ? populatedImps : imps) - .at(resolvedAt != null ? resolvedAt : at) - .cur(resolvedCurrencies != null ? resolvedCurrencies : cur) - .tmax(resolvedTmax != null ? resolvedTmax : tmax) - .ext(populatedExt != null ? populatedExt : ext) - .build(); - } else { - result = bidRequest; - } - return result; - } - - private void checkBlacklistedApp(BidRequest bidRequest) { - final App app = bidRequest.getApp(); - final String appId = app != null ? app.getId() : null; - - if (StringUtils.isNotBlank(appId) && blacklistedApps.contains(appId)) { - throw new BlacklistedAppException( - String.format("Prebid-server does not process requests from App ID: %s", appId)); - } - } - - /** - * Populates the request body's 'device' section from the incoming http request if the original is partially filled - * and the request contains necessary info (User-Agent, IP-address). - */ - private Device populateDevice(Device device, HttpServerRequest request) { - final String deviceIp = device != null ? device.getIp() : null; - final String deviceIpv6 = device != null ? device.getIpv6() : null; - - String resolvedIp = sanitizeIp(deviceIp, IpAddress.IP.v4); - String resolvedIpv6 = sanitizeIp(deviceIpv6, IpAddress.IP.v6); - - if (resolvedIp == null && resolvedIpv6 == null) { - final IpAddress requestIp = findIpFromRequest(request); - - resolvedIp = getIpIfVersionIs(requestIp, IpAddress.IP.v4); - resolvedIpv6 = getIpIfVersionIs(requestIp, IpAddress.IP.v6); - } - - logWarnIfNoIp(resolvedIp, resolvedIpv6); - - final String ua = device != null ? device.getUa() : null; - - if (!Objects.equals(deviceIp, resolvedIp) - || !Objects.equals(deviceIpv6, resolvedIpv6) - || StringUtils.isBlank(ua)) { - - final Device.DeviceBuilder builder = device == null ? Device.builder() : device.toBuilder(); - builder.ua(StringUtils.isNotBlank(ua) ? ua : paramsExtractor.uaFrom(request)); - - builder - .ip(resolvedIp) - .ipv6(resolvedIpv6); - - return builder.build(); - } - - return null; - } - - private String sanitizeIp(String ip, IpAddress.IP version) { - final IpAddress ipAddress = ip != null ? ipAddressHelper.toIpAddress(ip) : null; - return ipAddress != null && ipAddress.getVersion() == version ? ipAddress.getIp() : null; - } - - private IpAddress findIpFromRequest(HttpServerRequest request) { - final List requestIps = paramsExtractor.ipFrom(request); - return requestIps.stream() - .map(ipAddressHelper::toIpAddress) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - - private static String getIpIfVersionIs(IpAddress requestIp, IpAddress.IP version) { - return requestIp != null && requestIp.getVersion() == version ? requestIp.getIp() : null; - } - - private void logWarnIfNoIp(String resolvedIp, String resolvedIpv6) { - if (resolvedIp == null && resolvedIpv6 == null) { - logger.warn("No IP address found in OpenRTB request and HTTP request headers."); - } - } - - /** - * Populates the request body's 'site' section from the incoming http request if the original is partially filled - * and the request contains necessary info (domain, page). - */ - private Site populateSite(Site site, HttpServerRequest request) { - Site result = null; - - final String page = site != null ? site.getPage() : null; - final String domain = site != null ? site.getDomain() : null; - final ExtSite siteExt = site != null ? site.getExt() : null; - final ObjectNode data = siteExt != null ? siteExt.getData() : null; - final boolean shouldSetExtAmp = siteExt == null || siteExt.getAmp() == null; - final ExtSite modifiedSiteExt = shouldSetExtAmp - ? ExtSite.of(0, data) - : null; - - String referer = null; - String parsedDomain = null; - if (StringUtils.isBlank(page) || StringUtils.isBlank(domain)) { - referer = paramsExtractor.refererFrom(request); - if (StringUtils.isNotBlank(referer)) { - try { - parsedDomain = paramsExtractor.domainFrom(referer); - } catch (PreBidException e) { - logger.warn("Error occurred while populating bid request: {0}", e.getMessage()); - logger.debug("Error occurred while populating bid request", e); - } - } - } - final boolean shouldModifyPageOrDomain = referer != null && parsedDomain != null; - - if (shouldModifyPageOrDomain || shouldSetExtAmp) { - final Site.SiteBuilder builder = site == null ? Site.builder() : site.toBuilder(); - if (shouldModifyPageOrDomain) { - builder.domain(StringUtils.isNotBlank(domain) ? domain : parsedDomain); - builder.page(StringUtils.isNotBlank(page) ? page : referer); - } - if (shouldSetExtAmp) { - builder.ext(modifiedSiteExt); - } - result = builder.build(); - } - return result; - } - - /** - * Populates the request body's 'user' section from the incoming http request if the original is partially filled. - */ - private User populateUser(User user) { - final ExtUser ext = userExtOrNull(user); - - if (ext != null) { - return user.toBuilder().ext(ext).build(); - } - return null; - } - - /** - * Returns updated {@link ExtUser} or null if no updates needed. - */ - private ExtUser userExtOrNull(User user) { - final ExtUser extUser = user != null ? user.getExt() : null; - - final ExtUserDigiTrust digitrust = extUser != null ? extUser.getDigitrust() : null; - if (digitrust != null && digitrust.getPref() == null) { - return extUser.toBuilder() - .digitrust(ExtUserDigiTrust.of(digitrust.getId(), digitrust.getKeyv(), 0)) - .build(); - } - return null; - } - - /** - * Returns {@link Source} with updated source.tid or null if nothing changed. - */ - private Source populateSource(Source source) { - final String tid = source != null ? source.getTid() : null; - if (StringUtils.isEmpty(tid)) { - final String generatedId = idGenerator.generateId(); - if (StringUtils.isNotEmpty(generatedId)) { - final Source.SourceBuilder builder = source != null ? source.toBuilder() : Source.builder(); - return builder - .tid(generatedId) - .build(); - } - } - return null; - } - - /** - * Updates imps with security 1, when secured request was received and imp security was not defined. - */ - private List populateImps(List imps, HttpServerRequest request) { - List result = null; - - if (Objects.equals(paramsExtractor.secureFrom(request), 1) - && imps.stream().map(Imp::getSecure).anyMatch(Objects::isNull)) { - result = imps.stream() - .map(imp -> imp.getSecure() == null ? imp.toBuilder().secure(1).build() : imp) - .collect(Collectors.toList()); - } - return result; - } - - /** - * Returns updated {@link ExtRequest} if required or null otherwise. - */ - private ExtRequest populateRequestExt(ExtRequest ext, BidRequest bidRequest, List imps) { - if (ext == null) { - return null; - } - - final ExtRequestPrebid prebid = ext.getPrebid(); - - final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, getImpMediaTypes(imps)); - final Map updatedAliases = aliasesOrNull(prebid, imps); - final ExtRequestPrebidCache updatedCache = cacheOrNull(prebid); - final ExtRequestPrebidChannel updatedChannel = channelOrNull(prebid, bidRequest); - - if (updatedTargeting != null || updatedAliases != null || updatedCache != null || updatedChannel != null) { - final ExtRequestPrebid.ExtRequestPrebidBuilder prebidBuilder = prebid != null - ? prebid.toBuilder() - : ExtRequestPrebid.builder(); - - return ExtRequest.of(prebidBuilder - .aliases(ObjectUtils.defaultIfNull(updatedAliases, - getIfNotNull(prebid, ExtRequestPrebid::getAliases))) - .targeting(ObjectUtils.defaultIfNull(updatedTargeting, - getIfNotNull(prebid, ExtRequestPrebid::getTargeting))) - .cache(ObjectUtils.defaultIfNull(updatedCache, - getIfNotNull(prebid, ExtRequestPrebid::getCache))) - .channel(ObjectUtils.defaultIfNull(updatedChannel, - getIfNotNull(prebid, ExtRequestPrebid::getChannel))) - .build()); - } - - return null; - } - - /** - * Iterates through impressions to check what media types each impression has and add them to the resulting set. - * If all four media types are present - no point to look any further. - */ - private static Set getImpMediaTypes(List imps) { - final Set impMediaTypes = new HashSet<>(); - for (Imp imp : imps) { - checkImpMediaTypes(imp, impMediaTypes); - if (impMediaTypes.size() > 3) { - break; - } - } - return impMediaTypes; - } - - /** - * Adds an existing media type to a set. - */ - private static void checkImpMediaTypes(Imp imp, Set impsMediaTypes) { - if (imp.getBanner() != null) { - impsMediaTypes.add(BidType.banner); - } - if (imp.getVideo() != null) { - impsMediaTypes.add(BidType.video); - } - if (imp.getAudio() != null) { - impsMediaTypes.add(BidType.audio); - } - if (imp.getXNative() != null) { - impsMediaTypes.add(BidType.xNative); - } - } - - /** - * Returns populated {@link ExtRequestTargeting} or null if no changes were applied. - */ - private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, Set impMediaTypes) { - final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; - - final boolean isTargetingNotNull = targeting != null; - final boolean isPriceGranularityNull = isTargetingNotNull - && (targeting.getPricegranularity() == null || targeting.getPricegranularity().isNull()); - final boolean isPriceGranularityTextual = isTargetingNotNull && !isPriceGranularityNull - && targeting.getPricegranularity().isTextual(); - final boolean isIncludeWinnersNull = isTargetingNotNull && targeting.getIncludewinners() == null; - final boolean isIncludeBidderKeysNull = isTargetingNotNull && targeting.getIncludebidderkeys() == null; - - final ExtRequestTargeting result; - if (isPriceGranularityNull || isPriceGranularityTextual || isIncludeWinnersNull || isIncludeBidderKeysNull) { - result = ExtRequestTargeting.builder() - .pricegranularity(populatePriceGranularity(targeting, isPriceGranularityNull, - isPriceGranularityTextual, impMediaTypes)) - .mediatypepricegranularity(targeting.getMediatypepricegranularity()) - .includewinners(isIncludeWinnersNull || targeting.getIncludewinners()) - .includebidderkeys(isIncludeBidderKeysNull - ? !isWinningOnly(prebid.getCache()) - : targeting.getIncludebidderkeys()) - .build(); - } else { - result = null; - } - return result; - } - - /** - * Returns winning only flag value. - */ - private boolean isWinningOnly(ExtRequestPrebidCache cache) { - final Boolean cacheWinningOnly = cache != null ? cache.getWinningonly() : null; - return ObjectUtils.defaultIfNull(cacheWinningOnly, shouldCacheOnlyWinningBids); - } - - /** - * Populates priceGranularity with converted value. - *

- * In case of missing Json node and missing media type price granularities - sets default custom value. - * In case of valid string price granularity replaced it with appropriate custom view. - * In case of invalid string value throws {@link InvalidRequestException}. - */ - private JsonNode populatePriceGranularity(ExtRequestTargeting targeting, boolean isPriceGranularityNull, - boolean isPriceGranularityTextual, Set impMediaTypes) { - final JsonNode priceGranularityNode = targeting.getPricegranularity(); - - final boolean hasAllMediaTypes = checkExistingMediaTypes(targeting.getMediatypepricegranularity()) - .containsAll(impMediaTypes); - - if (isPriceGranularityNull && !hasAllMediaTypes) { - return mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)); - } - if (isPriceGranularityTextual) { - final PriceGranularity priceGranularity; - try { - priceGranularity = PriceGranularity.createFromString(priceGranularityNode.textValue()); - } catch (PreBidException e) { - throw new InvalidRequestException(e.getMessage()); - } - return mapper.mapper().valueToTree(ExtPriceGranularity.from(priceGranularity)); - } - return priceGranularityNode; - } - - /** - * Checks {@link ExtMediaTypePriceGranularity} object for present media types and returns a set of existing ones. - */ - private static Set checkExistingMediaTypes(ExtMediaTypePriceGranularity mediaTypePriceGranularity) { - if (mediaTypePriceGranularity == null) { - return Collections.emptySet(); - } - final Set priceGranularityTypes = new HashSet<>(); - - final JsonNode banner = mediaTypePriceGranularity.getBanner(); - if (banner != null && !banner.isNull()) { - priceGranularityTypes.add(BidType.banner); - } - final JsonNode video = mediaTypePriceGranularity.getVideo(); - if (video != null && !video.isNull()) { - priceGranularityTypes.add(BidType.video); - } - final JsonNode xNative = mediaTypePriceGranularity.getXNative(); - if (xNative != null && !xNative.isNull()) { - priceGranularityTypes.add(BidType.xNative); - } - return priceGranularityTypes; - } - - /** - * Returns aliases according to request.imp[i].ext.{bidder} - * or null (if no aliases at all or they are already presented in request). - */ - private Map aliasesOrNull(ExtRequestPrebid prebid, List imps) { - final Map aliases = getIfNotNullOrDefault(prebid, ExtRequestPrebid::getAliases, - Collections.emptyMap()); - - // go through imps' bidders and figure out preconfigured aliases - final Map resolvedAliases = imps.stream() - .filter(Objects::nonNull) - .filter(imp -> imp.getExt() != null) // request validator is not called yet - .flatMap(imp -> asStream(imp.getExt().fieldNames()) - .filter(bidder -> !aliases.containsKey(bidder)) - .filter(bidderCatalog::isAlias)) - .distinct() - .collect(Collectors.toMap(Function.identity(), bidderCatalog::nameByAlias)); - - final Map result; - if (resolvedAliases.isEmpty()) { - result = null; - } else { - result = new HashMap<>(aliases); - result.putAll(resolvedAliases); - } - return result; - } - - /** - * Returns populated {@link ExtRequestPrebidCache} or null if no changes were applied. - */ - private ExtRequestPrebidCache cacheOrNull(ExtRequestPrebid prebid) { - final ExtRequestPrebidCache cache = prebid != null ? prebid.getCache() : null; - final Boolean cacheWinningOnly = cache != null ? cache.getWinningonly() : null; - if (cacheWinningOnly == null && shouldCacheOnlyWinningBids) { - return ExtRequestPrebidCache.of( - getIfNotNull(cache, ExtRequestPrebidCache::getBids), - getIfNotNull(cache, ExtRequestPrebidCache::getVastxml), - true); - } - return null; - } - - /** - * Returns populated {@link ExtRequestPrebidChannel} or null if no changes were applied. - */ - private ExtRequestPrebidChannel channelOrNull(ExtRequestPrebid prebid, BidRequest bidRequest) { - final String existingChannelName = getIfNotNull(getIfNotNull(prebid, - ExtRequestPrebid::getChannel), - ExtRequestPrebidChannel::getName); - - if (StringUtils.isNotBlank(existingChannelName)) { - return null; - } - - if (bidRequest.getSite() != null) { - return ExtRequestPrebidChannel.of(WEB_CHANNEL); - } else if (bidRequest.getApp() != null) { - return ExtRequestPrebidChannel.of(APP_CHANNEL); - } - - return null; - } - - /** - * Returns updated request.at or null if nothing changed. - *

- * Set the auction type to 1 if it wasn't on the request, since header bidding is generally a first-price auction. - */ - private static Integer resolveAt(Integer at) { - return at == null || at == 0 ? 1 : null; - } - - /** - * Returns default list of currencies if it wasn't on the request, otherwise null. - */ - private List resolveCurrencies(List currencies) { - return CollectionUtils.isEmpty(currencies) && adServerCurrency != null - ? Collections.singletonList(adServerCurrency) - : null; - } - - /** - * Determines request timeout with the help of {@link TimeoutResolver}. - * Returns resolved new value or null if existing request timeout doesn't need to update. - */ - private static Long resolveTmax(Long requestTimeout, TimeoutResolver timeoutResolver) { - final long timeout = timeoutResolver.resolve(requestTimeout); - return !Objects.equals(requestTimeout, timeout) ? timeout : null; - } - - private static Stream asStream(Iterator iterator) { - final Iterable iterable = () -> iterator; - return StreamSupport.stream(iterable.spliterator(), false); - } - - private static R getIfNotNull(T target, Function getter) { - return target != null ? getter.apply(target) : null; - } - - private static R getIfNotNullOrDefault(T target, Function getter, R defaultValue) { - return ObjectUtils.defaultIfNull(getIfNotNull(target, getter), defaultValue); - } - - /** - * Performs thorough validation of fully constructed {@link BidRequest} that is going to be used to hold an auction. - */ - BidRequest validateRequest(BidRequest bidRequest) { - final ValidationResult validationResult = requestValidator.validate(bidRequest); - if (validationResult.hasErrors()) { - throw new InvalidRequestException(validationResult.getErrors()); - } - return bidRequest; - } - - /** - * Returns {@link Timeout} based on request.tmax and adjustment value of {@link TimeoutResolver}. - */ - private Timeout timeout(BidRequest bidRequest, long startTime, TimeoutResolver timeoutResolver) { - final long timeout = timeoutResolver.adjustTimeout(bidRequest.getTmax()); - return timeoutFactory.create(startTime, timeout); - } - - /** - * Returns {@link Account} fetched by {@link ApplicationSettings}. - */ - private Future accountFrom(BidRequest bidRequest, Timeout timeout, RoutingContext routingContext) { - final String accountId = accountIdFrom(bidRequest); - final boolean blankAccountId = StringUtils.isBlank(accountId); - - if (CollectionUtils.isNotEmpty(blacklistedAccounts) && !blankAccountId - && blacklistedAccounts.contains(accountId)) { - throw new BlacklistedAccountException(String.format("Prebid-server has blacklisted Account ID: %s, please " - + "reach out to the prebid server host.", accountId)); - } - - return blankAccountId - ? responseForEmptyAccount(routingContext) - : applicationSettings.getAccountById(accountId, timeout) - .recover(exception -> accountFallback(exception, accountId, routingContext)); - } - - /** - * Extracts publisher id either from {@link BidRequest}.app.publisher or {@link BidRequest}.site.publisher. - * If neither is present returns empty string. - */ - private String accountIdFrom(BidRequest bidRequest) { - final App app = bidRequest.getApp(); - final Publisher appPublisher = app != null ? app.getPublisher() : null; - final Site site = bidRequest.getSite(); - final Publisher sitePublisher = site != null ? site.getPublisher() : null; - - final Publisher publisher = ObjectUtils.defaultIfNull(appPublisher, sitePublisher); - final String publisherId = publisher != null ? resolvePublisherId(publisher) : null; - return ObjectUtils.defaultIfNull(publisherId, StringUtils.EMPTY); - } - - /** - * Resolves what value should be used as a publisher id - either taken from publisher.ext.parentAccount - * or publisher.id in this respective priority. - */ - private String resolvePublisherId(Publisher publisher) { - final String parentAccountId = parentAccountIdFromExtPublisher(publisher.getExt()); - return ObjectUtils.defaultIfNull(parentAccountId, publisher.getId()); - } - - /** - * Parses publisher.ext and returns parentAccount value from it. Returns null if any parsing error occurs. - */ - private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { - final ExtPublisherPrebid extPublisherPrebid = extPublisher != null ? extPublisher.getPrebid() : null; - return extPublisherPrebid != null ? StringUtils.stripToNull(extPublisherPrebid.getParentAccount()) : null; - } - - private Future responseForEmptyAccount(RoutingContext routingContext) { - EMPTY_ACCOUNT_LOGGER.warn(accountErrorMessage("Account not specified", routingContext), 100); - return responseForUnknownAccount(StringUtils.EMPTY); - } - - private static String accountErrorMessage(String message, RoutingContext routingContext) { - final HttpServerRequest request = routingContext.request(); - return String.format("%s, Url: %s and Referer: %s", message, request.absoluteURI(), - request.headers().get(HttpUtil.REFERER_HEADER)); - } - - private Future accountFallback(Throwable exception, String accountId, - RoutingContext routingContext) { - if (exception instanceof PreBidException) { - UNKNOWN_ACCOUNT_LOGGER.warn(accountErrorMessage(exception.getMessage(), routingContext), 100); - } else { - logger.warn("Error occurred while fetching account: {0}", exception.getMessage()); - logger.debug("Error occurred while fetching account", exception); - } - - // hide all errors occurred while fetching account - return responseForUnknownAccount(accountId); - } - - private Future responseForUnknownAccount(String accountId) { - return enforceValidAccount - ? Future.failedFuture(new UnauthorizedAccountException( - String.format("Unauthorized account id: %s", accountId), accountId)) - : Future.succeededFuture(Account.empty(accountId)); - } - - private BidRequest enrichBidRequestWithAccountAndPrivacyData( - BidRequest bidRequest, Account account, PrivacyContext privacyContext) { - - final ExtRequest requestExt = bidRequest.getExt(); - final ExtRequest enrichedRequestExt = enrichExtRequest(requestExt, account); - - final Device device = bidRequest.getDevice(); - final Device enrichedDevice = enrichDevice(device, privacyContext); - - if (enrichedRequestExt != null || enrichedDevice != null) { - return bidRequest.toBuilder() - .ext(ObjectUtils.defaultIfNull(enrichedRequestExt, requestExt)) - .device(ObjectUtils.defaultIfNull(enrichedDevice, device)) - .build(); - } - - return bidRequest; - } - - private ExtRequest enrichExtRequest(ExtRequest ext, Account account) { - final ExtRequestPrebid prebidExt = getIfNotNull(ext, ExtRequest::getPrebid); - final String integration = getIfNotNull(prebidExt, ExtRequestPrebid::getIntegration); - final String accountDefaultIntegration = account.getDefaultIntegration(); - - if (StringUtils.isBlank(integration) && StringUtils.isNotBlank(accountDefaultIntegration)) { - final ExtRequestPrebid.ExtRequestPrebidBuilder prebidExtBuilder = - prebidExt != null ? prebidExt.toBuilder() : ExtRequestPrebid.builder(); - - prebidExtBuilder.integration(accountDefaultIntegration); - - return ExtRequest.of(prebidExtBuilder.build()); - } - - return null; - } - - private Device enrichDevice(Device device, PrivacyContext privacyContext) { - final String ipAddress = privacyContext.getIpAddress(); - final String country = getIfNotNull(privacyContext.getTcfContext().getGeoInfo(), GeoInfo::getCountry); - - final String ipAddressInRequest = getIfNotNull(device, Device::getIp); - - final Geo geo = getIfNotNull(device, Device::getGeo); - final String countryFromRequest = getIfNotNull(geo, Geo::getCountry); - - final boolean shouldUpdateIp = ipAddress != null && !Objects.equals(ipAddressInRequest, ipAddress); - final boolean shouldUpdateCountry = country != null && !Objects.equals(countryFromRequest, country); - - if (shouldUpdateIp || shouldUpdateCountry) { - final Device.DeviceBuilder deviceBuilder = device != null ? device.toBuilder() : Device.builder(); - - if (shouldUpdateIp) { - deviceBuilder.ip(ipAddress); - } - - if (shouldUpdateCountry) { - final Geo.GeoBuilder geoBuilder = geo != null ? geo.toBuilder() : Geo.builder(); - geoBuilder.country(country); - deviceBuilder.geo(geoBuilder.build()); - } - - return deviceBuilder.build(); - } - - return null; - } - - private static MetricName requestTypeMetric(BidRequest bidRequest) { - return bidRequest.getApp() != null ? MetricName.openrtb2app : MetricName.openrtb2web; - } -} diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index c95b42f9b20..0cb1204a81b 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -1,7 +1,6 @@ package org.prebid.server.auction; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; @@ -16,6 +15,7 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.Response; import com.iab.openrtb.response.SeatBid; +import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; @@ -24,25 +24,37 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidInfo; import org.prebid.server.auction.model.BidRequestCacheInfo; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.auction.model.BidderResponseInfo; +import org.prebid.server.auction.model.MultiBidConfig; +import org.prebid.server.auction.model.TargetingInfo; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.bidder.model.BidderSeatBidInfo; import org.prebid.server.cache.CacheService; import org.prebid.server.cache.model.CacheContext; -import org.prebid.server.cache.model.CacheIdInfo; +import org.prebid.server.cache.model.CacheInfo; import org.prebid.server.cache.model.CacheServiceResult; import org.prebid.server.cache.model.DebugHttpCall; +import org.prebid.server.deals.model.DeepDebugLog; +import org.prebid.server.deals.model.TxnLog; import org.prebid.server.events.EventsContext; import org.prebid.server.events.EventsService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.identity.IdGenerator; +import org.prebid.server.identity.IdGeneratorType; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; @@ -60,19 +72,25 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; +import org.prebid.server.proto.openrtb.ext.response.ExtDebugPgmetrics; +import org.prebid.server.proto.openrtb.ext.response.ExtDebugTrace; import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall; import org.prebid.server.proto.openrtb.ext.response.ExtResponseCache; import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; +import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.VideoStoredDataResult; +import org.prebid.server.util.LineItemUtil; +import org.prebid.server.vast.VastModifier; import java.math.BigDecimal; import java.time.Clock; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; @@ -81,7 +99,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -89,18 +106,17 @@ public class BidResponseCreator { - private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = - new TypeReference>() { - }; - private static final String CACHE = "cache"; private static final String PREBID_EXT = "prebid"; + private static final Integer DEFAULT_BID_LIMIT_MIN = 1; private final CacheService cacheService; private final BidderCatalog bidderCatalog; + private final VastModifier vastModifier; private final EventsService eventsService; private final StoredRequestProcessor storedRequestProcessor; - private final boolean generateBidId; + private final IdGenerator bidIdGenerator; + private final HookStageExecutor hookStageExecutor; private final int truncateAttrChars; private final Clock clock; private final JacksonMapper mapper; @@ -108,21 +124,28 @@ public class BidResponseCreator { private final String cacheHost; private final String cachePath; private final String cacheAssetUrlTemplate; + private final WinningBidComparatorFactory winningBidComparatorFactory; public BidResponseCreator(CacheService cacheService, BidderCatalog bidderCatalog, + VastModifier vastModifier, EventsService eventsService, StoredRequestProcessor storedRequestProcessor, - boolean generateBidId, + WinningBidComparatorFactory winningBidComparatorFactory, + IdGenerator bidIdGenerator, + HookStageExecutor hookStageExecutor, int truncateAttrChars, Clock clock, JacksonMapper mapper) { this.cacheService = Objects.requireNonNull(cacheService); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + this.vastModifier = Objects.requireNonNull(vastModifier); this.eventsService = Objects.requireNonNull(eventsService); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); - this.generateBidId = generateBidId; + this.winningBidComparatorFactory = Objects.requireNonNull(winningBidComparatorFactory); + this.bidIdGenerator = Objects.requireNonNull(bidIdGenerator); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.truncateAttrChars = validateTruncateAttrChars(truncateAttrChars); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); @@ -132,6 +155,13 @@ public BidResponseCreator(CacheService cacheService, cacheAssetUrlTemplate = Objects.requireNonNull(cacheService.getCachedAssetURLTemplate()); } + private static int validateTruncateAttrChars(int truncateAttrChars) { + if (truncateAttrChars < 0 || truncateAttrChars > 255) { + throw new IllegalArgumentException("truncateAttrChars must be between 0 and 255"); + } + return truncateAttrChars; + } + /** * Creates an OpenRTB {@link BidResponse} from the bids supplied by the bidder, * including processing of winning bids with cache IDs. @@ -139,102 +169,328 @@ public BidResponseCreator(CacheService cacheService, Future create(List bidderResponses, AuctionContext auctionContext, BidRequestCacheInfo cacheInfo, - boolean debugEnabled) { + Map bidderToMultiBids) { + + final List imps = auctionContext.getBidRequest().getImp(); + final EventsContext eventsContext = createEventsContext(auctionContext); + + return videoStoredDataResult(auctionContext) + .compose(videoStoredDataResult -> { + final List modifiedBidderResponses = + updateBids(bidderResponses, videoStoredDataResult, auctionContext, eventsContext, imps); + + return invokeProcessedBidderResponseHooks(modifiedBidderResponses, auctionContext) + .map(updatedBidderResponses -> toBidderResponseInfos(updatedBidderResponses, imps)) + .compose(bidderResponseInfos -> cacheBidsAndCreateResponse( + bidderResponseInfos, + auctionContext, + cacheInfo, + bidderToMultiBids, + videoStoredDataResult, + eventsContext)); + }); + } - final long auctionTimestamp = auctionTimestamp(auctionContext); + private List updateBids(List bidderResponses, + VideoStoredDataResult videoStoredDataResult, + AuctionContext auctionContext, + EventsContext eventsContext, + List imps) { - if (isEmptyBidderResponses(bidderResponses)) { - final BidRequest bidRequest = auctionContext.getBidRequest(); - return Future.succeededFuture(BidResponse.builder() - .id(bidRequest.getId()) - .cur(bidRequest.getCur().get(0)) - .nbr(0) // signal "Unknown Error" - .seatbid(Collections.emptyList()) - .ext(mapper.mapper().valueToTree(toExtBidResponse( - bidderResponses, - auctionContext, - CacheServiceResult.empty(), - VideoStoredDataResult.empty(), - auctionTimestamp, - debugEnabled, - null))) - .build()); + final List result = new ArrayList<>(); + for (final BidderResponse bidderResponse : bidderResponses) { + final String bidder = bidderResponse.getBidder(); + + final List modifiedBidderBids = new ArrayList<>(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + for (final BidderBid bidderBid : seatBid.getBids()) { + final Bid receivedBid = bidderBid.getBid(); + final BidType bidType = bidderBid.getType(); + + final Imp correspondingImp = correspondingImp(receivedBid, imps); + final ExtDealLine extDealLine = LineItemUtil.extDealLineFrom(receivedBid, correspondingImp, mapper); + final String lineItemId = extDealLine != null ? extDealLine.getLineItemId() : null; + + final Bid modifiedBid = updateBid( + receivedBid, bidType, bidder, videoStoredDataResult, auctionContext, eventsContext, lineItemId); + modifiedBidderBids.add(bidderBid.with(modifiedBid)); + } + + final BidderSeatBid modifiedSeatBid = seatBid.with(modifiedBidderBids); + result.add(bidderResponse.with(modifiedSeatBid)); } + return result; + } - return cacheBidsAndCreateResponse( - bidderResponses, - auctionContext, - cacheInfo, - auctionTimestamp, - debugEnabled); + private Bid updateBid(Bid bid, + BidType bidType, + String bidder, + VideoStoredDataResult videoStoredDataResult, + AuctionContext auctionContext, + EventsContext eventsContext, + String lineItemId) { + + final Account account = auctionContext.getAccount(); + final List debugWarnings = auctionContext.getDebugWarnings(); + + final String generatedBidId = bidIdGenerator.getType() != IdGeneratorType.none + ? bidIdGenerator.generateId() + : null; + final String effectiveBidId = ObjectUtils.defaultIfNull(generatedBidId, bid.getId()); + + return bid.toBuilder() + .adm(updateBidAdm(bid, + bidType, + bidder, + account, + eventsContext, + effectiveBidId, + debugWarnings, + lineItemId)) + .ext(updateBidExt( + bid, + bidType, + bidder, + account, + videoStoredDataResult, + eventsContext, + generatedBidId, + effectiveBidId, + lineItemId)) + .build(); } - private static int validateTruncateAttrChars(int truncateAttrChars) { - if (truncateAttrChars < 0 || truncateAttrChars > 255) { - throw new IllegalArgumentException("truncateAttrChars must be between 0 and 255"); + private String updateBidAdm( + Bid bid, + BidType bidType, + String bidder, + Account account, + EventsContext eventsContext, + String effectiveBidId, + List debugWarnings, + String lineItemId) { + + final String bidAdm = bid.getAdm(); + return BidType.video.equals(bidType) + ? vastModifier.createBidVastXml( + bidder, + bidAdm, + bid.getNurl(), + effectiveBidId, + account.getId(), + eventsContext, + debugWarnings, + lineItemId) + : bidAdm; + } + + private ObjectNode updateBidExt(Bid bid, + BidType bidType, + String bidder, + Account account, + VideoStoredDataResult videoStoredDataResult, + EventsContext eventsContext, + String generatedBidId, + String effectiveBidId, + String lineItemId) { + + final ExtBidPrebid updatedExtBidPrebid = updateBidExtPrebid( + bid, + bidType, + bidder, + account, + videoStoredDataResult, + eventsContext, + generatedBidId, + effectiveBidId, + lineItemId); + final ObjectNode existingBidExt = bid.getExt(); + + final ObjectNode updatedBidExt = mapper.mapper().createObjectNode(); + + if (existingBidExt != null && !existingBidExt.isEmpty()) { + updatedBidExt.setAll(existingBidExt); } - return truncateAttrChars; + updatedBidExt.set(PREBID_EXT, mapper.mapper().valueToTree(updatedExtBidPrebid)); + + return updatedBidExt; + } + + private ExtBidPrebid updateBidExtPrebid( + Bid bid, + BidType bidType, + String bidder, + Account account, + VideoStoredDataResult videoStoredDataResult, + EventsContext eventsContext, + String generatedBidId, + String effectiveBidId, + String lineItemId) { + + final Video storedVideo = videoStoredDataResult.getImpIdToStoredVideo().get(bid.getImpid()); + final Events events = createEvents(bidder, account, effectiveBidId, eventsContext, lineItemId); + final ExtBidPrebidVideo extBidPrebidVideo = getExtBidPrebidVideo(bid.getExt()); + + final ExtBidPrebid extBidPrebid = getExtPrebid(bid.getExt()); + final ExtBidPrebid.ExtBidPrebidBuilder extBidPrebidBuilder = extBidPrebid != null + ? extBidPrebid.toBuilder() + : ExtBidPrebid.builder(); + + return extBidPrebidBuilder + .bidid(generatedBidId) + .type(bidType) + .storedRequestAttributes(storedVideo) + .events(events) + .video(extBidPrebidVideo) + .build(); + } + + private JsonNode getAndRemoveProperty(String propertyName, ObjectNode node) { + final JsonNode property = node.get(propertyName); + node.remove(propertyName); + + return property; } /** * Checks whether bidder responses are empty or contain no bids. */ - private static boolean isEmptyBidderResponses(List bidderResponses) { - return bidderResponses.isEmpty() || bidderResponses.stream() - .map(bidderResponse -> bidderResponse.getSeatBid().getBids()) + private static boolean isEmptyBidderResponses(List bidderResponseInfos) { + return bidderResponseInfos.isEmpty() || bidderResponseInfos.stream() + .map(bidderResponseInfo -> bidderResponseInfo.getSeatBid().getBidsInfos()) .allMatch(CollectionUtils::isEmpty); } - private Future cacheBidsAndCreateResponse(List bidderResponses, + private List toBidderResponseInfos(List bidderResponses, List imps) { + final List result = new ArrayList<>(); + for (final BidderResponse bidderResponse : bidderResponses) { + final String bidder = bidderResponse.getBidder(); + + final List bidInfos = new ArrayList<>(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + + for (final BidderBid bidderBid : seatBid.getBids()) { + final Bid bid = bidderBid.getBid(); + final BidType type = bidderBid.getType(); + final BidInfo bidInfo = toBidInfo(bid, type, imps, bidder); + bidInfos.add(bidInfo); + } + + final BidderSeatBidInfo bidderSeatBidInfo = BidderSeatBidInfo.of( + bidInfos, + seatBid.getHttpCalls(), + seatBid.getErrors()); + + result.add(BidderResponseInfo.of(bidder, bidderSeatBidInfo, bidderResponse.getResponseTime())); + } + + return result; + } + + private BidInfo toBidInfo(Bid bid, BidType type, List imps, String bidder) { + final Imp correspondingImp = correspondingImp(bid, imps); + final ExtDealLine extDealLine = LineItemUtil.extDealLineFrom(bid, correspondingImp, mapper); + final String lineItemId = extDealLine != null ? extDealLine.getLineItemId() : null; + return BidInfo.builder() + .bid(bid) + .bidType(type) + .bidder(bidder) + .correspondingImp(correspondingImp) + .lineItemId(lineItemId) + .build(); + } + + private static Imp correspondingImp(Bid bid, List imps) { + final String impId = bid.getImpid(); + return imps.stream() + .filter(imp -> Objects.equals(impId, imp.getId())) + .findFirst() + // Should never occur. See ResponseBidValidator + .orElseThrow( + () -> new PreBidException(String.format("Bid with impId %s doesn't have matched imp", impId))); + } + + private Future> invokeProcessedBidderResponseHooks(List bidderResponses, + AuctionContext auctionContext) { + + return CompositeFuture.join(bidderResponses.stream() + .map(bidderResponse -> hookStageExecutor + .executeProcessedBidderResponseStage(bidderResponse, auctionContext) + .map(stageResult -> rejectBidderResponseOrProceed(stageResult, bidderResponse))) + .collect(Collectors.toList())) + .map(CompositeFuture::list); + } + + private static BidderResponse rejectBidderResponseOrProceed( + HookStageExecutionResult stageResult, + BidderResponse bidderResponse) { + + final List bids = + stageResult.isShouldReject() ? Collections.emptyList() : stageResult.getPayload().bids(); + + return bidderResponse + .with(bidderResponse.getSeatBid() + .with(bids)); + } + + private Future cacheBidsAndCreateResponse(List bidderResponses, AuctionContext auctionContext, BidRequestCacheInfo cacheInfo, - long auctionTimestamp, - boolean debugEnabled) { + Map bidderToMultiBids, + VideoStoredDataResult videoStoredDataResult, + EventsContext eventsContext) { final BidRequest bidRequest = auctionContext.getBidRequest(); + if (isEmptyBidderResponses(bidderResponses)) { + return Future.succeededFuture(BidResponse.builder() + .id(bidRequest.getId()) + .cur(bidRequest.getCur().get(0)) + .nbr(0) // signal "Unknown Error" + .seatbid(Collections.emptyList()) + .ext(toExtBidResponse( + bidderResponses, + auctionContext, + CacheServiceResult.empty(), + VideoStoredDataResult.empty(), + eventsContext.getAuctionTimestamp(), + null)) + .build()); + } - bidderResponses.forEach(BidResponseCreator::removeRedundantBids); + final ExtRequestTargeting targeting = targeting(bidRequest); + final TxnLog txnLog = auctionContext.getTxnLog(); - ExtRequestTargeting targeting = targeting(bidRequest); - final Set winningBids = newOrEmptySet(targeting); - final Set winningBidsByBidder = newOrEmptySet(targeting); + final List bidderResponseInfos = + toBidderResponseWithTargetingBidInfos(bidderResponses, bidderToMultiBids, preferDeals(targeting), + txnLog); - // determine winning bids only if targeting is present - if (targeting != null) { - populateWinningBids(bidderResponses, winningBids, winningBidsByBidder); - } + final Set bidInfos = bidderResponseInfos.stream() + .map(BidderResponseInfo::getSeatBid) + .map(BidderSeatBidInfo::getBidsInfos) + .filter(CollectionUtils::isNotEmpty) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); - final Set bidsToCache = cacheInfo.isShouldCacheWinningBidsOnly() - ? winningBids - : bidderResponses.stream().flatMap(BidResponseCreator::getBids).collect(Collectors.toSet()); + final Set winningBidInfos = targeting == null + ? null + : bidInfos.stream() + .filter(bidInfo -> bidInfo.getTargetingInfo().isWinningBid()) + .collect(Collectors.toSet()); - final EventsContext eventsContext = EventsContext.builder() - .enabledForAccount(eventsEnabledForAccount(auctionContext)) - .enabledForRequest(eventsEnabledForRequest(auctionContext)) - .auctionTimestamp(auctionTimestamp) - .integration(integrationFrom(auctionContext)) - .build(); + updateSentToClientTxnLog(txnLog, bidInfos); - return toBidsWithCacheIds( - bidderResponses, - bidsToCache, - auctionContext, - cacheInfo, - eventsContext) - .compose(cacheResult -> videoStoredDataResult(bidRequest.getImp(), auctionContext.getTimeout()) - .map(videoStoredDataResult -> toBidResponse( - bidderResponses, - auctionContext, - targeting, - winningBids, - winningBidsByBidder, - cacheInfo, - cacheResult, - videoStoredDataResult, - eventsContext, - auctionTimestamp, - debugEnabled))); + final Set bidsToCache = cacheInfo.isShouldCacheWinningBidsOnly() ? winningBidInfos : bidInfos; + + return cacheBids(bidsToCache, auctionContext, cacheInfo, eventsContext) + .map(cacheResult -> toBidResponse( + bidderResponseInfos, + auctionContext, + targeting, + cacheInfo, + cacheResult, + videoStoredDataResult, + eventsContext)); } private static ExtRequestTargeting targeting(BidRequest bidRequest) { @@ -243,247 +499,305 @@ private static ExtRequestTargeting targeting(BidRequest bidRequest) { return prebid != null ? prebid.getTargeting() : null; } - /** - * Extracts auction timestamp from {@link ExtRequest} or get it from {@link Clock} if it is null. - */ - private long auctionTimestamp(AuctionContext auctionContext) { - final ExtRequest requestExt = auctionContext.getBidRequest().getExt(); - final ExtRequestPrebid prebid = requestExt != null ? requestExt.getPrebid() : null; - final Long auctionTimestamp = prebid != null ? prebid.getAuctiontimestamp() : null; - return auctionTimestamp != null ? auctionTimestamp : clock.millis(); + private static boolean preferDeals(ExtRequestTargeting targeting) { + return BooleanUtils.toBooleanDefaultIfNull(targeting != null ? targeting.getPreferdeals() : null, false); } - /** - * Returns {@link ExtBidResponse} object, populated with response time, errors and debug info (if requested) - * from all bidders. - */ - private ExtBidResponse toExtBidResponse(List bidderResponses, - AuctionContext auctionContext, - CacheServiceResult cacheResult, - VideoStoredDataResult videoStoredDataResult, - long auctionTimestamp, - boolean debugEnabled, - Map> bidErrors) { + private List toBidderResponseWithTargetingBidInfos( + List bidderResponses, + Map bidderToMultiBids, + boolean preferDeals, + TxnLog txnLog) { - final BidRequest bidRequest = auctionContext.getBidRequest(); + final Map> bidderResponseToReducedBidInfos = bidderResponses.stream() + .collect(Collectors.toMap( + Function.identity(), + bidderResponse -> toSortedMultiBidInfo(bidderResponse, bidderToMultiBids, preferDeals))); - final ExtResponseDebug extResponseDebug = debugEnabled - ? ExtResponseDebug.of(toExtHttpCalls(bidderResponses, cacheResult), bidRequest) - : null; - final Map> errors = - toExtBidderErrors(bidderResponses, auctionContext, cacheResult, videoStoredDataResult, bidErrors); - final Map responseTimeMillis = toResponseTimes(bidderResponses, cacheResult); + final Map>> impIdToBidderToBidInfos = bidderResponseToReducedBidInfos.values() + .stream() + .flatMap(Collection::stream) + .collect(Collectors.groupingBy( + bidInfo -> bidInfo.getCorrespondingImp().getId(), + Collectors.groupingBy(BidInfo::getBidder))); - return ExtBidResponse.of(extResponseDebug, errors, responseTimeMillis, bidRequest.getTmax(), null, - ExtBidResponsePrebid.of(auctionTimestamp)); - } + // Best bids from bidders for imp + final Set winningBids = new HashSet<>(); + // All bids from bidder for imp + final Set winningBidsByBidder = new HashSet<>(); - private static void removeRedundantBids(BidderResponse bidderResponse) { - final List responseBidderBids = bidderResponse.getSeatBid().getBids(); - final Map> impIdToBidderBid = responseBidderBids.stream() - .collect(Collectors.groupingBy(bidderBid -> bidderBid.getBid().getImpid())); + for (final Map> bidderToBidInfos : impIdToBidderToBidInfos.values()) { - final List mostValuableBids = impIdToBidderBid.values().stream() - .map(BidResponseCreator::mostValuableBid) - .collect(Collectors.toList()); + bidderToBidInfos.values().forEach(winningBidsByBidder::addAll); + + bidderToBidInfos.values().stream() + .flatMap(Collection::stream) + .max(winningBidComparatorFactory.create(preferDeals)) + .ifPresent(winningBids::add); + } - responseBidderBids.retainAll(mostValuableBids); + final Map> impIdToLineItemIds = impIdToBidderToBidInfos.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + impIdToBidderToBidInfoEntry -> toLineItemIds(impIdToBidderToBidInfoEntry.getValue().values()))); + + updateTopMatchAndLostAuctionLineItemsMetric(winningBids, txnLog, impIdToLineItemIds); + + return bidderResponseToReducedBidInfos.entrySet().stream() + .map(responseToBidInfos -> injectBidInfoWithTargeting( + responseToBidInfos.getKey(), + responseToBidInfos.getValue(), + bidderToMultiBids, + winningBids, + winningBidsByBidder)) + .collect(Collectors.toList()); } - private static BidderBid mostValuableBid(List bidderBids) { - if (bidderBids.size() == 1) { - return bidderBids.get(0); - } + private List toSortedMultiBidInfo(BidderResponseInfo bidderResponse, + Map bidderToMultiBids, + boolean preferDeals) { - final List dealBidderBids = bidderBids.stream() - .filter(bidderBid -> StringUtils.isNotBlank(bidderBid.getBid().getDealid())) + final List bidInfos = bidderResponse.getSeatBid().getBidsInfos(); + final Map> impIdToBidInfos = bidInfos.stream() + .collect(Collectors.groupingBy(bidInfo -> bidInfo.getCorrespondingImp().getId())); + + final MultiBidConfig multiBid = bidderToMultiBids.get(bidderResponse.getBidder()); + final Integer bidLimit = multiBid != null ? multiBid.getMaxBids() : DEFAULT_BID_LIMIT_MIN; + + return impIdToBidInfos.values().stream() + .map(infos -> sortReducedBidInfo(infos, bidLimit, preferDeals)) + .flatMap(Collection::stream) .collect(Collectors.toList()); + } - List processedBidderBids = dealBidderBids.isEmpty() ? bidderBids : dealBidderBids; + private List sortReducedBidInfo(List bidInfos, int limit, boolean preferDeals) { + return bidInfos.stream() + .sorted(winningBidComparatorFactory.create(preferDeals).reversed()) + .limit(limit) + .collect(Collectors.toList()); + } - return processedBidderBids.stream() - .max(Comparator.comparing(bidderBid -> bidderBid.getBid().getPrice(), Comparator.naturalOrder())) - .orElse(bidderBids.get(0)); + private static Set toLineItemIds(Collection> bidInfos) { + return bidInfos.stream() + .flatMap(Collection::stream) + .map(BidInfo::getLineItemId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); } /** - * Returns new {@link HashSet} in case of existing keywordsCreator or empty collection if null. + * Updates sent to client as top match and auction lost to line item metric. */ - private static Set newOrEmptySet(ExtRequestTargeting targeting) { - return targeting != null ? new HashSet<>() : Collections.emptySet(); + private static void updateTopMatchAndLostAuctionLineItemsMetric(Set winningBidInfos, + TxnLog txnLog, + Map> impToLineItemIds) { + for (BidInfo winningBidInfo : winningBidInfos) { + final String winningLineItemId = winningBidInfo.getLineItemId(); + if (winningLineItemId != null) { + txnLog.lineItemSentToClientAsTopMatch().add(winningLineItemId); + + final String impIdOfWinningBid = winningBidInfo.getBid().getImpid(); + impToLineItemIds.get(impIdOfWinningBid).stream() + .filter(lineItemId -> !Objects.equals(lineItemId, winningLineItemId)) + .forEach(lineItemId -> txnLog.lostAuctionToLineItems().get(lineItemId).add(winningLineItemId)); + } + } } - /** - * Populates 2 input sets: - *

- * - winning bids for each impId (ad unit code) through all bidder responses. - *
- * - winning bids for each impId but for separate bidder. - *

- * Winning bid is the one with the highest price. - */ - private static void populateWinningBids(List bidderResponses, Set winningBids, - Set winningBidsByBidder) { - final Map winningBidsMap = new HashMap<>(); // impId -> Bid - final Map> winningBidsByBidderMap = new HashMap<>(); // impId -> [bidder -> Bid] + private BidderResponseInfo injectBidInfoWithTargeting(BidderResponseInfo bidderResponseInfo, + List bidderBidInfos, + Map bidderToMultiBids, + Set winningBids, + Set winningBidsByBidder) { - for (BidderResponse bidderResponse : bidderResponses) { - final String bidder = bidderResponse.getBidder(); + final String bidder = bidderResponseInfo.getBidder(); + final List bidInfosWithTargeting = toBidInfoWithTargeting(bidderBidInfos, bidder, bidderToMultiBids, + winningBids, winningBidsByBidder); - for (BidderBid bidderBid : bidderResponse.getSeatBid().getBids()) { - final Bid bid = bidderBid.getBid(); + final BidderSeatBidInfo seatBid = bidderResponseInfo.getSeatBid(); + final BidderSeatBidInfo modifiedSeatBid = seatBid.with(bidInfosWithTargeting); + return bidderResponseInfo.with(modifiedSeatBid); + } - tryAddWinningBid(bid, winningBidsMap); - tryAddWinningBidByBidder(bid, bidder, winningBidsByBidderMap); - } - } + private List toBidInfoWithTargeting(List bidderBidInfos, + String bidder, + Map bidderToMultiBids, + Set winningBids, + Set winningBidsByBidder) { - winningBids.addAll(winningBidsMap.values()); + final Map> impIdToBidInfos = bidderBidInfos.stream() + .collect(Collectors.groupingBy(bidInfo -> bidInfo.getCorrespondingImp().getId())); - final List bidsByBidder = winningBidsByBidderMap.values().stream() - .flatMap(bidsByBidderMap -> bidsByBidderMap.values().stream()) + return impIdToBidInfos.values().stream() + .map(bidInfos -> injectTargeting(bidInfos, bidder, bidderToMultiBids, winningBids, winningBidsByBidder)) + .flatMap(Collection::stream) .collect(Collectors.toList()); - winningBidsByBidder.addAll(bidsByBidder); } - /** - * Tries to add a winning bid for each impId. - */ - private static void tryAddWinningBid(Bid bid, Map winningBids) { - final String impId = bid.getImpid(); - - if (!winningBids.containsKey(impId) || bid.getPrice().compareTo(winningBids.get(impId).getPrice()) > 0) { - winningBids.put(impId, bid); + private List injectTargeting(List bidderImpIdBidInfos, + String bidder, + Map bidderToMultiBids, + Set winningBids, + Set winningBidsByBidder) { + + final List result = new ArrayList<>(); + + final MultiBidConfig multiBid = bidderToMultiBids.get(bidder); + final String bidderCodePrefix = multiBid != null ? multiBid.getTargetBidderCodePrefix() : null; + + final int multiBidSize = bidderImpIdBidInfos.size(); + for (int i = 0; i < multiBidSize; i++) { + // first bid have highest value and can't be extra bid. + final boolean isFirstBid = i == 0; + final String targetingBidderCode = isFirstBid + ? bidder + : bidderCodePrefix == null ? null : String.format("%s%s", bidderCodePrefix, i + 1); + + final BidInfo bidInfo = bidderImpIdBidInfos.get(i); + final TargetingInfo targetingInfo = TargetingInfo.builder() + .isTargetingEnabled(targetingBidderCode != null) + .isBidderWinningBid(winningBidsByBidder.contains(bidInfo)) + .isWinningBid(winningBids.contains(bidInfo)) + .isAddTargetBidderCode(targetingBidderCode != null && multiBidSize > 1) + .bidderCode(targetingBidderCode) + .build(); + + final BidInfo modifiedBidInfo = bidInfo.toBuilder().targetingInfo(targetingInfo).build(); + result.add(modifiedBidInfo); } + + return result; } /** - * Tries to add a winning bid for each impId for separate bidder. + * Increments sent to client metrics for each bid with deal. */ - private static void tryAddWinningBidByBidder(Bid bid, String bidder, - Map> winningBidsByBidder) { - final String impId = bid.getImpid(); + private void updateSentToClientTxnLog(TxnLog txnLog, Set bidInfos) { + bidInfos.stream() + .map(BidInfo::getLineItemId) + .filter(Objects::nonNull) + .forEach(lineItemId -> txnLog.lineItemsSentToClient().add(lineItemId)); + } - if (!winningBidsByBidder.containsKey(impId)) { - final Map bidsByBidder = new HashMap<>(); - bidsByBidder.put(bidder, bid); + /** + * Returns {@link ExtBidResponse} object, populated with response time, errors and debug info (if requested) + * from all bidders. + */ + private ExtBidResponse toExtBidResponse(List bidderResponseInfos, + AuctionContext auctionContext, + CacheServiceResult cacheResult, + VideoStoredDataResult videoStoredDataResult, + long auctionTimestamp, + Map> bidErrors) { - winningBidsByBidder.put(impId, bidsByBidder); - } else { - final Map bidsByBidder = winningBidsByBidder.get(impId); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled(); - if (!bidsByBidder.containsKey(bidder) - || bid.getPrice().compareTo(bidsByBidder.get(bidder).getPrice()) > 0) { - bidsByBidder.put(bidder, bid); - } - } + final ExtResponseDebug extResponseDebug = + toExtResponseDebug(bidderResponseInfos, auctionContext, cacheResult, debugEnabled); + final Map> errors = + toExtBidderErrors(bidderResponseInfos, auctionContext, cacheResult, videoStoredDataResult, bidErrors); + final Map> warnings = debugEnabled + ? toExtBidderWarnings(auctionContext) + : null; + final Map responseTimeMillis = toResponseTimes(bidderResponseInfos, cacheResult); + + return ExtBidResponse.builder() + .debug(extResponseDebug) + .errors(errors) + .warnings(warnings) + .responsetimemillis(responseTimeMillis) + .tmaxrequest(bidRequest.getTmax()) + .prebid(ExtBidResponsePrebid.of(auctionTimestamp, null)) + .build(); } - private static Stream getBids(BidderResponse bidderResponse) { - return Stream.of(bidderResponse) - .map(BidderResponse::getSeatBid) - .filter(Objects::nonNull) - .map(BidderSeatBid::getBids) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .map(BidderBid::getBid); + private ExtResponseDebug toExtResponseDebug(List bidderResponseInfos, + AuctionContext auctionContext, + CacheServiceResult cacheResult, + boolean debugEnabled) { + final DeepDebugLog deepDebugLog = auctionContext.getDeepDebugLog(); + + final Map> httpCalls = debugEnabled + ? toExtHttpCalls(bidderResponseInfos, cacheResult, auctionContext.getDebugHttpCalls()) + : null; + final BidRequest bidRequest = debugEnabled ? auctionContext.getBidRequest() : null; + final ExtDebugPgmetrics extDebugPgmetrics = debugEnabled ? toExtDebugPgmetrics( + auctionContext.getTxnLog()) : null; + final ExtDebugTrace extDebugTrace = deepDebugLog.isDeepDebugEnabled() ? toExtDebugTrace(deepDebugLog) : null; + + return httpCalls == null && bidRequest == null && extDebugPgmetrics == null && extDebugTrace == null + ? null + : ExtResponseDebug.of(httpCalls, bidRequest, extDebugPgmetrics, extDebugTrace); } /** * Corresponds cacheId (or null if not present) to each {@link Bid}. */ - private Future toBidsWithCacheIds(List bidderResponses, - Set bidsToCache, - AuctionContext auctionContext, - BidRequestCacheInfo cacheInfo, - EventsContext eventsContext) { - + private Future cacheBids(Set bidsToCache, + AuctionContext auctionContext, + BidRequestCacheInfo cacheInfo, + EventsContext eventsContext) { if (!cacheInfo.isDoCaching()) { return Future.succeededFuture(CacheServiceResult.of(null, null, toMapBidsWithEmptyCacheIds(bidsToCache))); } // do not submit non deals bids with zero price to prebid cache - final List bidsValidToBeCached = bidsToCache.stream() + final List bidsValidToBeCached = bidsToCache.stream() .filter(BidResponseCreator::isValidForCaching) .collect(Collectors.toList()); - final boolean shouldCacheVideoBids = cacheInfo.isShouldCacheVideoBids(); - - final Map> bidderToVideoBidIdsToModify = - shouldCacheVideoBids && eventsEnabledForAccount(auctionContext) - ? getBidderAndVideoBidIdsToModify(bidderResponses, auctionContext.getBidRequest().getImp()) - : Collections.emptyMap(); - final Map> bidderToBidIds = bidderResponses.stream() - .collect(Collectors.toMap(BidderResponse::getBidder, bidderResponse -> getBids(bidderResponse) - .map(Bid::getId) - .collect(Collectors.toList()))); - final CacheContext cacheContext = CacheContext.builder() .cacheBidsTtl(cacheInfo.getCacheBidsTtl()) .cacheVideoBidsTtl(cacheInfo.getCacheVideoBidsTtl()) .shouldCacheBids(cacheInfo.isShouldCacheBids()) - .shouldCacheVideoBids(shouldCacheVideoBids) - .bidderToVideoBidIdsToModify(bidderToVideoBidIdsToModify) - .bidderToBidIds(bidderToBidIds) + .shouldCacheVideoBids(cacheInfo.isShouldCacheVideoBids()) .build(); return cacheService.cacheBidsOpenrtb(bidsValidToBeCached, auctionContext, cacheContext, eventsContext) .map(cacheResult -> addNotCachedBids(cacheResult, bidsToCache)); } - private static boolean isValidForCaching(Bid bid) { + private static boolean isValidForCaching(BidInfo bidInfo) { + final Bid bid = bidInfo.getBid(); final BigDecimal price = bid.getPrice(); return bid.getDealid() != null ? price.compareTo(BigDecimal.ZERO) >= 0 : price.compareTo(BigDecimal.ZERO) > 0; } - private Map> getBidderAndVideoBidIdsToModify(List bidderResponses, - List imps) { - - return bidderResponses.stream() - .filter(bidderResponse -> bidderCatalog.isModifyingVastXmlAllowed(bidderResponse.getBidder())) - .collect(Collectors.toMap(BidderResponse::getBidder, bidderResponse -> getBids(bidderResponse) - .filter(bid -> isVideoBid(bid, imps)) - .map(Bid::getId) - .collect(Collectors.toList()))); - } - - private static boolean isVideoBid(Bid bid, List imps) { - return imps.stream() - .filter(imp -> imp.getVideo() != null) - .map(Imp::getId) - .anyMatch(impId -> bid.getImpid().equals(impId)); - } - /** * Creates a map with {@link Bid} as a key and null as a value. */ - private static Map toMapBidsWithEmptyCacheIds(Set bids) { + private static Map toMapBidsWithEmptyCacheIds(Set bids) { return bids.stream() - .collect(Collectors.toMap(Function.identity(), ignored -> CacheIdInfo.empty())); + .map(BidInfo::getBid) + .collect(Collectors.toMap(Function.identity(), ignored -> CacheInfo.empty())); } /** * Adds bids with no cache id info. */ - private static CacheServiceResult addNotCachedBids(CacheServiceResult cacheResult, Set bids) { - final Map bidToCacheIdInfo = cacheResult.getCacheBids(); - - if (bids.size() > bidToCacheIdInfo.size()) { - final Map updatedBidToCacheIdInfo = new HashMap<>(bidToCacheIdInfo); - for (Bid bid : bids) { - if (!updatedBidToCacheIdInfo.containsKey(bid)) { - updatedBidToCacheIdInfo.put(bid, CacheIdInfo.empty()); + private static CacheServiceResult addNotCachedBids(CacheServiceResult cacheResult, Set bidInfos) { + final Map bidToCacheId = cacheResult.getCacheBids(); + + if (bidInfos.size() > bidToCacheId.size()) { + final Map updatedBidToCacheInfo = new HashMap<>(bidToCacheId); + for (BidInfo bidInfo : bidInfos) { + final Bid bid = bidInfo.getBid(); + if (!updatedBidToCacheInfo.containsKey(bid)) { + updatedBidToCacheInfo.put(bid, CacheInfo.empty()); } } - return CacheServiceResult.of(cacheResult.getHttpCall(), cacheResult.getError(), updatedBidToCacheIdInfo); + return CacheServiceResult.of(cacheResult.getHttpCall(), cacheResult.getError(), updatedBidToCacheInfo); } return cacheResult; } - private static Map> toExtHttpCalls(List bidderResponses, - CacheServiceResult cacheResult) { + private static Map> toExtHttpCalls(List bidderResponses, + CacheServiceResult cacheResult, + Map> contextHttpCalls) { final Map> bidderHttpCalls = bidderResponses.stream() - .collect(Collectors.toMap(BidderResponse::getBidder, + .collect(Collectors.toMap( + BidderResponseInfo::getBidder, bidderResponse -> ListUtils.emptyIfNull(bidderResponse.getSeatBid().getHttpCalls()))); final DebugHttpCall httpCall = cacheResult.getHttpCall(); @@ -492,9 +806,15 @@ private static Map> toExtHttpCalls(List> contextExtHttpCalls = contextHttpCalls.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, serviceToHttpCall -> serviceToHttpCall.getValue().stream() + .map(BidResponseCreator::toExtHttpCall) + .collect(Collectors.toList()))); + final Map> httpCalls = new HashMap<>(); httpCalls.putAll(bidderHttpCalls); httpCalls.putAll(cacheHttpCalls); + httpCalls.putAll(contextExtHttpCalls); return httpCalls.isEmpty() ? null : httpCalls; } @@ -504,20 +824,53 @@ private static ExtHttpCall toExtHttpCall(DebugHttpCall debugHttpCall) { .requestbody(debugHttpCall.getRequestBody()) .status(debugHttpCall.getResponseStatus()) .responsebody(debugHttpCall.getResponseBody()) + .requestheaders(debugHttpCall.getRequestHeaders()) .build(); } - private Map> toExtBidderErrors(List bidderResponses, + private static ExtDebugPgmetrics toExtDebugPgmetrics(TxnLog txnLog) { + final ExtDebugPgmetrics extDebugPgmetrics = ExtDebugPgmetrics.builder() + .matchedDomainTargeting(nullIfEmpty(txnLog.lineItemsMatchedDomainTargeting())) + .matchedWholeTargeting(nullIfEmpty(txnLog.lineItemsMatchedWholeTargeting())) + .matchedTargetingFcapped(nullIfEmpty(txnLog.lineItemsMatchedTargetingFcapped())) + .matchedTargetingFcapLookupFailed(nullIfEmpty(txnLog.lineItemsMatchedTargetingFcapLookupFailed())) + .readyToServe(nullIfEmpty(txnLog.lineItemsReadyToServe())) + .pacingDeferred(nullIfEmpty(txnLog.lineItemsPacingDeferred())) + .sentToBidder(nullIfEmpty(txnLog.lineItemsSentToBidder())) + .sentToBidderAsTopMatch(nullIfEmpty(txnLog.lineItemsSentToBidderAsTopMatch())) + .receivedFromBidder(nullIfEmpty(txnLog.lineItemsReceivedFromBidder())) + .responseInvalidated(nullIfEmpty(txnLog.lineItemsResponseInvalidated())) + .sentToClient(nullIfEmpty(txnLog.lineItemsSentToClient())) + .sentToClientAsTopMatch(nullIfEmpty(txnLog.lineItemSentToClientAsTopMatch())) + .build(); + return extDebugPgmetrics.equals(ExtDebugPgmetrics.EMPTY) ? null : extDebugPgmetrics; + } + + private static ExtDebugTrace toExtDebugTrace(DeepDebugLog deepDebugLog) { + final List entries = deepDebugLog.entries(); + final List dealsTrace = entries.stream() + .filter(extTraceDeal -> StringUtils.isEmpty(extTraceDeal.getLineItemId())) + .collect(Collectors.toList()); + final Map> lineItemsTrace = entries.stream() + .filter(extTraceDeal -> StringUtils.isNotEmpty(extTraceDeal.getLineItemId())) + .collect(Collectors.groupingBy(ExtTraceDeal::getLineItemId, Collectors.toList())); + return CollectionUtils.isNotEmpty(entries) + ? ExtDebugTrace.of(CollectionUtils.isEmpty(dealsTrace) ? null : dealsTrace, + MapUtils.isEmpty(lineItemsTrace) ? null : lineItemsTrace) + : null; + } + + private Map> toExtBidderErrors(List bidderResponses, AuctionContext auctionContext, CacheServiceResult cacheResult, VideoStoredDataResult videoStoredDataResult, Map> bidErrors) { - final BidRequest bidRequest = auctionContext.getBidRequest(); final Map> errors = new HashMap<>(); errors.putAll(extractBidderErrors(bidderResponses)); - errors.putAll(extractDeprecatedBiddersErrors(bidRequest)); - errors.putAll(extractPrebidErrors(cacheResult, videoStoredDataResult, auctionContext)); + errors.putAll(extractDeprecatedBiddersErrors(auctionContext.getBidRequest())); + errors.putAll(extractPrebidErrors(videoStoredDataResult, auctionContext)); + errors.putAll(extractCacheErrors(cacheResult)); if (MapUtils.isNotEmpty(bidErrors)) { addBidErrors(errors, bidErrors); } @@ -528,10 +881,12 @@ private Map> toExtBidderErrors(List /** * Returns a map with bidder name as a key and list of {@link ExtBidderError}s as a value. */ - private static Map> extractBidderErrors(List bidderResponses) { + private static Map> extractBidderErrors( + Collection bidderResponses) { + return bidderResponses.stream() .filter(bidderResponse -> CollectionUtils.isNotEmpty(bidderResponse.getSeatBid().getErrors())) - .collect(Collectors.toMap(BidderResponse::getBidder, + .collect(Collectors.toMap(BidderResponseInfo::getBidder, bidderResponse -> errorsDetails(bidderResponse.getSeatBid().getErrors()))); } @@ -559,37 +914,21 @@ private Map> extractDeprecatedBiddersErrors(BidRequ } /** - * Returns a singleton map with "prebid" as a key and list of {@link ExtBidderError}s cache errors as a value. + * Returns a singleton map with "prebid" as a key and list of {@link ExtBidderError}s errors as a value. */ - private static Map> extractPrebidErrors(CacheServiceResult cacheResult, - VideoStoredDataResult videoStoredDataResult, + private static Map> extractPrebidErrors(VideoStoredDataResult videoStoredDataResult, AuctionContext auctionContext) { - final List cacheErrors = extractCacheErrors(cacheResult); final List storedErrors = extractStoredErrors(videoStoredDataResult); final List contextErrors = extractContextErrors(auctionContext); - if (cacheErrors.isEmpty() && storedErrors.isEmpty() && contextErrors.isEmpty()) { + if (storedErrors.isEmpty() && contextErrors.isEmpty()) { return Collections.emptyMap(); } - final List collectedErrors = Stream.concat(contextErrors.stream(), - Stream.concat(storedErrors.stream(), cacheErrors.stream())) + final List collectedErrors = Stream.concat(contextErrors.stream(), storedErrors.stream()) .collect(Collectors.toList()); return Collections.singletonMap(PREBID_EXT, collectedErrors); } - /** - * Returns a list of {@link ExtBidderError}s of cache errors. - */ - private static List extractCacheErrors(CacheServiceResult cacheResult) { - final Throwable error = cacheResult.getError(); - if (error != null) { - final ExtBidderError extBidderError = ExtBidderError.of(BidderError.Type.generic.getCode(), - error.getMessage()); - return Collections.singletonList(extBidderError); - } - return Collections.emptyList(); - } - /** * Returns a list of {@link ExtBidderError}s of stored request errors. */ @@ -612,6 +951,19 @@ private static List extractContextErrors(AuctionContext auctionC .collect(Collectors.toList()); } + /** + * Returns a singleton map with "cache" as a key and list of {@link ExtBidderError}s cache errors as a value. + */ + private static Map> extractCacheErrors(CacheServiceResult cacheResult) { + final Throwable error = cacheResult.getError(); + if (error != null) { + final ExtBidderError extBidderError = ExtBidderError.of(BidderError.Type.generic.getCode(), + error.getMessage()); + return Collections.singletonMap(CACHE, Collections.singletonList(extBidderError)); + } + return Collections.emptyMap(); + } + /** * Adds bid errors: if value by key exists - add errors to its list, otherwise - add an entry. */ @@ -627,6 +979,22 @@ private static void addBidErrors(Map> errors, } } + private Map> toExtBidderWarnings(AuctionContext auctionContext) { + final Map> warnings = new HashMap<>(extractContextWarnings(auctionContext)); + + return warnings.isEmpty() ? null : warnings; + } + + private static Map> extractContextWarnings(AuctionContext auctionContext) { + final List contextWarnings = auctionContext.getDebugWarnings().stream() + .map(message -> ExtBidderError.of(BidderError.Type.generic.getCode(), message)) + .collect(Collectors.toList()); + + return contextWarnings.isEmpty() + ? Collections.emptyMap() + : Collections.singletonMap(PREBID_EXT, contextWarnings); + } + private static Stream asStream(Iterator iterator) { final Iterable iterable = () -> iterator; return StreamSupport.stream(iterable.spliterator(), false); @@ -635,10 +1003,10 @@ private static Stream asStream(Iterator iterator) { /** * Returns a map with response time by bidders and cache. */ - private static Map toResponseTimes(List bidderResponses, + private static Map toResponseTimes(Collection bidderResponses, CacheServiceResult cacheResult) { final Map responseTimeMillis = bidderResponses.stream() - .collect(Collectors.toMap(BidderResponse::getBidder, BidderResponse::getResponseTime)); + .collect(Collectors.toMap(BidderResponseInfo::getBidder, BidderResponseInfo::getResponseTime)); final DebugHttpCall debugHttpCall = cacheResult.getHttpCall(); final Integer cacheResponseTime = debugHttpCall != null ? debugHttpCall.getResponseTimeMillis() : null; @@ -651,58 +1019,57 @@ private static Map toResponseTimes(List bidderR /** * Returns {@link BidResponse} based on list of {@link BidderResponse}s and {@link CacheServiceResult}. */ - private BidResponse toBidResponse(List bidderResponses, + private BidResponse toBidResponse(List bidderResponseInfos, AuctionContext auctionContext, ExtRequestTargeting targeting, - Set winningBids, - Set winningBidsByBidder, - BidRequestCacheInfo cacheInfo, + BidRequestCacheInfo requestCacheInfo, CacheServiceResult cacheResult, VideoStoredDataResult videoStoredDataResult, - EventsContext eventsContext, - long auctionTimestamp, - boolean debugEnabled) { + EventsContext eventsContext) { final BidRequest bidRequest = auctionContext.getBidRequest(); final Account account = auctionContext.getAccount(); final Map> bidErrors = new HashMap<>(); - final List seatBids = bidderResponses.stream() - .filter(bidderResponse -> !bidderResponse.getSeatBid().getBids().isEmpty()) - .map(bidderResponse -> toSeatBid( - bidderResponse, + final List seatBids = bidderResponseInfos.stream() + .map(BidderResponseInfo::getSeatBid) + .map(BidderSeatBidInfo::getBidsInfos) + .filter(CollectionUtils::isNotEmpty) + .map(bidInfos -> toSeatBid( + bidInfos, targeting, bidRequest, - winningBids, - winningBidsByBidder, - cacheInfo, + requestCacheInfo, cacheResult.getCacheBids(), - videoStoredDataResult, account, - bidErrors, - eventsContext)) + bidErrors)) .collect(Collectors.toList()); + final Long auctionTimestamp = eventsContext.getAuctionTimestamp(); final ExtBidResponse extBidResponse = toExtBidResponse( - bidderResponses, + bidderResponseInfos, auctionContext, cacheResult, videoStoredDataResult, auctionTimestamp, - debugEnabled, bidErrors); return BidResponse.builder() .id(bidRequest.getId()) .cur(bidRequest.getCur().get(0)) .seatbid(seatBids) - .ext(mapper.mapper().valueToTree(extBidResponse)) + .ext(extBidResponse) .build(); } - private Future videoStoredDataResult(List imps, Timeout timeout) { + private Future videoStoredDataResult(AuctionContext auctionContext) { + final List imps = auctionContext.getBidRequest().getImp(); + final String accountId = auctionContext.getAccount().getId(); + final Timeout timeout = auctionContext.getTimeout(); + final List errors = new ArrayList<>(); final List storedVideoInjectableImps = new ArrayList<>(); + for (Imp imp : imps) { try { if (checkEchoVideoAttrs(imp)) { @@ -713,7 +1080,7 @@ private Future videoStoredDataResult(List imps, Time } } - return storedRequestProcessor.videoStoredDataResult(storedVideoInjectableImps, errors, timeout) + return storedRequestProcessor.videoStoredDataResult(accountId, storedVideoInjectableImps, errors, timeout) .otherwise(throwable -> VideoStoredDataResult.of(Collections.emptyMap(), Collections.singletonList(throwable.getMessage()))); } @@ -741,34 +1108,32 @@ private boolean checkEchoVideoAttrs(Imp imp) { * Creates an OpenRTB {@link SeatBid} for a bidder. It will contain all the bids supplied by a bidder and a "bidder" * extension field populated. */ - private SeatBid toSeatBid(BidderResponse bidderResponse, + private SeatBid toSeatBid(List bidInfos, ExtRequestTargeting targeting, BidRequest bidRequest, - Set winningBids, - Set winningBidsByBidder, - BidRequestCacheInfo cacheInfo, - Map cachedBids, - VideoStoredDataResult videoStoredDataResult, + BidRequestCacheInfo requestCacheInfo, + Map bidToCacheInfo, Account account, - Map> bidErrors, - EventsContext eventsContext) { + Map> bidErrors) { - final String bidder = bidderResponse.getBidder(); - - final List bids = bidderResponse.getSeatBid().getBids().stream() - .map(bidderBid -> toBid( - bidderBid, - bidder, + final String bidder = bidInfos.stream() + .map(BidInfo::getBidder) + .findFirst() + // Should never occur + .orElseThrow(() -> new IllegalArgumentException("Bidder was not defined for bidInfo")); + + final List bids = bidInfos.stream() + .map(bidInfo -> injectAdmWithCacheInfo( + bidInfo, + requestCacheInfo, + bidToCacheInfo, + bidErrors)) + .filter(Objects::nonNull) + .map(bidInfo -> toBid( + bidInfo, targeting, bidRequest, - winningBids, - winningBidsByBidder, - cacheInfo, - cachedBids, - videoStoredDataResult.getImpIdToStoredVideo(), - account, - eventsContext, - bidErrors)) + account)) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -779,56 +1144,66 @@ private SeatBid toSeatBid(BidderResponse bidderResponse, .build(); } - /** - * Returns an OpenRTB {@link Bid} with "prebid" and "bidder" extension fields populated. - */ - private Bid toBid(BidderBid bidderBid, - String bidder, - ExtRequestTargeting targeting, - BidRequest bidRequest, - Set winningBids, - Set winningBidsByBidder, - BidRequestCacheInfo cacheInfo, - Map bidsWithCacheIds, - Map impIdToStoredVideo, - Account account, - EventsContext eventsContext, - Map> bidErrors) { - - final Bid bid = bidderBid.getBid(); - final BidType bidType = bidderBid.getType(); - - // preliminary variables are needed because bid is changing below, so we can lost it in winning bids sets - final boolean isWinningBid = winningBids.contains(bid); - final boolean isWinningBidByBidder = winningBidsByBidder.contains(bid); - - final CacheIdInfo cacheIdInfo = bidsWithCacheIds.get(bid); - final String cacheId = cacheIdInfo != null ? cacheIdInfo.getCacheId() : null; - final String videoCacheId = cacheIdInfo != null ? cacheIdInfo.getVideoCacheId() : null; - - if ((videoCacheId != null && !cacheInfo.isReturnCreativeVideoBids()) - || (cacheId != null && !cacheInfo.isReturnCreativeBids())) { - bid.setAdm(null); + private BidInfo injectAdmWithCacheInfo(BidInfo bidInfo, + BidRequestCacheInfo requestCacheInfo, + Map bidsWithCacheIds, + Map> bidErrors) { + + final Bid bid = bidInfo.getBid(); + final BidType bidType = bidInfo.getBidType(); + final String bidder = bidInfo.getBidder(); + final Imp correspondingImp = bidInfo.getCorrespondingImp(); + + final CacheInfo cacheInfo = bidsWithCacheIds.get(bid); + final String cacheId = cacheInfo != null ? cacheInfo.getCacheId() : null; + final String videoCacheId = cacheInfo != null ? cacheInfo.getVideoCacheId() : null; + + String modifiedBidAdm = bid.getAdm(); + if ((videoCacheId != null && !requestCacheInfo.isReturnCreativeVideoBids()) + || (cacheId != null && !requestCacheInfo.isReturnCreativeBids())) { + modifiedBidAdm = null; } - final boolean isApp = bidRequest.getApp() != null; - if (isApp && bidType.equals(BidType.xNative) && bid.getAdm() != null) { + if (bidType.equals(BidType.xNative) && modifiedBidAdm != null) { try { - addNativeMarkup(bid, bidRequest.getImp()); + modifiedBidAdm = createNativeMarkup(modifiedBidAdm, correspondingImp); } catch (PreBidException e) { - bidErrors.putIfAbsent(bidder, new ArrayList<>()); - bidErrors.get(bidder) + bidErrors.computeIfAbsent(bidder, ignored -> new ArrayList<>()) .add(ExtBidderError.of(BidderError.Type.bad_server_response.getCode(), e.getMessage())); return null; } } + final Bid modifiedBid = bid.toBuilder().adm(modifiedBidAdm).build(); + return bidInfo.toBuilder() + .bid(modifiedBid) + .cacheInfo(cacheInfo) + .build(); + } + + /** + * Returns an OpenRTB {@link Bid} with "prebid" and "bidder" extension fields populated. + */ + private Bid toBid(BidInfo bidInfo, ExtRequestTargeting targeting, BidRequest bidRequest, Account account) { + final TargetingInfo targetingInfo = bidInfo.getTargetingInfo(); + final BidType bidType = bidInfo.getBidType(); + final Bid bid = bidInfo.getBid(); + + final CacheInfo cacheInfo = bidInfo.getCacheInfo(); + final String cacheId = cacheInfo != null ? cacheInfo.getCacheId() : null; + final String videoCacheId = cacheInfo != null ? cacheInfo.getVideoCacheId() : null; + + final boolean isApp = bidRequest.getApp() != null; + final Map targetingKeywords; - if (targeting != null && isWinningBidByBidder) { + final String bidderCode = targetingInfo.getBidderCode(); + if (targeting != null && targetingInfo.isTargetingEnabled() && targetingInfo.isBidderWinningBid()) { final TargetingKeywordsCreator keywordsCreator = resolveKeywordsCreator(bidType, targeting, isApp, bidRequest, account); - targetingKeywords = keywordsCreator.makeFor(bid, bidder, isWinningBid, cacheId, videoCacheId); + final boolean isWinningBid = targetingInfo.isWinningBid(); + targetingKeywords = keywordsCreator.makeFor(bid, bidderCode, isWinningBid, cacheId, + bidType.getName(), videoCacheId); } else { targetingKeywords = null; } @@ -837,43 +1212,44 @@ private Bid toBid(BidderBid bidderBid, final CacheAsset vastXml = videoCacheId != null ? toCacheAsset(videoCacheId) : null; final ExtResponseCache cache = bids != null || vastXml != null ? ExtResponseCache.of(bids, vastXml) : null; - final String generatedBidId = generateBidId ? UUID.randomUUID().toString() : null; - final String eventBidId = ObjectUtils.defaultIfNull(generatedBidId, bid.getId()); - final Video storedVideo = impIdToStoredVideo.get(bid.getImpid()); - final Events events = createEvents(bidder, account, eventBidId, eventsContext); - final ExtBidPrebidVideo extBidPrebidVideo = getExtBidPrebidVideo(bid.getExt()); + final ObjectNode originalBidExt = bid.getExt(); + final ExtBidPrebid extBidPrebid = getExtPrebid(originalBidExt); + final ExtBidPrebid.ExtBidPrebidBuilder extBidPrebidBuilder = extBidPrebid != null + ? extBidPrebid.toBuilder() + : ExtBidPrebid.builder(); - final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder() - .bidid(generatedBidId) - .type(bidType) + final ExtBidPrebid updatedExtBidPrebid = extBidPrebidBuilder .targeting(targetingKeywords) + .targetBidderCode(targetingInfo.isAddTargetBidderCode() ? bidderCode : null) .cache(cache) - .storedRequestAttributes(storedVideo) - .events(events) - .video(extBidPrebidVideo) .build(); - final ExtPrebid bidExt = ExtPrebid.of(extBidPrebid, bid.getExt()); - bid.setExt(mapper.mapper().valueToTree(bidExt)); + final ObjectNode updatedBidExt = + originalBidExt != null ? originalBidExt.deepCopy() : mapper.mapper().createObjectNode(); + updatedBidExt.set(PREBID_EXT, mapper.mapper().valueToTree(updatedExtBidPrebid)); - return bid; + final Integer ttl = cacheInfo != null ? ObjectUtils.max(cacheInfo.getTtl(), cacheInfo.getVideoTtl()) : null; + + return bid.toBuilder() + .ext(updatedBidExt) + .exp(ttl) + .build(); } - private void addNativeMarkup(Bid bid, List imps) { + private String createNativeMarkup(String bidAdm, Imp correspondingImp) { final Response nativeMarkup; try { - nativeMarkup = mapper.decodeValue(bid.getAdm(), Response.class); + nativeMarkup = mapper.decodeValue(bidAdm, Response.class); } catch (DecodeException e) { throw new PreBidException(e.getMessage()); } final List responseAssets = nativeMarkup.getAssets(); if (CollectionUtils.isNotEmpty(responseAssets)) { - final Native nativeImp = imps.stream() - .filter(imp -> imp.getId().equals(bid.getImpid()) && imp.getXNative() != null) - .findFirst() - .map(Imp::getXNative) - .orElseThrow(() -> new PreBidException("Could not find native imp")); + final Native nativeImp = correspondingImp != null ? correspondingImp.getXNative() : null; + if (nativeImp == null) { + throw new PreBidException("Could not find native imp"); + } final Request nativeRequest; try { @@ -883,8 +1259,10 @@ private void addNativeMarkup(Bid bid, List imps) { } responseAssets.forEach(asset -> setAssetTypes(asset, nativeRequest.getAssets())); - bid.setAdm(mapper.encode(nativeMarkup)); + return mapper.encode(nativeMarkup); } + + return bidAdm; } private static void setAssetTypes(Asset responseAsset, List requestAssets) { @@ -918,23 +1296,23 @@ private static com.iab.openrtb.request.Asset getAssetById(int assetId, .orElse(com.iab.openrtb.request.Asset.EMPTY); } - private Events createEvents(String bidder, - Account account, - String eventBidId, - EventsContext eventsContext) { - - return eventsContext.isEnabledForAccount() && eventsContext.isEnabledForRequest() - ? eventsService.createEvent( - eventBidId, - bidder, - account.getId(), - eventsContext.getAuctionTimestamp(), - eventsContext.getIntegration()) - : null; + private EventsContext createEventsContext(AuctionContext auctionContext) { + return EventsContext.builder() + .auctionId(auctionContext.getBidRequest().getId()) + .enabledForAccount(eventsEnabledForAccount(auctionContext)) + .enabledForRequest(eventsEnabledForRequest(auctionContext)) + .auctionTimestamp(auctionTimestamp(auctionContext)) + .integration(integrationFrom(auctionContext)) + .build(); } private static boolean eventsEnabledForAccount(AuctionContext auctionContext) { - return BooleanUtils.isTrue(auctionContext.getAccount().getEventsEnabled()); + final AccountAuctionConfig accountAuctionConfig = auctionContext.getAccount().getAuction(); + final AccountEventsConfig accountEventsConfig = + accountAuctionConfig != null ? accountAuctionConfig.getEvents() : null; + final Boolean accountEventsEnabled = accountEventsConfig != null ? accountEventsConfig.getEnabled() : null; + + return BooleanUtils.isTrue(accountEventsEnabled); } private static boolean eventsEnabledForRequest(AuctionContext auctionContext) { @@ -942,9 +1320,10 @@ private static boolean eventsEnabledForRequest(AuctionContext auctionContext) { } private static boolean eventsEnabledForChannel(AuctionContext auctionContext) { - final AccountAnalyticsConfig analyticsConfig = ObjectUtils.defaultIfNull( - auctionContext.getAccount().getAnalyticsConfig(), AccountAnalyticsConfig.fallback()); - final Map channelConfig = analyticsConfig.getAuctionEvents(); + final AccountAnalyticsConfig analyticsConfig = auctionContext.getAccount().getAnalytics(); + final Map channelConfig = ObjectUtils.defaultIfNull( + analyticsConfig != null ? analyticsConfig.getAuctionEvents() : null, + AccountAnalyticsConfig.fallbackAuctionEvents()); final String channelFromRequest = channelFromRequest(auctionContext.getBidRequest()); @@ -970,6 +1349,44 @@ private static boolean eventsAllowedByRequest(AuctionContext auctionContext) { return prebid != null && prebid.getEvents() != null; } + /** + * Extracts auction timestamp from {@link ExtRequest} or get it from {@link Clock} if it is null. + */ + private long auctionTimestamp(AuctionContext auctionContext) { + final ExtRequest requestExt = auctionContext.getBidRequest().getExt(); + final ExtRequestPrebid prebid = requestExt != null ? requestExt.getPrebid() : null; + final Long auctionTimestamp = prebid != null ? prebid.getAuctiontimestamp() : null; + return auctionTimestamp != null ? auctionTimestamp : clock.millis(); + } + + private static String integrationFrom(AuctionContext auctionContext) { + final ExtRequest extRequest = auctionContext.getBidRequest().getExt(); + final ExtRequestPrebid prebid = extRequest == null ? null : extRequest.getPrebid(); + + return prebid != null ? prebid.getIntegration() : null; + } + + private Events createEvents(String bidder, + Account account, + String bidId, + EventsContext eventsContext, + String lineItemId) { + + if (!eventsContext.isEnabledForAccount()) { + return null; + } + + return eventsContext.isEnabledForRequest() || StringUtils.isNotEmpty(lineItemId) + ? eventsService.createEvent( + bidId, + bidder, + account.getId(), + lineItemId, + eventsContext.isEnabledForRequest(), + eventsContext) + : null; + } + private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, ExtRequestTargeting targeting, boolean isApp, @@ -985,8 +1402,10 @@ private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, * Extracts targeting keywords settings from the bid request and creates {@link TargetingKeywordsCreator} * instance if it is present. */ - private TargetingKeywordsCreator keywordsCreator( - ExtRequestTargeting targeting, boolean isApp, BidRequest bidRequest, Account account) { + private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, + boolean isApp, + BidRequest bidRequest, + Account account) { final JsonNode priceGranularityNode = targeting.getPricegranularity(); return priceGranularityNode == null || priceGranularityNode.isNull() @@ -1004,7 +1423,6 @@ private Map keywordsCreatorByBidType(ExtReque Account account) { final ExtMediaTypePriceGranularity mediaTypePriceGranularity = targeting.getMediatypepricegranularity(); - if (mediaTypePriceGranularity == null) { return Collections.emptyMap(); } @@ -1042,6 +1460,7 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe parsePriceGranularity(priceGranularity), targeting.getIncludewinners(), targeting.getIncludebidderkeys(), + BooleanUtils.isTrue(targeting.getIncludeformat()), isApp, resolveTruncateAttrChars(targeting, account), cacheHost, @@ -1053,9 +1472,13 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe * Returns max targeting keyword length. */ private int resolveTruncateAttrChars(ExtRequestTargeting targeting, Account account) { + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + final Integer accountTruncateTargetAttr = + accountAuctionConfig != null ? accountAuctionConfig.getTruncateTargetAttr() : null; + return ObjectUtils.firstNonNull( truncateAttrCharsOrNull(targeting.getTruncateattrchars()), - truncateAttrCharsOrNull(account.getTruncateTargetAttr()), + truncateAttrCharsOrNull(accountTruncateTargetAttr), truncateAttrChars); } @@ -1084,20 +1507,34 @@ private CacheAsset toCacheAsset(String cacheId) { return CacheAsset.of(cacheAssetUrlTemplate.concat(cacheId), cacheId); } - private String integrationFrom(AuctionContext auctionContext) { - final ExtRequest extRequest = auctionContext.getBidRequest().getExt(); - final ExtRequestPrebid prebid = extRequest == null ? null : extRequest.getPrebid(); + private static Set nullIfEmpty(Set set) { + if (set.isEmpty()) { + return null; + } + return Collections.unmodifiableSet(set); + } - return prebid != null ? prebid.getIntegration() : null; + private static Map nullIfEmpty(Map map) { + if (map.isEmpty()) { + return null; + } + return Collections.unmodifiableMap(map); } /** * Creates {@link ExtBidPrebidVideo} from bid extension. */ private ExtBidPrebidVideo getExtBidPrebidVideo(ObjectNode bidExt) { - final ExtPrebid extPrebid = mapper.mapper() - .convertValue(bidExt, EXT_PREBID_TYPE_REFERENCE); - final ExtBidPrebid extBidPrebid = extPrebid != null ? extPrebid.getPrebid() : null; + final ExtBidPrebid extBidPrebid = getExtPrebid(bidExt); + return extBidPrebid != null ? extBidPrebid.getVideo() : null; } + + private ExtBidPrebid getExtPrebid(ObjectNode bidExt) { + if (bidExt == null || !bidExt.hasNonNull(PREBID_EXT)) { + return null; + } + + return mapper.mapper().convertValue(bidExt.get(PREBID_EXT), ExtBidPrebid.class); + } } diff --git a/src/main/java/org/prebid/server/auction/BidResponsePostProcessor.java b/src/main/java/org/prebid/server/auction/BidResponsePostProcessor.java index fcad2277082..26ff19c39a9 100644 --- a/src/main/java/org/prebid/server/auction/BidResponsePostProcessor.java +++ b/src/main/java/org/prebid/server/auction/BidResponsePostProcessor.java @@ -3,9 +3,9 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; -import io.vertx.ext.web.RoutingContext; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.cookie.proto.Uids; +import org.prebid.server.model.HttpRequestContext; import org.prebid.server.settings.model.Account; /** @@ -17,15 +17,18 @@ public interface BidResponsePostProcessor { /** * This method is called when auction is finished. * - * @param context represents initial web request + * @param httpRequest represents initial web request * @param uidsCookie auction request {@link Uids} container * @param bidRequest original auction request * @param bidResponse auction result * @param account {@link Account} fetched from request * @return a {@link Future} with (possibly modified) auction result */ - Future postProcess(RoutingContext context, UidsCookie uidsCookie, BidRequest bidRequest, - BidResponse bidResponse, Account account); + Future postProcess(HttpRequestContext httpRequest, + UidsCookie uidsCookie, + BidRequest bidRequest, + BidResponse bidResponse, + Account account); /** * Returns {@link NoOpBidResponsePostProcessor} instance that just does nothing to original auction result. @@ -39,8 +42,12 @@ static BidResponsePostProcessor noOp() { */ class NoOpBidResponsePostProcessor implements BidResponsePostProcessor { @Override - public Future postProcess(RoutingContext context, UidsCookie uidsCookie, BidRequest bidRequest, - BidResponse bidResponse, Account account) { + public Future postProcess(HttpRequestContext httpRequest, + UidsCookie uidsCookie, + BidRequest bidRequest, + BidResponse bidResponse, + Account account) { + return Future.succeededFuture(bidResponse); } } diff --git a/src/main/java/org/prebid/server/auction/BidderAliases.java b/src/main/java/org/prebid/server/auction/BidderAliases.java index ed05653f02b..7b005b59066 100644 --- a/src/main/java/org/prebid/server/auction/BidderAliases.java +++ b/src/main/java/org/prebid/server/auction/BidderAliases.java @@ -1,9 +1,9 @@ package org.prebid.server.auction; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.bidder.BidderCatalog; -import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -18,32 +18,28 @@ public class BidderAliases { private final BidderCatalog bidderCatalog; - private BidderAliases( - Map aliasToBidder, Map aliasToVendorId, BidderCatalog bidderCatalog) { + private BidderAliases(Map aliasToBidder, Map aliasToVendorId, + BidderCatalog bidderCatalog) { - this.aliasToBidder = ObjectUtils.firstNonNull(aliasToBidder, Collections.emptyMap()); - this.aliasToVendorId = ObjectUtils.firstNonNull(aliasToVendorId, Collections.emptyMap()); + this.aliasToBidder = MapUtils.emptyIfNull(aliasToBidder); + this.aliasToVendorId = MapUtils.emptyIfNull(aliasToVendorId); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); } - public static BidderAliases of( - Map aliasToBidder, Map aliasToVendorId, BidderCatalog bidderCatalog) { + public static BidderAliases of(Map aliasToBidder, Map aliasToVendorId, + BidderCatalog bidderCatalog) { return new BidderAliases(aliasToBidder, aliasToVendorId, bidderCatalog); } - public static BidderAliases of(BidderCatalog bidderCatalog) { - return new BidderAliases(null, null, bidderCatalog); - } - public boolean isAliasDefined(String alias) { - return aliasToBidder.containsKey(alias) || bidderCatalog.isAlias(alias); + return aliasToBidder.containsKey(alias); } public String resolveBidder(String aliasOrBidder) { return aliasToBidder.containsKey(aliasOrBidder) ? aliasToBidder.get(aliasOrBidder) - : ObjectUtils.firstNonNull(resolveBidderViaCatalog(aliasOrBidder), aliasOrBidder); + : ObjectUtils.defaultIfNull(resolveBidderViaCatalog(aliasOrBidder), aliasOrBidder); } public Integer resolveAliasVendorId(String alias) { @@ -53,8 +49,7 @@ public Integer resolveAliasVendorId(String alias) { } private String resolveBidderViaCatalog(String aliasOrBidder) { - final String resolvedBidder = bidderCatalog.nameByAlias(aliasOrBidder); - return bidderCatalog.isActive(resolvedBidder) ? resolvedBidder : null; + return bidderCatalog.isActive(aliasOrBidder) ? aliasOrBidder : null; } private Integer resolveAliasVendorIdViaCatalog(String alias) { diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index faa5c681611..49031d1cd8a 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -2,13 +2,19 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.DecimalNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Content; +import com.iab.openrtb.request.Deal; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Pmp; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Source; import com.iab.openrtb.request.User; +import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -24,7 +30,9 @@ import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.auction.model.MultiBidConfig; import org.prebid.server.auction.model.StoredResponseResult; +import org.prebid.server.auction.model.Tuple2; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.HttpBidderRequester; @@ -34,30 +42,71 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.deals.DealsProcessor; +import org.prebid.server.deals.events.ApplicationEventService; +import org.prebid.server.deals.model.TxnLog; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.CriteriaLogManager; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.proto.openrtb.ext.ExtPrebidBidders; +import org.prebid.server.proto.openrtb.ext.request.BidAdjustmentMediaType; import org.prebid.server.proto.openrtb.ext.request.ExtApp; -import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigFpd; +import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigOrtb; +import org.prebid.server.proto.openrtb.ext.request.ExtDeal; +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidadjustmentfactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtModules; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; import org.prebid.server.settings.model.Account; +import org.prebid.server.util.DealUtil; +import org.prebid.server.util.LineItemUtil; import org.prebid.server.util.StreamUtil; import org.prebid.server.validation.ResponseBidValidator; import org.prebid.server.validation.model.ValidationResult; @@ -65,12 +114,17 @@ import java.math.BigDecimal; import java.time.Clock; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -81,10 +135,13 @@ public class ExchangeService { private static final Logger logger = LoggerFactory.getLogger(ExchangeService.class); private static final String PREBID_EXT = "prebid"; - private static final String CONTEXT_EXT = "context"; - private static final String DATA = "data"; + private static final String BIDDER_EXT = "bidder"; + private static final String ORIGINAL_BID_CPM = "origbidcpm"; + private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; private static final String ALL_BIDDERS_CONFIG = "*"; - private static final String GENERIC_SCHAIN_KEY = "*"; + private static final Integer DEFAULT_MULTIBID_LIMIT_MIN = 1; + private static final Integer DEFAULT_MULTIBID_LIMIT_MAX = 9; + private static final String EID_ALLOWED_FOR_ALL_BIDDERS = "*"; private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000); @@ -93,30 +150,38 @@ public class ExchangeService { private final StoredResponseProcessor storedResponseProcessor; private final PrivacyEnforcementService privacyEnforcementService; private final FpdResolver fpdResolver; + private final SchainResolver schainResolver; private final HttpBidderRequester httpBidderRequester; private final ResponseBidValidator responseBidValidator; private final CurrencyConversionService currencyService; private final BidResponseCreator bidResponseCreator; + private final ApplicationEventService applicationEventService; private final BidResponsePostProcessor bidResponsePostProcessor; + private final HookStageExecutor hookStageExecutor; private final HttpInteractionLogger httpInteractionLogger; private final Metrics metrics; private final Clock clock; private final JacksonMapper mapper; + private final CriteriaLogManager criteriaLogManager; public ExchangeService(long expectedCacheTime, BidderCatalog bidderCatalog, StoredResponseProcessor storedResponseProcessor, PrivacyEnforcementService privacyEnforcementService, FpdResolver fpdResolver, + SchainResolver schainResolver, HttpBidderRequester httpBidderRequester, ResponseBidValidator responseBidValidator, CurrencyConversionService currencyService, BidResponseCreator bidResponseCreator, BidResponsePostProcessor bidResponsePostProcessor, + HookStageExecutor hookStageExecutor, + ApplicationEventService applicationEventService, HttpInteractionLogger httpInteractionLogger, Metrics metrics, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + CriteriaLogManager criteriaLogManager) { if (expectedCacheTime < 0) { throw new IllegalArgumentException("Expected cache time should be positive"); @@ -126,15 +191,19 @@ public ExchangeService(long expectedCacheTime, this.storedResponseProcessor = Objects.requireNonNull(storedResponseProcessor); this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); this.fpdResolver = Objects.requireNonNull(fpdResolver); + this.schainResolver = Objects.requireNonNull(schainResolver); this.httpBidderRequester = Objects.requireNonNull(httpBidderRequester); this.responseBidValidator = Objects.requireNonNull(responseBidValidator); this.currencyService = Objects.requireNonNull(currencyService); this.bidResponseCreator = Objects.requireNonNull(bidResponseCreator); this.bidResponsePostProcessor = Objects.requireNonNull(bidResponsePostProcessor); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); + this.applicationEventService = applicationEventService; this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); + this.criteriaLogManager = Objects.requireNonNull(criteriaLogManager); } /** @@ -142,44 +211,69 @@ public ExchangeService(long expectedCacheTime, * response containing returned bids and additional information in extensions. */ public Future holdAuction(AuctionContext context) { + return processAuctionRequest(context) + .map(bidResponse -> enrichWithHooksDebugInfo(bidResponse, context)) + .map(bidResponse -> updateHooksMetrics(context, bidResponse)); + } + + private Future processAuctionRequest(AuctionContext context) { + return context.isRequestRejected() + ? Future.succeededFuture(emptyResponse()) + : runAuction(context); + } + + private static BidResponse emptyResponse() { + return BidResponse.builder() + .seatbid(Collections.emptyList()) + .build(); + } + + private Future runAuction(AuctionContext context) { final UidsCookie uidsCookie = context.getUidsCookie(); final BidRequest bidRequest = context.getBidRequest(); final Timeout timeout = context.getTimeout(); final Account account = context.getAccount(); + final List debugWarnings = context.getDebugWarnings(); - final List storedResponse = new ArrayList<>(); + final List storedAuctionResponses = new ArrayList<>(); final BidderAliases aliases = aliases(bidRequest); final String publisherId = account.getId(); final BidRequestCacheInfo cacheInfo = bidRequestCacheInfo(bidRequest); - final boolean debugEnabled = isDebugEnabled(bidRequest); + final Map bidderToMultiBid = bidderToMultiBids(bidRequest, debugWarnings); - return storedResponseProcessor.getStoredResponseResult(bidRequest.getImp(), aliases, timeout) - .map(storedResponseResult -> populateStoredResponse(storedResponseResult, storedResponse)) - .compose(impsRequiredRequest -> extractBidderRequests(context, impsRequiredRequest, aliases)) + return storedResponseProcessor.getStoredResponseResult(bidRequest.getImp(), timeout) + .map(storedResponseResult -> populateStoredResponse(storedResponseResult, storedAuctionResponses)) + .compose(storedResponseResult -> extractBidderRequests( + context, storedResponseResult, aliases, bidderToMultiBid)) .map(bidderRequests -> updateRequestMetric( bidderRequests, uidsCookie, aliases, publisherId, context.getRequestTypeMetric())) .map(bidderRequests -> maybeLogBidderInteraction(context, bidderRequests)) - .compose(bidderRequests -> CompositeFuture.join(bidderRequests.stream() - .map(bidderRequest -> requestBids( - bidderRequest, - auctionTimeout(timeout, cacheInfo.isDoCaching()), - debugEnabled, - aliases)) - .collect(Collectors.toList()))) + .compose(bidderRequests -> CompositeFuture.join( + bidderRequests.stream() + .map(bidderRequest -> invokeHooksAndRequestBids( + context, + bidderRequest, + auctionTimeout(timeout, cacheInfo.isDoCaching()), + aliases)) + .collect(Collectors.toList()))) // send all the requests to the bidders and gathers results .map(CompositeFuture::list) .map(bidderResponses -> storedResponseProcessor.mergeWithBidderResponses( - bidderResponses, storedResponse, bidRequest.getImp())) - .map(bidderResponses -> validateAndAdjustBids(bidRequest, bidderResponses)) + bidderResponses, storedAuctionResponses, bidRequest.getImp())) + .map(bidderResponses -> validateAndAdjustBids(bidderResponses, context, aliases)) .map(bidderResponses -> updateMetricsFromResponses(bidderResponses, publisherId, aliases)) // produce response from bidder results .compose(bidderResponses -> bidResponseCreator.create( bidderResponses, context, cacheInfo, - debugEnabled)) + bidderToMultiBid)) + .map(bidResponse -> publishAuctionEvent(bidResponse, context)) + .map(bidResponse -> criteriaLogManager.traceResponse(logger, bidResponse, context.getBidRequest(), + context.getDebugContext().isDebugEnabled())) .compose(bidResponse -> bidResponsePostProcessor.postProcess( - context.getRoutingContext(), uidsCookie, bidRequest, bidResponse, account)); + context.getHttpRequest(), uidsCookie, bidRequest, bidResponse, account)) + .compose(bidResponse -> invokeResponseHooks(context, bidResponse)); } private BidderAliases aliases(BidRequest bidRequest) { @@ -194,18 +288,6 @@ private static ExtRequestTargeting targeting(BidRequest bidRequest) { return prebid != null ? prebid.getTargeting() : null; } - private static Map> currencyRates(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - final ExtRequestCurrency currency = prebid != null ? prebid.getCurrency() : null; - return currency != null ? currency.getRates() : null; - } - - private static Boolean usepbsrates(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - final ExtRequestCurrency currency = prebid != null ? prebid.getCurrency() : null; - return currency != null ? currency.getUsepbsrates() : null; - } - /** * Creates {@link BidRequestCacheInfo} based on {@link BidRequest} model. */ @@ -249,30 +331,85 @@ private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { return BidRequestCacheInfo.noCache(); } - /** - * Determines debug flag from {@link BidRequest} or {@link ExtRequest}. - */ - private static boolean isDebugEnabled(BidRequest bidRequest) { - if (Objects.equals(bidRequest.getTest(), 1)) { - return true; - } - final ExtRequestPrebid extRequestPrebid = extRequestPrebid(bidRequest); - return extRequestPrebid != null && Objects.equals(extRequestPrebid.getDebug(), 1); - } - private static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { final ExtRequest requestExt = bidRequest.getExt(); return requestExt != null ? requestExt.getPrebid() : null; } + private static Map bidderToMultiBids(BidRequest bidRequest, List debugWarnings) { + final ExtRequestPrebid extRequestPrebid = extRequestPrebid(bidRequest); + final Collection multiBids = extRequestPrebid != null + ? CollectionUtils.emptyIfNull(extRequestPrebid.getMultibid()) + : Collections.emptyList(); + + final Map bidderToMultiBid = new HashMap<>(); + for (ExtRequestPrebidMultiBid prebidMultiBid : multiBids) { + final String bidder = prebidMultiBid.getBidder(); + final List bidders = prebidMultiBid.getBidders(); + final Integer maxBids = prebidMultiBid.getMaxBids(); + final String codePrefix = prebidMultiBid.getTargetBidderCodePrefix(); + + if (bidder != null && CollectionUtils.isNotEmpty(bidders)) { + debugWarnings.add(String.format("Invalid MultiBid: bidder %s and bidders %s specified. " + + "Only bidder %s will be used.", bidder, bidders, bidder)); + + tryAddBidderWithMultiBid(bidder, maxBids, codePrefix, bidderToMultiBid, debugWarnings); + continue; + } + + if (bidder != null) { + tryAddBidderWithMultiBid(bidder, maxBids, codePrefix, bidderToMultiBid, debugWarnings); + } else if (CollectionUtils.isNotEmpty(bidders)) { + if (codePrefix != null) { + debugWarnings.add(String.format("Invalid MultiBid: CodePrefix %s that was specified for bidders %s " + + "will be skipped.", codePrefix, bidders)); + } + + bidders.forEach(currentBidder -> + tryAddBidderWithMultiBid(currentBidder, maxBids, null, bidderToMultiBid, debugWarnings)); + } else { + debugWarnings.add("Invalid MultiBid: Bidder and bidders was not specified."); + } + } + + return bidderToMultiBid; + } + + private static void tryAddBidderWithMultiBid(String bidder, + Integer maxBids, + String codePrefix, + Map bidderToMultiBid, + List debugWarnings) { + if (bidderToMultiBid.containsKey(bidder)) { + debugWarnings.add(String.format("Invalid MultiBid: Bidder %s specified multiple times.", bidder)); + return; + } + + if (maxBids == null) { + debugWarnings.add(String.format("Invalid MultiBid: MaxBids for bidder %s is not specified and " + + "will be skipped.", bidder)); + return; + } + + bidderToMultiBid.put(bidder, toMultiBid(bidder, maxBids, codePrefix)); + } + + private static MultiBidConfig toMultiBid(String bidder, Integer maxBids, String codePrefix) { + final int bidLimit = maxBids < DEFAULT_MULTIBID_LIMIT_MIN + ? DEFAULT_MULTIBID_LIMIT_MIN + : maxBids > DEFAULT_MULTIBID_LIMIT_MAX ? DEFAULT_MULTIBID_LIMIT_MAX : maxBids; + + return MultiBidConfig.of(bidder, bidLimit, codePrefix); + } + /** * Populates storedResponse parameter with stored {@link List} and returns {@link List} for which * request to bidders should be performed. */ - private static List populateStoredResponse(StoredResponseResult storedResponseResult, - List storedResponse) { - storedResponse.addAll(storedResponseResult.getStoredResponse()); - return storedResponseResult.getRequiredRequestImps(); + private static StoredResponseResult populateStoredResponse(StoredResponseResult storedResponseResult, + List storedResponse) { + storedResponse.addAll(storedResponseResult.getAuctionStoredResponse()); + return storedResponseResult; } /** @@ -284,9 +421,9 @@ private static List populateStoredResponse(StoredResponseResult storedRespo * i.e. the bidders will not see any other extension fields. If Imp extension name is alias, which is also defined * in bidRequest.ext.prebid.aliases and valid, separate {@link BidRequest} will be created for this alias and sent * to appropriate bidder. - * For example suppose {@link BidRequest} has two {@link Imp}s. First one with imp.ext[].rubicon and - * imp.ext[].rubiconAlias and second with imp.ext[].appnexus and imp.ext[].rubicon. Three {@link BidRequest}s will - * be created: + * For example suppose {@link BidRequest} has two {@link Imp}s. First one with imp.ext.prebid.bidder.rubicon and + * imp.ext.prebid.bidder.rubiconAlias and second with imp.ext.prebid.bidder.appnexus and + * imp.ext.prebid.bidder.rubicon. Three {@link BidRequest}s will be created: * 1. {@link BidRequest} with one {@link Imp}, where bidder extension points to rubiconAlias extension and will be * sent to Rubicon bidder. * 2. {@link BidRequest} with two {@link Imp}s, where bidder extension points to appropriate rubicon extension from @@ -304,22 +441,27 @@ private static List populateStoredResponse(StoredResponseResult storedRespo * {@link Imp}, and are known to {@link BidderCatalog} or aliases from bidRequest.ext.prebid.aliases. */ private Future> extractBidderRequests(AuctionContext context, - List requestedImps, - BidderAliases aliases) { - // sanity check: discard imps without extension - final List imps = requestedImps.stream() - .filter(imp -> imp.getExt() != null) + StoredResponseResult storedResponseResult, + BidderAliases aliases, + Map bidderToMultiBid) { + final List imps = storedResponseResult.getRequiredRequestImps().stream() + .filter(imp -> bidderParamsFromImpExt(imp.getExt()) != null) + .map(imp -> DealsProcessor.removeDealsOnlyBiddersWithoutDeals(imp, context)) + .filter(Objects::nonNull) .collect(Collectors.toList()); - // identify valid bidders and aliases out of imps final List bidders = imps.stream() - .flatMap(imp -> StreamUtil.asStream(imp.getExt().fieldNames()) - .filter(bidder -> !Objects.equals(bidder, PREBID_EXT) && !Objects.equals(bidder, CONTEXT_EXT)) + .flatMap(imp -> StreamUtil.asStream(bidderParamsFromImpExt(imp.getExt()).fieldNames()) .filter(bidder -> isValidBidder(bidder, aliases))) .distinct() .collect(Collectors.toList()); - return makeBidderRequests(bidders, context, aliases, imps); + return makeBidderRequests(bidders, context, aliases, storedResponseResult.getImpBidderToStoredBidResponse(), + imps, bidderToMultiBid); + } + + private static JsonNode bidderParamsFromImpExt(ObjectNode ext) { + return ext.get(PREBID_EXT).get(BIDDER_EXT); } /** @@ -347,7 +489,9 @@ private boolean isValidBidder(String bidder, BidderAliases aliases) { private Future> makeBidderRequests(List bidders, AuctionContext context, BidderAliases aliases, - List imps) { + Map> impBidderToStoredResponse, + List imps, + Map bidderToMultiBid) { final BidRequest bidRequest = context.getBidRequest(); final User user = bidRequest.getUser(); @@ -355,42 +499,58 @@ private Future> makeBidderRequests(List bidders, final Map uidsBody = uidsFromBody(extUser); final ExtRequest requestExt = bidRequest.getExt(); - final Map biddersToConfigs = getBiddersToConfigs(requestExt); - + final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); + final Map biddersToConfigs = getBiddersToConfigs(prebid); + final Map> eidPermissions = getEidPermissions(prebid); final Map bidderToUser = - prepareUsers(bidders, context, aliases, bidRequest, extUser, uidsBody, biddersToConfigs); + prepareUsers(bidders, context, aliases, bidRequest, extUser, uidsBody, biddersToConfigs, + eidPermissions); return privacyEnforcementService .mask(context, bidderToUser, bidders, aliases) .map(bidderToPrivacyResult -> - getBidderRequests(bidderToPrivacyResult, bidRequest, imps, biddersToConfigs)); + getBidderRequests(bidderToPrivacyResult, bidRequest, impBidderToStoredResponse, imps, + bidderToMultiBid, biddersToConfigs, aliases)); } - private Map getBiddersToConfigs(ExtRequest requestExt) { - final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); + private Map getBiddersToConfigs(ExtRequestPrebid prebid) { final List bidderConfigs = prebid == null ? null : prebid.getBidderconfig(); if (CollectionUtils.isEmpty(bidderConfigs)) { return Collections.emptyMap(); } - final Map bidderToConfig = new HashMap<>(); + final Map bidderToConfig = new HashMap<>(); bidderConfigs.stream() .filter(prebidBidderConfig -> prebidBidderConfig.getBidders().contains(ALL_BIDDERS_CONFIG)) - .map(prebidBidderConfig -> prebidBidderConfig.getConfig().getFpd()) + .map(prebidBidderConfig -> prebidBidderConfig.getConfig().getOrtb2()) .findFirst() .ifPresent(extBidderConfigFpd -> bidderToConfig.put(ALL_BIDDERS_CONFIG, extBidderConfigFpd)); for (ExtRequestPrebidBidderConfig config : bidderConfigs) { for (String bidder : config.getBidders()) { - final ExtBidderConfigFpd concreteFpd = config.getConfig().getFpd(); + final ExtBidderConfigOrtb concreteFpd = config.getConfig().getOrtb2(); bidderToConfig.putIfAbsent(bidder, concreteFpd); } } return bidderToConfig; } + /** + * Retrieves user eids from {@link ExtRequestPrebid} and converts them to map, where keys are eids sources + * and values are allowed bidders + */ + private Map> getEidPermissions(ExtRequestPrebid prebid) { + final ExtRequestPrebidData prebidData = prebid != null ? prebid.getData() : null; + final List eidPermissions = prebidData != null + ? prebidData.getEidPermissions() + : null; + return CollectionUtils.emptyIfNull(eidPermissions).stream() + .collect(Collectors.toMap(ExtRequestPrebidDataEidPermissions::getSource, + ExtRequestPrebidDataEidPermissions::getBidders)); + } + /** * Returns UIDs from request.user.ext or empty map if not defined. */ @@ -416,18 +576,19 @@ private Map prepareUsers(List bidders, BidRequest bidRequest, ExtUser extUser, Map uidsBody, - Map biddersToConfigs) { + Map biddersToConfigs, + Map> eidPermissions) { final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); final Map bidderToUser = new HashMap<>(); for (String bidder : bidders) { - final ExtBidderConfigFpd fpdConfig = ObjectUtils.firstNonNull(biddersToConfigs.get(bidder), + final ExtBidderConfigOrtb fpdConfig = ObjectUtils.defaultIfNull(biddersToConfigs.get(bidder), biddersToConfigs.get(ALL_BIDDERS_CONFIG)); final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.contains(bidder); final User preparedUser = prepareUser(bidRequest.getUser(), extUser, bidder, aliases, uidsBody, - context.getUidsCookie(), useFirstPartyData, fpdConfig); + context.getUidsCookie(), useFirstPartyData, fpdConfig, eidPermissions); bidderToUser.put(bidder, preparedUser); } return bidderToUser; @@ -437,7 +598,8 @@ private Map prepareUsers(List bidders, * Returns original {@link User} if user.buyeruid already contains uid value for bidder. * Otherwise, returns new {@link User} containing updated {@link ExtUser} and user.buyeruid. *

- * Also, removes user.ext.prebid (if present) and user.ext.data (in case bidder does not use first party data). + * Also, removes user.ext.prebid (if present), user.ext.data and user.data (in case bidder does not use first + * party data). */ private User prepareUser(User user, ExtUser extUser, @@ -446,15 +608,21 @@ private User prepareUser(User user, Map uidsBody, UidsCookie uidsCookie, boolean useFirstPartyData, - ExtBidderConfigFpd fpdConfig) { + ExtBidderConfigOrtb fpdConfig, + Map> eidPermissions) { final String updatedBuyerUid = updateUserBuyerUid(user, bidder, aliases, uidsBody, uidsCookie); - final boolean shouldCleanPrebid = extUser != null && extUser.getPrebid() != null; - final boolean shouldCleanData = extUser != null && extUser.getData() != null && !useFirstPartyData; - final boolean shouldUpdateUserExt = shouldCleanData || shouldCleanPrebid; + final List userEids = extractUserEids(user); + final List allowedUserEids = resolveAllowedEids(userEids, bidder, eidPermissions); + final boolean shouldUpdateUserEids = extUser != null + && allowedUserEids.size() != CollectionUtils.emptyIfNull(userEids).size(); + final boolean shouldCleanExtPrebid = extUser != null && extUser.getPrebid() != null; + final boolean shouldCleanExtData = extUser != null && extUser.getData() != null && !useFirstPartyData; + final boolean shouldUpdateUserExt = shouldCleanExtData || shouldCleanExtPrebid || shouldUpdateUserEids; + final boolean shouldCleanData = user != null && user.getData() != null && !useFirstPartyData; User maskedUser = user; - if (updatedBuyerUid != null || shouldUpdateUserExt) { + if (updatedBuyerUid != null || shouldUpdateUserExt || shouldCleanData) { final User.UserBuilder userBuilder = user == null ? User.builder() : user.toBuilder(); if (updatedBuyerUid != null) { userBuilder.buyeruid(updatedBuyerUid); @@ -462,12 +630,17 @@ private User prepareUser(User user, if (shouldUpdateUserExt) { final ExtUser updatedExtUser = extUser.toBuilder() - .prebid(shouldCleanPrebid ? null : extUser.getPrebid()) - .data(shouldCleanData ? null : extUser.getData()) + .prebid(shouldCleanExtPrebid ? null : extUser.getPrebid()) + .data(shouldCleanExtData ? null : extUser.getData()) + .eids(shouldUpdateUserEids ? nullIfEmpty(allowedUserEids) : userEids) .build(); userBuilder.ext(updatedExtUser.isEmpty() ? null : updatedExtUser); } + if (shouldCleanData) { + userBuilder.data(null); + } + maskedUser = userBuilder.build(); } @@ -489,6 +662,37 @@ private String updateUserBuyerUid(User user, String bidder, BidderAliases aliase : null; } + /** + * Extracts {@link List} from {@link User}. + * Returns null if user or its extension is null. + */ + private List extractUserEids(User user) { + final ExtUser extUser = user != null ? user.getExt() : null; + return extUser != null ? extUser.getEids() : null; + } + + /** + * Returns {@link List} allowed by {@param eidPermissions} per source per bidder. + */ + private List resolveAllowedEids(List userEids, String bidder, + Map> eidPermissions) { + return CollectionUtils.emptyIfNull(userEids) + .stream() + .filter(extUserEid -> isUserEidAllowed(extUserEid.getSource(), eidPermissions, bidder)) + .collect(Collectors.toList()); + } + + /** + * Returns true if {@param source} allowed by {@param eidPermissions} for particular bidder taking into account + * ealiases. + */ + private boolean isUserEidAllowed(String source, Map> eidPermissions, String bidder) { + final List allowedBidders = eidPermissions.get(source); + return CollectionUtils.isEmpty(allowedBidders) + || allowedBidders.contains(EID_ALLOWED_FOR_ALL_BIDDERS) + || allowedBidders.contains(bidder); + } + /** * Extracts UID from uids from body or {@link UidsCookie}. */ @@ -509,18 +713,27 @@ private String resolveCookieFamilyName(String bidder) { */ private List getBidderRequests(List bidderPrivacyResults, BidRequest bidRequest, + Map> impBidderToStoredBidResponse, List imps, - Map biddersToConfigs) { + Map bidderToMultiBid, + Map biddersToConfigs, + BidderAliases aliases) { + + final Map bidderToPrebidBidders = bidderToPrebidBidders(bidRequest); - final ExtRequest requestExt = bidRequest.getExt(); - final Map bidderToPrebidBidders = bidderToPrebidBidders(requestExt); - final Map bidderToPrebidSchains = bidderToPrebidSchains(requestExt); final List bidderRequests = bidderPrivacyResults.stream() // for each bidder create a new request that is a copy of original request except buyerid, imp // extensions, ext.prebid.data.bidders and ext.prebid.bidders. // Also, check whether to pass user.ext.data, app.ext.data and site.ext.data or not. - .map(bidderPrivacyResult -> createBidderRequest(bidderPrivacyResult, bidRequest, imps, - biddersToConfigs, bidderToPrebidBidders, bidderToPrebidSchains)) + .map(bidderPrivacyResult -> createBidderRequest( + bidderPrivacyResult, + bidRequest, + impBidderToStoredBidResponse, + imps, + bidderToMultiBid, + biddersToConfigs, + bidderToPrebidBidders, + aliases)) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -532,8 +745,8 @@ private List getBidderRequests(List bidderPr /** * Extracts a map of bidders to their arguments from {@link ObjectNode} prebid.bidders. */ - private static Map bidderToPrebidBidders(ExtRequest requestExt) { - final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); + private static Map bidderToPrebidBidders(BidRequest bidRequest) { + final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); final ObjectNode bidders = prebid == null ? null : prebid.getBidders(); if (bidders == null || bidders.isNull()) { @@ -549,43 +762,17 @@ private static Map bidderToPrebidBidders(ExtRequest requestExt return bidderToPrebidParameters; } - /** - * Extracts a map of bidders to their arguments from {@link ObjectNode} prebid.schains. - */ - private static Map bidderToPrebidSchains(ExtRequest requestExt) { - final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); - final List schains = prebid == null ? null : prebid.getSchains(); - - if (schains == null || schains.isEmpty()) { - return Collections.emptyMap(); - } - - final Map bidderToPrebidSchains = new HashMap<>(); - for (ExtRequestPrebidSchain schain : schains) { - final List bidders = schain.getBidders(); - if (CollectionUtils.isNotEmpty(bidders)) { - for (String bidder : bidders) { - if (bidderToPrebidSchains.containsKey(bidder)) { - bidderToPrebidSchains.remove(bidder); - logger.debug("Schain bidder {0} is rejected since it was defined more than once", bidder); - continue; - } - bidderToPrebidSchains.put(bidder, schain.getSchain()); - } - } - } - return bidderToPrebidSchains; - } - /** * Returns {@link BidderRequest} for the given bidder. */ private BidderRequest createBidderRequest(BidderPrivacyResult bidderPrivacyResult, BidRequest bidRequest, + Map> impBidderToStoredBidResponse, List imps, - Map biddersToConfigs, + Map bidderToMultiBid, + Map biddersToConfigs, Map bidderToPrebidBidders, - Map bidderToPrebidSchains) { + BidderAliases bidderAliases) { final String bidder = bidderPrivacyResult.getRequestBidder(); if (bidderPrivacyResult.isBlockedRequestByTcf()) { @@ -595,29 +782,28 @@ private BidderRequest createBidderRequest(BidderPrivacyResult bidderPrivacyResul final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.contains(bidder); - final ExtBidderConfigFpd fpdConfig = ObjectUtils.firstNonNull(biddersToConfigs.get(bidder), + final ExtBidderConfigOrtb fpdConfig = ObjectUtils.defaultIfNull(biddersToConfigs.get(bidder), biddersToConfigs.get(ALL_BIDDERS_CONFIG)); - final Site bidRequestSite = bidRequest.getSite(); final App bidRequestApp = bidRequest.getApp(); + final Site bidRequestSite = bidRequestApp == null ? bidRequest.getSite() : null; final ObjectNode fpdSite = fpdConfig != null ? fpdConfig.getSite() : null; final ObjectNode fpdApp = fpdConfig != null ? fpdConfig.getApp() : null; - if (bidRequestSite != null && fpdApp != null || bidRequestApp != null && fpdSite != null) { - logger.info("Request to bidder {0} rejected as both bidRequest.site and bidRequest.app are present" - + " after fpd data have been merged", bidder); - return null; - } + // stored bid response supported only for single imp requests + final String storedBidResponse = impBidderToStoredBidResponse.size() == 1 + ? impBidderToStoredBidResponse.get(imps.get(0).getId()).get(bidder) + : null; - return BidderRequest.of(bidder, bidRequest.toBuilder() + return BidderRequest.of(bidder, storedBidResponse, bidRequest.toBuilder() // User was already prepared above .user(bidderPrivacyResult.getUser()) .device(bidderPrivacyResult.getDevice()) - .imp(prepareImps(bidder, imps, useFirstPartyData)) + .imp(prepareImps(bidder, imps, useFirstPartyData, bidderAliases)) .app(prepareApp(bidRequestApp, fpdApp, useFirstPartyData)) .site(prepareSite(bidRequestSite, fpdSite, useFirstPartyData)) - .source(prepareSource(bidder, bidderToPrebidSchains, bidRequest.getSource())) - .ext(prepareExt(bidder, bidderToPrebidBidders, bidRequest.getExt())) + .source(prepareSource(bidder, bidRequest)) + .ext(prepareExt(bidder, bidderToPrebidBidders, bidderToMultiBid, bidRequest.getExt())) .build()); } @@ -625,47 +811,96 @@ private BidderRequest createBidderRequest(BidderPrivacyResult bidderPrivacyResul * For each given imp creates a new imp with extension crafted to contain only "prebid", "context" and * bidder-specific extension. */ - private List prepareImps(String bidder, List imps, boolean useFirstPartyData) { + private List prepareImps(String bidder, List imps, boolean useFirstPartyData, BidderAliases aliases) { return imps.stream() - .filter(imp -> imp.getExt().hasNonNull(bidder)) + .filter(imp -> bidderParamsFromImpExt(imp.getExt()).hasNonNull(bidder)) .map(imp -> imp.toBuilder() + .pmp(preparePmp(bidder, imp.getPmp(), aliases)) .ext(prepareImpExt(bidder, imp.getExt(), useFirstPartyData)) .build()) .collect(Collectors.toList()); } + /** + * Removes deal from {@link Pmp} if bidder's deals doesn't contain it. + */ + private Pmp preparePmp(String bidder, Pmp pmp, BidderAliases aliases) { + final List originalDeals = pmp != null ? pmp.getDeals() : null; + if (CollectionUtils.isEmpty(originalDeals)) { + return pmp; + } + + final List updatedDeals = originalDeals.stream() + .map(deal -> Tuple2.of(deal, toExtDeal(deal.getExt()))) + .filter((Tuple2 tuple) -> DealUtil.isBidderHasDeal(bidder, tuple.getRight(), aliases)) + .map((Tuple2 tuple) -> prepareDeal(tuple.getLeft(), tuple.getRight())) + .collect(Collectors.toList()); + + return pmp.toBuilder().deals(updatedDeals).build(); + } + + /** + * Returns {@link ExtDeal} from the given {@link ObjectNode}. + */ + private ExtDeal toExtDeal(ObjectNode ext) { + if (ext == null) { + return null; + } + try { + return mapper.mapper().treeToValue(ext, ExtDeal.class); + } catch (JsonProcessingException e) { + throw new PreBidException( + String.format("Error decoding bidRequest.imp.pmp.deal.ext: %s", e.getMessage()), e); + } + } + + /** + * Removes bidder from imp[].pmp.deal[].ext.line object if presents. + */ + private Deal prepareDeal(Deal deal, ExtDeal extDeal) { + final ExtDealLine line = extDeal != null ? extDeal.getLine() : null; + final ExtDealLine updatedLine = line != null + ? ExtDealLine.of(line.getLineItemId(), line.getExtLineItemId(), line.getSizes(), null) + : null; + + return updatedLine != null + ? deal.toBuilder().ext(mapper.mapper().valueToTree(ExtDeal.of(updatedLine))).build() + : deal; + } + /** * Creates a new imp extension for particular bidder having: *

    *
  • "prebid" field populated with an imp.ext.prebid field value, may be null
  • + *
  • "bidder" field populated with an imp.ext.prebid.bidder.{bidder} field value, not null
  • *
  • "context" field populated with an imp.ext.context field value, may be null
  • - *
  • "bidder" field populated with an imp.ext.{bidder} field value, not null
  • + *
  • "data" field populated with an imp.ext.data field value, may be null
  • *
*/ private ObjectNode prepareImpExt(String bidder, ObjectNode impExt, boolean useFirstPartyData) { - final JsonNode impExtPrebid = prepareImpExtPrebid(bidder, impExt.get(PREBID_EXT)); - final ObjectNode result = mapper.mapper().valueToTree(ExtPrebid.of(impExtPrebid, impExt.get(bidder))); - - final JsonNode contextNode = impExt.get(CONTEXT_EXT); - final boolean isContextNodePresent = contextNode != null && !contextNode.isNull(); - if (isContextNodePresent) { - final JsonNode contextNodeCopy = contextNode.deepCopy(); - if (!useFirstPartyData && contextNodeCopy.isObject()) { - ((ObjectNode) contextNodeCopy).remove(DATA); - } - result.set(CONTEXT_EXT, contextNodeCopy); + final ObjectNode modifiedImpExt = impExt.deepCopy(); + + final JsonNode impExtPrebid = cleanBidderParamsFromImpExtPrebid(impExt.get(PREBID_EXT)); + if (impExtPrebid == null) { + modifiedImpExt.remove(PREBID_EXT); + } else { + modifiedImpExt.set(PREBID_EXT, impExtPrebid); } - return result; + + modifiedImpExt.set(BIDDER_EXT, bidderParamsFromImpExt(impExt).get(bidder)); + + return fpdResolver.resolveImpExt(modifiedImpExt, useFirstPartyData); } - private JsonNode prepareImpExtPrebid(String bidder, JsonNode extImpPrebidNode) { - if (extImpPrebidNode != null && extImpPrebidNode.hasNonNull(bidder)) { - final ExtImpPrebid extImpPrebid = extImpPrebid(extImpPrebidNode).toBuilder() - .bidder((ObjectNode) extImpPrebidNode.get(bidder)) // leave appropriate bidder related data - .build(); - return mapper.mapper().valueToTree(extImpPrebid); + private JsonNode cleanBidderParamsFromImpExtPrebid(JsonNode extImpPrebidNode) { + if (extImpPrebidNode.size() > 1) { + return mapper.mapper().valueToTree( + extImpPrebid(extImpPrebidNode).toBuilder() + .bidder(null) + .build()); } - return extImpPrebidNode; + + return null; } /** @@ -680,14 +915,21 @@ private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { } /** - * Checks whether to pass the app.ext.data depending on request having a first party data + * Checks whether to pass the app.ext.data and app.content.data depending on request having a first party data * allowed for given bidder or not. And merge masked app with fpd config. */ private App prepareApp(App app, ObjectNode fpdApp, boolean useFirstPartyData) { final ExtApp appExt = app != null ? app.getExt() : null; + final Content content = app != null ? app.getContent() : null; - final App maskedApp = appExt != null && appExt.getData() != null && !useFirstPartyData - ? app.toBuilder().ext(maskExtApp(appExt)).build() + final boolean shouldCleanExtData = appExt != null && appExt.getData() != null && !useFirstPartyData; + final boolean shouldCleanContentData = content != null && content.getData() != null && !useFirstPartyData; + + final App maskedApp = shouldCleanExtData || shouldCleanContentData + ? app.toBuilder() + .ext(shouldCleanExtData ? maskExtApp(appExt) : appExt) + .content(shouldCleanContentData ? prepareContent(content) : content) + .build() : app; return useFirstPartyData @@ -701,14 +943,21 @@ private ExtApp maskExtApp(ExtApp appExt) { } /** - * Checks whether to pass the site.ext.data depending on request having a first party data + * Checks whether to pass the site.ext.data and site.content.data depending on request having a first party data * allowed for given bidder or not. And merge masked site with fpd config. */ private Site prepareSite(Site site, ObjectNode fpdSite, boolean useFirstPartyData) { final ExtSite siteExt = site != null ? site.getExt() : null; + final Content content = site != null ? site.getContent() : null; + + final boolean shouldCleanExtData = siteExt != null && siteExt.getData() != null && !useFirstPartyData; + final boolean shouldCleanContentData = content != null && content.getData() != null && !useFirstPartyData; - final Site maskedSite = siteExt != null && siteExt.getData() != null && !useFirstPartyData - ? site.toBuilder().ext(maskExtSite(siteExt)).build() + final Site maskedSite = shouldCleanExtData || shouldCleanContentData + ? site.toBuilder() + .ext(shouldCleanExtData ? maskExtSite(siteExt) : siteExt) + .content(shouldCleanContentData ? prepareContent(content) : content) + .build() : site; return useFirstPartyData @@ -716,6 +965,14 @@ private Site prepareSite(Site site, ObjectNode fpdSite, boolean useFirstPartyDat : maskedSite; } + private Content prepareContent(Content content) { + final Content updatedContent = content.toBuilder() + .data(null) + .build(); + + return updatedContent.isEmpty() ? null : updatedContent; + } + private ExtSite maskExtSite(ExtSite siteExt) { final ExtSite maskedExtSite = ExtSite.of(siteExt.getAmp(), null); return maskedExtSite.isEmpty() ? null : maskedExtSite; @@ -724,10 +981,10 @@ private ExtSite maskExtSite(ExtSite siteExt) { /** * Returns {@link Source} with corresponding request.ext.prebid.schains. */ - private Source prepareSource(String bidder, Map bidderToSchain, - Source receivedSource) { - final ExtRequestPrebidSchainSchain defaultSchain = bidderToSchain.get(GENERIC_SCHAIN_KEY); - final ExtRequestPrebidSchainSchain bidderSchain = bidderToSchain.getOrDefault(bidder, defaultSchain); + private Source prepareSource(String bidder, BidRequest bidRequest) { + final Source receivedSource = bidRequest.getSource(); + + final ExtRequestPrebidSchainSchain bidderSchain = schainResolver.resolveForBidder(bidder, bidRequest); if (bidderSchain == null) { return receivedSource; @@ -748,6 +1005,7 @@ private Source prepareSource(String bidder, Map bidderToPrebidBidders, + Map bidderToMultiBid, ExtRequest requestExt) { final ExtRequestPrebid extPrebid = requestExt != null ? requestExt.getPrebid() : null; @@ -760,7 +1018,12 @@ private ExtRequest prepareExt(String bidder, final boolean suppressBidderConfig = extPrebidBidderconfig != null; final boolean suppressPrebidData = extPrebidData != null; - if (bidderToPrebidBidders.isEmpty() && !suppressSchains && !suppressBidderConfig && !suppressPrebidData) { + if (bidderToPrebidBidders.isEmpty() + && bidderToMultiBid.isEmpty() + && !suppressSchains + && !suppressBidderConfig + && !suppressPrebidData) { + return requestExt; } @@ -775,6 +1038,7 @@ private ExtRequest prepareExt(String bidder, return ExtRequest.of( extPrebidBuilder + .multibid(resolveExtRequestMultiBids(bidderToMultiBid.get(bidder), bidder)) .bidders(bidders) .schains(null) .data(null) @@ -782,12 +1046,24 @@ private ExtRequest prepareExt(String bidder, .build()); } + private List resolveExtRequestMultiBids(MultiBidConfig multiBidConfig, + String bidder) { + return multiBidConfig != null + ? Collections.singletonList(ExtRequestPrebidMultiBid.of( + bidder, null, multiBidConfig.getMaxBids(), multiBidConfig.getTargetBidderCodePrefix())) + : null; + } + /** * Updates 'account.*.request', 'request' and 'no_cookie_requests' metrics for each {@link BidderRequest}. */ - private List updateRequestMetric(List bidderRequests, UidsCookie uidsCookie, - BidderAliases aliases, String publisherId, + private List updateRequestMetric(List bidderRequests, + UidsCookie uidsCookie, + BidderAliases aliases, + String publisherId, MetricName requestTypeMetric) { + + metrics.updateRequestBidderCardinalityMetric(bidderRequests.size()); metrics.updateAccountRequestMetrics(publisherId, requestTypeMetric); for (BidderRequest bidderRequest : bidderRequests) { @@ -798,34 +1074,79 @@ private List updateRequestMetric(List bidderReques metrics.updateAdapterRequestTypeAndNoCookieMetrics(bidder, requestTypeMetric, !isApp && noBuyerId); } + return bidderRequests; } - private static BigDecimal bidAdjustmentForBidder(BidRequest bidRequest, String bidder) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - final Map bidAdjustmentFactors = prebid != null ? prebid.getBidadjustmentfactors() : null; - return bidAdjustmentFactors != null ? bidAdjustmentFactors.get(bidder) : null; + private Future invokeHooksAndRequestBids(AuctionContext auctionContext, + BidderRequest bidderRequest, + Timeout timeout, + BidderAliases aliases) { + + final CaseInsensitiveMultiMap headers = auctionContext.getHttpRequest().getHeaders(); + final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled(); + + return hookStageExecutor.executeBidderRequestStage(bidderRequest, auctionContext) + .compose(stageResult -> requestBidsOrRejectBidder( + stageResult, bidderRequest, timeout, headers, debugEnabled, aliases)) + .compose(bidderResponse -> hookStageExecutor.executeRawBidderResponseStage( + bidderResponse, auctionContext) + .map(stageResult -> rejectBidderResponseOrProceed(stageResult, bidderResponse))); + } + + private Future requestBidsOrRejectBidder( + HookStageExecutionResult hookStageResult, + BidderRequest bidderRequest, + Timeout timeout, + CaseInsensitiveMultiMap requestHeaders, + boolean debugEnabled, + BidderAliases aliases) { + + return hookStageResult.isShouldReject() + ? Future.succeededFuture(BidderResponse.of(bidderRequest.getBidder(), BidderSeatBid.empty(), 0)) + : requestBids( + bidderRequest.with(hookStageResult.getPayload().bidRequest()), + timeout, + requestHeaders, + debugEnabled, + aliases); + } + + private BidderResponse rejectBidderResponseOrProceed(HookStageExecutionResult stageResult, + BidderResponse bidderResponse) { + + final List bids = stageResult.isShouldReject() + ? Collections.emptyList() + : stageResult.getPayload().bids(); + + return bidderResponse + .with(bidderResponse.getSeatBid().with(bids)); } /** * Passes the request to a corresponding bidder and wraps response in {@link BidderResponse} which also holds * recorded response time. */ - private Future requestBids( - BidderRequest bidderRequest, Timeout timeout, boolean debugEnabled, BidderAliases aliases) { + private Future requestBids(BidderRequest bidderRequest, + Timeout timeout, + CaseInsensitiveMultiMap requestHeaders, + boolean debugEnabled, + BidderAliases aliases) { final String bidderName = bidderRequest.getBidder(); final Bidder bidder = bidderCatalog.bidderByName(aliases.resolveBidder(bidderName)); final long startTime = clock.millis(); - return httpBidderRequester.requestBids(bidder, bidderRequest.getBidRequest(), timeout, debugEnabled) + return httpBidderRequester.requestBids(bidder, bidderRequest, timeout, requestHeaders, debugEnabled) .map(seatBid -> BidderResponse.of(bidderName, seatBid, responseTime(startTime))); } - private List validateAndAdjustBids(BidRequest bidRequest, List bidderResponses) { + private List validateAndAdjustBids( + List bidderResponses, AuctionContext auctionContext, BidderAliases aliases) { + return bidderResponses.stream() - .map(bidderResponse -> validBidderResponse(bidderResponse, bidRequest.getCur())) - .map(bidderResponse -> applyBidPriceChanges(bidderResponse, bidRequest)) + .map(bidderResponse -> validBidderResponse(bidderResponse, auctionContext, aliases)) + .map(bidderResponse -> applyBidPriceChanges(bidderResponse, auctionContext.getBidRequest())) .collect(Collectors.toList()); } @@ -836,28 +1157,44 @@ private List validateAndAdjustBids(BidRequest bidRequest, List * Returns input argument as the result if no errors found or creates new {@link BidderResponse} otherwise. */ - private BidderResponse validBidderResponse(BidderResponse bidderResponse, List requestCurrencies) { - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - final List bids = seatBid.getBids(); + private BidderResponse validBidderResponse( + BidderResponse bidderResponse, AuctionContext auctionContext, BidderAliases aliases) { - final List validBids = new ArrayList<>(bids.size()); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); final List errors = new ArrayList<>(seatBid.getErrors()); + final List requestCurrencies = bidRequest.getCur(); if (requestCurrencies.size() > 1) { errors.add(BidderError.badInput( String.format("Cur parameter contains more than one currency. %s will be used", requestCurrencies.get(0)))); } - for (BidderBid bid : bids) { - final ValidationResult validationResult = responseBidValidator.validate(bid.getBid()); + final List bids = seatBid.getBids(); + final List validBids = new ArrayList<>(bids.size()); + + final TxnLog txnLog = auctionContext.getTxnLog(); + final String bidder = bidderResponse.getBidder(); + + for (final BidderBid bid : bids) { + final String lineItemId = LineItemUtil.lineItemIdFrom(bid.getBid(), bidRequest.getImp(), mapper); + maybeRecordInTxnLog(lineItemId, () -> txnLog.lineItemsReceivedFromBidder().get(bidder)); + + final ValidationResult validationResult = + responseBidValidator.validate(bid, bidderResponse.getBidder(), auctionContext, aliases); + + if (validationResult.hasWarnings()) { + addAsBidderErrors(validationResult.getWarnings(), errors); + } + if (validationResult.hasErrors()) { - for (String error : validationResult.getErrors()) { - errors.add(BidderError.generic(error)); - } - } else { - validBids.add(bid); + addAsBidderErrors(validationResult.getErrors(), errors); + maybeRecordInTxnLog(lineItemId, txnLog::lineItemsResponseInvalidated); + continue; } + + validBids.add(bid); } return errors.isEmpty() @@ -865,6 +1202,23 @@ private BidderResponse validBidderResponse(BidderResponse bidderResponse, List messages, List errors) { + messages.stream().map(BidderError::generic).forEach(errors::add); + } + + private static void maybeRecordInTxnLog(String lineItemId, Supplier> metricSupplier) { + if (lineItemId != null) { + metricSupplier.get().add(lineItemId); + } + } + + private BidResponse publishAuctionEvent(BidResponse bidResponse, AuctionContext auctionContext) { + if (applicationEventService != null) { + applicationEventService.publishAuctionEvent(auctionContext); + } + return bidResponse; + } + /** * Performs changes on {@link Bid}s price depends on different between adServerCurrency and bidCurrency, * and adjustment factor. Will drop bid if currency conversion is needed but not possible. @@ -884,39 +1238,135 @@ private BidderResponse applyBidPriceChanges(BidderResponse bidderResponse, BidRe final List errors = new ArrayList<>(seatBid.getErrors()); final String adServerCurrency = bidRequest.getCur().get(0); - final BigDecimal priceAdjustmentFactor = bidAdjustmentForBidder(bidRequest, bidderResponse.getBidder()); - final Boolean usepbsrates = usepbsrates(bidRequest); for (final BidderBid bidderBid : bidderBids) { - final Bid bid = bidderBid.getBid(); - final String bidCurrency = bidderBid.getBidCurrency(); - final BigDecimal price = bid.getPrice(); try { - final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( - price, currencyRates(bidRequest), adServerCurrency, bidCurrency, usepbsrates); - - final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); - - if (adjustedPrice.compareTo(price) != 0) { - bid.setPrice(adjustedPrice); - } - updatedBidderBids.add(bidderBid); + final BidderBid updatedBidderBid = + updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency); + updatedBidderBids.add(updatedBidderBid); } catch (PreBidException e) { - errors.add(BidderError.generic( - String.format("Unable to covert bid currency %s to desired ad server currency %s. %s", - bidCurrency, adServerCurrency, e.getMessage()))); + errors.add(BidderError.generic(e.getMessage())); } } return bidderResponse.with(BidderSeatBid.of(updatedBidderBids, seatBid.getHttpCalls(), errors)); } + private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid, + BidderResponse bidderResponse, + BidRequest bidRequest, + String adServerCurrency) { + final Bid bid = bidderBid.getBid(); + final String bidCurrency = bidderBid.getBidCurrency(); + final BigDecimal price = bid.getPrice(); + + final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( + price, bidRequest, adServerCurrency, StringUtils.stripToNull(bidCurrency)); + + final BigDecimal priceAdjustmentFactor = + bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid); + final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); + + final ObjectNode bidExt = bid.getExt(); + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); + + updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency); + + final Bid.BidBuilder bidBuilder = bid.toBuilder(); + if (adjustedPrice.compareTo(price) != 0) { + bidBuilder.price(adjustedPrice); + } + + if (!updatedBidExt.isEmpty()) { + bidBuilder.ext(updatedBidExt); + } + + return bidderBid.with(bidBuilder.build()); + } + + private static BidAdjustmentMediaType resolveBidAdjustmentMediaType(String bidImpId, + List imps, + BidType bidType) { + + switch (bidType) { + case banner: + return BidAdjustmentMediaType.banner; + case xNative: + return BidAdjustmentMediaType.xNative; + case audio: + return BidAdjustmentMediaType.audio; + case video: + return resolveBidAdjustmentVideoMediaType(bidImpId, imps); + default: + throw new PreBidException("BidType not present for bidderBid"); + } + } + + private static BidAdjustmentMediaType resolveBidAdjustmentVideoMediaType(String bidImpId, List imps) { + final Video bidImpVideo = imps.stream() + .filter(imp -> imp.getId().equals(bidImpId)) + .map(Imp::getVideo) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + + if (bidImpVideo == null) { + return null; + } + + final Integer placement = bidImpVideo.getPlacement(); + return placement == null || Objects.equals(placement, 1) + ? BidAdjustmentMediaType.video + : BidAdjustmentMediaType.video_outstream; + } + + private static BigDecimal bidAdjustmentForBidder(String bidder, + BidRequest bidRequest, + BidderBid bidderBid) { + final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestBidadjustmentfactors extBidadjustmentfactors = prebid != null + ? prebid.getBidadjustmentfactors() + : null; + if (extBidadjustmentfactors == null) { + return null; + } + final BidAdjustmentMediaType mediaType = + resolveBidAdjustmentMediaType(bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType()); + + return resolveBidAdjustmentFactor(extBidadjustmentfactors, mediaType, bidder); + } + + private static BigDecimal resolveBidAdjustmentFactor(ExtRequestBidadjustmentfactors extBidadjustmentfactors, + BidAdjustmentMediaType mediaType, + String bidder) { + final Map> mediatypes = + extBidadjustmentfactors.getMediatypes(); + final Map adjustmentsByMediatypes = mediatypes != null ? mediatypes.get(mediaType) : null; + final BigDecimal adjustmentFactorByMediaType = + adjustmentsByMediatypes != null ? adjustmentsByMediatypes.get(bidder) : null; + if (adjustmentFactorByMediaType != null) { + return adjustmentFactorByMediaType; + } + return extBidadjustmentfactors.getAdjustments().get(bidder); + } + private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 ? price.multiply(priceAdjustmentFactor) : price; } + private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) { + addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price)); + if (StringUtils.isNotBlank(bidCurrency)) { + addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency)); + } + } + + private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) { + node.set(propertyName, propertyValue); + } + private int responseTime(long startTime) { return Math.toIntExact(clock.millis() - startTime); } @@ -982,6 +1432,11 @@ private List updateMetricsFromResponses( return bidderResponses; } + private Future invokeResponseHooks(AuctionContext auctionContext, BidResponse bidResponse) { + return hookStageExecutor.executeAuctionResponseStage(bidResponse, auctionContext) + .map(stageResult -> stageResult.getPayload().bidResponse()); + } + /** * Resolves {@link MetricName} by {@link BidderError.Type} value. */ @@ -1006,4 +1461,250 @@ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { } return errorMetric; } + + private BidResponse enrichWithHooksDebugInfo(BidResponse bidResponse, AuctionContext context) { + final ExtModules extModules = toExtModules(context); + + if (extModules == null) { + return bidResponse; + } + + final ExtBidResponse ext = bidResponse.getExt(); + final ExtBidResponsePrebid extPrebid = ext != null ? ext.getPrebid() : null; + + final ExtBidResponsePrebid updatedExtPrebid = ExtBidResponsePrebid.of( + extPrebid != null ? extPrebid.getAuctiontimestamp() : null, + extModules); + final ExtBidResponse updatedExt = (ext != null ? ext.toBuilder() : ExtBidResponse.builder()) + .prebid(updatedExtPrebid) + .build(); + + return bidResponse.toBuilder() + .ext(updatedExt) + .build(); + } + + private static ExtModules toExtModules(AuctionContext context) { + final Map>> errors = + toHookMessages(context, HookExecutionOutcome::getErrors); + final Map>> warnings = + toHookMessages(context, HookExecutionOutcome::getWarnings); + final ExtModulesTrace trace = toHookTrace(context); + + return ObjectUtils.anyNotNull(errors, warnings, trace) ? ExtModules.of(errors, warnings, trace) : null; + } + + private static Map>> toHookMessages( + AuctionContext context, + Function> messagesGetter) { + + if (!context.getDebugContext().isDebugEnabled()) { + return null; + } + + final Map> hookOutcomesByModule = + context.getHookExecutionContext().getStageOutcomes().values().stream() + .flatMap(Collection::stream) + .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) + .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) + .filter(hookOutcome -> CollectionUtils.isNotEmpty(messagesGetter.apply(hookOutcome))) + .collect(Collectors.groupingBy( + hookOutcome -> hookOutcome.getHookId().getModuleCode())); + + final Map>> messagesByModule = hookOutcomesByModule.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + outcomes -> outcomes.getValue().stream() + .collect(Collectors.groupingBy( + hookOutcome -> hookOutcome.getHookId().getHookImplCode())) + .entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + messagesLists -> messagesLists.getValue().stream() + .map(messagesGetter) + .flatMap(Collection::stream) + .collect(Collectors.toList()))))); + + return !messagesByModule.isEmpty() ? messagesByModule : null; + } + + private static ExtModulesTrace toHookTrace(AuctionContext context) { + final TraceLevel traceLevel = context.getDebugContext().getTraceLevel(); + + if (traceLevel == null) { + return null; + } + + final List stages = context.getHookExecutionContext().getStageOutcomes() + .entrySet().stream() + .map(stageOutcome -> toTraceStage(stageOutcome.getKey(), stageOutcome.getValue(), traceLevel)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (stages.isEmpty()) { + return null; + } + + final long executionTime = stages.stream().mapToLong(ExtModulesTraceStage::getExecutionTime).sum(); + + return ExtModulesTrace.of(executionTime, stages); + } + + private static ExtModulesTraceStage toTraceStage(Stage stage, + List stageOutcomes, + TraceLevel level) { + + final List extStageOutcomes = stageOutcomes.stream() + .map(stageOutcome -> toTraceStageOutcome(stageOutcome, level)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (extStageOutcomes.isEmpty()) { + return null; + } + + final long executionTime = extStageOutcomes.stream() + .mapToLong(ExtModulesTraceStageOutcome::getExecutionTime) + .max() + .orElse(0L); + + return ExtModulesTraceStage.of(stage, executionTime, extStageOutcomes); + } + + private static ExtModulesTraceStageOutcome toTraceStageOutcome( + StageExecutionOutcome stageOutcome, TraceLevel level) { + + final List groups = stageOutcome.getGroups().stream() + .map(group -> toTraceGroup(group, level)) + .collect(Collectors.toList()); + + if (groups.isEmpty()) { + return null; + } + + final long executionTime = groups.stream().mapToLong(ExtModulesTraceGroup::getExecutionTime).sum(); + + return ExtModulesTraceStageOutcome.of(stageOutcome.getEntity(), executionTime, groups); + } + + private static ExtModulesTraceGroup toTraceGroup(GroupExecutionOutcome group, TraceLevel level) { + final List invocationResults = group.getHooks().stream() + .map(hook -> toTraceInvocationResult(hook, level)) + .collect(Collectors.toList()); + + final long executionTime = invocationResults.stream() + .mapToLong(ExtModulesTraceInvocationResult::getExecutionTime) + .max() + .orElse(0L); + + return ExtModulesTraceGroup.of(executionTime, invocationResults); + } + + private static ExtModulesTraceInvocationResult toTraceInvocationResult(HookExecutionOutcome hook, + TraceLevel level) { + return ExtModulesTraceInvocationResult.builder() + .hookId(hook.getHookId()) + .executionTime(hook.getExecutionTime()) + .status(hook.getStatus()) + .message(hook.getMessage()) + .action(hook.getAction()) + .debugMessages(level == TraceLevel.verbose ? hook.getDebugMessages() : null) + .analyticsTags(level == TraceLevel.verbose ? toTraceAnalyticsTags(hook.getAnalyticsTags()) : null) + .build(); + } + + private static ExtModulesTraceAnalyticsTags toTraceAnalyticsTags(Tags analyticsTags) { + if (analyticsTags == null) { + return null; + } + + return ExtModulesTraceAnalyticsTags.of(CollectionUtils.emptyIfNull(analyticsTags.activities()).stream() + .filter(Objects::nonNull) + .map(ExchangeService::toTraceAnalyticsActivity) + .collect(Collectors.toList())); + } + + private static ExtModulesTraceAnalyticsActivity toTraceAnalyticsActivity(Activity activity) { + return ExtModulesTraceAnalyticsActivity.of( + activity.name(), + activity.status(), + CollectionUtils.emptyIfNull(activity.results()).stream() + .filter(Objects::nonNull) + .map(ExchangeService::toTraceAnalyticsResult) + .collect(Collectors.toList())); + } + + private static ExtModulesTraceAnalyticsResult toTraceAnalyticsResult(Result result) { + final AppliedTo appliedTo = result.appliedTo(); + final ExtModulesTraceAnalyticsAppliedTo extAppliedTo = appliedTo != null + ? ExtModulesTraceAnalyticsAppliedTo.builder() + .impIds(appliedTo.impIds()) + .bidders(appliedTo.bidders()) + .request(appliedTo.request() ? Boolean.TRUE : null) + .response(appliedTo.response() ? Boolean.TRUE : null) + .bidIds(appliedTo.bidIds()) + .build() + : null; + + return ExtModulesTraceAnalyticsResult.of(result.status(), result.values(), extAppliedTo); + } + + private T updateHooksMetrics(AuctionContext context, T result) { + final EnumMap> stageOutcomes = + context.getHookExecutionContext().getStageOutcomes(); + + final Account account = context.getAccount(); + + stageOutcomes.forEach((stage, outcomes) -> updateHooksStageMetrics(account, stage, outcomes)); + + // account might be null if request is rejected by the entrypoint hook + if (account != null) { + final String accountId = account.getId(); + + stageOutcomes.values().stream() + .flatMap(Collection::stream) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .collect(Collectors.groupingBy( + outcome -> outcome.getHookId().getModuleCode(), + Collectors.summingLong(HookExecutionOutcome::getExecutionTime))) + .forEach((moduleCode, executionTime) -> + metrics.updateAccountModuleDurationMetric(accountId, moduleCode, executionTime)); + } + + return result; + } + + private void updateHooksStageMetrics(Account account, Stage stage, List stageOutcomes) { + stageOutcomes.stream() + .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) + .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) + .forEach(hookOutcome -> updateHookInvocationMetrics(account, stage, hookOutcome)); + } + + private void updateHookInvocationMetrics(Account account, Stage stage, HookExecutionOutcome hookOutcome) { + final HookId hookId = hookOutcome.getHookId(); + final ExecutionStatus status = hookOutcome.getStatus(); + final ExecutionAction action = hookOutcome.getAction(); + final String moduleCode = hookId.getModuleCode(); + + metrics.updateHooksMetrics( + moduleCode, + stage, + hookId.getHookImplCode(), + status, + hookOutcome.getExecutionTime(), + action); + + // account might be null if request is rejected by the entrypoint hook + if (account != null) { + metrics.updateAccountHooksMetrics(account.getId(), moduleCode, status, action); + } + } + + private List nullIfEmpty(List value) { + return CollectionUtils.isEmpty(value) ? null : value; + } } diff --git a/src/main/java/org/prebid/server/auction/FpdResolver.java b/src/main/java/org/prebid/server/auction/FpdResolver.java index ba6643be2b7..7ac70523e71 100644 --- a/src/main/java/org/prebid/server/auction/FpdResolver.java +++ b/src/main/java/org/prebid/server/auction/FpdResolver.java @@ -1,6 +1,5 @@ package org.prebid.server.auction; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -8,12 +7,11 @@ import com.iab.openrtb.request.Site; import com.iab.openrtb.request.User; import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.proto.openrtb.ext.request.ExtApp; import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfig; -import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigFpd; -import org.prebid.server.proto.openrtb.ext.request.ExtImp; +import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigOrtb; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; @@ -21,7 +19,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.request.Targeting; -import org.prebid.server.util.JsonMergeUtil; import java.util.ArrayList; import java.util.Arrays; @@ -53,12 +50,11 @@ public class FpdResolver { "privacypolicy", "mobile")); private final JacksonMapper jacksonMapper; - private final JsonMergeUtil jsonMergeUtil; + private final JsonMerger jsonMerger; - public FpdResolver(JacksonMapper jacksonMapper) { + public FpdResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) { this.jacksonMapper = Objects.requireNonNull(jacksonMapper); - - this.jsonMergeUtil = new JsonMergeUtil(jacksonMapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); } public User resolveUser(User originUser, ObjectNode fpdUser) { @@ -174,21 +170,74 @@ public ObjectNode resolveImpExt(ObjectNode impExt, ObjectNode targeting) { if (impExt == null) { return jacksonMapper.mapper().createObjectNode() - .set(CONTEXT, jacksonMapper.mapper().createObjectNode().set(DATA, targeting)); + .set(DATA, targeting); } - final JsonNode extImpContext = impExt.get(CONTEXT); - final JsonNode extImpContextData = extImpContext != null && extImpContext.isObject() - ? extImpContext.get(DATA) - : null; + final JsonNode extImpData = impExt.get(DATA); - final ObjectNode resolvedData = extImpContextData != null - ? (ObjectNode) jsonMergeUtil.merge(targeting, extImpContextData) + final ObjectNode resolvedData = extImpData != null + ? (ObjectNode) jsonMerger.merge(targeting, extImpData) : targeting; - return extImpContext != null && extImpContext.isObject() - ? impExt.set(CONTEXT, ((ObjectNode) extImpContext).set(DATA, resolvedData)) - : impExt.set(CONTEXT, jacksonMapper.mapper().createObjectNode().set(DATA, resolvedData)); + return impExt.set(DATA, resolvedData); + } + + /** + * @param impExt might be modified within method + */ + public ObjectNode resolveImpExt(ObjectNode impExt, boolean useFirstPartyData) { + removeOrReplace(impExt, CONTEXT, sanitizeImpExtContext(impExt, useFirstPartyData)); + removeOrReplace(impExt, DATA, sanitizeImpExtData(impExt, useFirstPartyData)); + + return impExt; + } + + private JsonNode sanitizeImpExtContext(ObjectNode originalImpExt, boolean useFirstPartyData) { + if (!originalImpExt.hasNonNull(CONTEXT)) { + return null; + } + + final JsonNode updatedContextNode = originalImpExt.get(CONTEXT).deepCopy(); + if (!useFirstPartyData && updatedContextNode.hasNonNull(DATA)) { + ((ObjectNode) updatedContextNode).remove(DATA); + } + + return updatedContextNode.isObject() && updatedContextNode.isEmpty() ? null : updatedContextNode; + } + + private JsonNode sanitizeImpExtData(ObjectNode impExt, boolean useFirstPartyData) { + if (!useFirstPartyData) { + return null; + } + + final JsonNode contextNode = impExt.hasNonNull(CONTEXT) ? impExt.get(CONTEXT) : null; + final JsonNode contextDataNode = + contextNode != null && contextNode.hasNonNull(DATA) ? contextNode.get(DATA) : null; + + final JsonNode dataNode = impExt.get(DATA); + + final boolean dataIsNullOrObject = + dataNode == null || dataNode.isObject(); + final boolean contextDataIsObject = + contextDataNode != null && !contextDataNode.isNull() && contextDataNode.isObject(); + + final JsonNode mergedDataNode = dataIsNullOrObject && contextDataIsObject + ? dataNode != null ? jsonMerger.merge(contextDataNode, dataNode) : contextDataNode + : dataNode; + + if (mergedDataNode != null && !mergedDataNode.isNull()) { + return mergedDataNode; + } + + return null; + } + + private void removeOrReplace(ObjectNode impExt, String field, JsonNode jsonNode) { + if (jsonNode == null) { + impExt.remove(field); + } else { + impExt.set(field, jsonNode); + } } public ExtRequest resolveBidRequestExt(ExtRequest extRequest, Targeting targeting) { @@ -213,20 +262,27 @@ public ExtRequest resolveBidRequestExt(ExtRequest extRequest, Targeting targetin .data(resolvedExtRequestPrebidData != null ? resolvedExtRequestPrebidData : extRequestPrebidData) - .bidderconfig(resolvedBidderConfig).build()); + .bidderconfig(resolvedBidderConfig) + .build()); } return extRequest; } private ExtRequestPrebidData resolveExtRequestPrebidData(ExtRequestPrebidData data, List fpdBidders) { - if (CollectionUtils.isEmpty(fpdBidders) && data == null) { + if (CollectionUtils.isEmpty(fpdBidders)) { return null; } final List originBidders = data != null ? data.getBidders() : Collections.emptyList(); return CollectionUtils.isEmpty(originBidders) - ? ExtRequestPrebidData.of(fpdBidders) - : ExtRequestPrebidData.of(mergeBidders(fpdBidders, originBidders)); + ? ExtRequestPrebidData.of(fpdBidders, null) + : ExtRequestPrebidData.of(mergeBidders(fpdBidders, originBidders), null); + } + + private List mergeBidders(List fpdBidders, List originBidders) { + final HashSet resolvedBidders = new HashSet<>(originBidders); + resolvedBidders.addAll(fpdBidders); + return new ArrayList<>(resolvedBidders); } private List createAllowedAllBidderConfig(Targeting targeting) { @@ -238,34 +294,20 @@ private List createAllowedAllBidderConfig(Targetin final List bidders = Collections.singletonList(ALLOW_ALL_BIDDERS); return Collections.singletonList(ExtRequestPrebidBidderConfig.of(bidders, - ExtBidderConfig.of(ExtBidderConfigFpd.of(siteNode, null, userNode)))); - } - - private List mergeBidders(List fpdBidders, List originBidders) { - final HashSet resolvedBidders = new HashSet<>(originBidders); - resolvedBidders.addAll(fpdBidders); - return new ArrayList<>(resolvedBidders); + ExtBidderConfig.of(null, ExtBidderConfigOrtb.of(siteNode, null, userNode)))); } private ObjectNode mergeExtData(JsonNode fpdData, JsonNode originData) { if (fpdData.isMissingNode() || !fpdData.isObject()) { - return originData != null && originData.isObject() ? (ObjectNode) originData : null; + return originData != null && originData.isObject() ? ((ObjectNode) originData).deepCopy() : null; } if (originData != null && originData.isObject()) { - return (ObjectNode) jsonMergeUtil.merge(fpdData, originData); + return (ObjectNode) jsonMerger.merge(fpdData, originData); } return fpdData.isObject() ? (ObjectNode) fpdData : null; } - private ExtImp getExtImp(ObjectNode extImp) { - try { - return jacksonMapper.mapper().treeToValue(extImp, ExtImp.class); - } catch (JsonProcessingException e) { - throw new InvalidRequestException(String.format("Failed to decode imp.ext: %s", e.getMessage())); - } - } - private static void setAttr(ObjectNode source, ObjectNode dest, String fieldName) { final JsonNode field = source.get(fieldName); if (field != null) { diff --git a/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java b/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java index a69526d2349..b8c2abe448d 100644 --- a/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java +++ b/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java @@ -1,18 +1,17 @@ package org.prebid.server.auction; import de.malkusch.whoisServerList.publicSuffixList.PublicSuffixList; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpServerRequest; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.HttpRequestContext; import org.prebid.server.util.HttpUtil; -import java.net.MalformedURLException; -import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -30,10 +29,10 @@ public ImplicitParametersExtractor(PublicSuffixList psl) { * Determines Referer by checking 'url_override' request parameter, or if it's empty 'Referer' header. Then if * result is not blank and missing 'http://' prefix appends it. */ - public String refererFrom(HttpServerRequest request) { - final String urlOverride = request.getParam("url_override"); + public String refererFrom(HttpRequestContext request) { + final String urlOverride = request.getQueryParams().get("url_override"); final String url = StringUtils.isNotBlank(urlOverride) ? urlOverride - : StringUtils.trimToNull(request.headers().get(HttpUtil.REFERER_HEADER)); + : StringUtils.trimToNull(request.getHeaders().get(HttpUtil.REFERER_HEADER)); return StringUtils.isNotBlank(url) && !StringUtils.startsWith(url, "http") ? String.format("http://%s", url) @@ -41,30 +40,20 @@ public String refererFrom(HttpServerRequest request) { } /** - * Determines domain the address refers to. + * Determines top-level domain of the passed host name. * - * @throws PreBidException if address does not represent valid URL, host could not be found in URL or top level - * domain could not be derived from the host + * @throws PreBidException if top level domain could not be derived from the host name */ - public String domainFrom(String urlString) throws PreBidException { - final URL url; - try { - url = new URL(urlString); - } catch (MalformedURLException e) { - throw new PreBidException(String.format("Invalid URL '%s': %s", urlString, e.getMessage()), e); - } - - final String host = url.getHost(); + public String domainFrom(String host) throws PreBidException { if (StringUtils.isBlank(host)) { - throw new PreBidException(String.format("Host not found from URL '%s'", url.toString())); + throw new PreBidException("Host is not defined or can not be derived from request"); } final String domain = psl.getRegistrableDomain(host); if (domain == null) { // null means effective top level domain plus one couldn't be derived - throw new PreBidException( - String.format("Invalid URL '%s': cannot derive eTLD+1 for domain %s", host, host)); + throw new PreBidException(String.format("Cannot derive eTLD+1 for host %s", host)); } return domain; @@ -73,17 +62,23 @@ public String domainFrom(String urlString) throws PreBidException { /** * Determines IP-Address candidates by checking http headers and remote host address. */ - public List ipFrom(HttpServerRequest request) { - final MultiMap headers = request.headers(); + public List ipFrom(CaseInsensitiveMultiMap headers, String host) { + return ipFrom(headers::get, host); + } + + public List ipFrom(io.vertx.core.MultiMap headers, String host) { + return ipFrom(headers::get, host); + } + private List ipFrom(Function headerGetter, String host) { final List candidates = new ArrayList<>(); - candidates.add(headers.get("True-Client-IP")); - final String xff = headers.get("X-Forwarded-For"); + candidates.add(headerGetter.apply("True-Client-IP")); + final String xff = headerGetter.apply("X-Forwarded-For"); if (xff != null) { candidates.addAll(Arrays.asList(xff.split(","))); } - candidates.add(headers.get("X-Real-IP")); - candidates.add(request.remoteAddress().host()); + candidates.add(headerGetter.apply("X-Real-IP")); + candidates.add(host); return candidates.stream() .map(StringUtils::trimToNull) @@ -94,17 +89,17 @@ public List ipFrom(HttpServerRequest request) { /** * Determines User-Agent by checking 'User-Agent' http header. */ - public String uaFrom(HttpServerRequest request) { - return StringUtils.trimToNull(request.headers().get(HttpUtil.USER_AGENT_HEADER)); + public String uaFrom(HttpRequestContext request) { + return StringUtils.trimToNull(request.getHeaders().get(HttpUtil.USER_AGENT_HEADER)); } /** * Determines the value of 'secure' flag by checking if 'X-Forwarded-Proto' contains 'value' or if HTTP request * scheme is 'https'. Returns 1 if one of these conditions evaluates to true or null otherwise. */ - public Integer secureFrom(HttpServerRequest httpRequest) { - return StringUtils.equalsIgnoreCase(httpRequest.headers().get("X-Forwarded-Proto"), "https") - || StringUtils.equalsIgnoreCase(httpRequest.scheme(), "https") + public Integer secureFrom(HttpRequestContext httpRequest) { + return StringUtils.equalsIgnoreCase(httpRequest.getHeaders().get("X-Forwarded-Proto"), "https") + || StringUtils.equalsIgnoreCase(httpRequest.getScheme(), "https") ? 1 : null; } } diff --git a/src/main/java/org/prebid/server/auction/InterstitialProcessor.java b/src/main/java/org/prebid/server/auction/InterstitialProcessor.java index b80b887ec0b..bf4b3507858 100644 --- a/src/main/java/org/prebid/server/auction/InterstitialProcessor.java +++ b/src/main/java/org/prebid/server/auction/InterstitialProcessor.java @@ -5,11 +5,11 @@ import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.proto.openrtb.ext.request.ExtDevice; import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; -import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java index fbe5eaf63b5..c85e286c2ea 100644 --- a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java +++ b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java @@ -12,7 +12,8 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.util.JsonMergeUtil; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.log.ConditionalLogger; import java.util.ArrayList; import java.util.Arrays; @@ -33,10 +34,13 @@ public class OrtbTypesResolver { private static final Logger logger = LoggerFactory.getLogger(OrtbTypesResolver.class); + private static final ConditionalLogger ORTB_TYPES_RESOLVING_LOGGER = + new ConditionalLogger("ortb_resolving_warnings", logger); private static final String USER = "user"; private static final String APP = "app"; private static final String SITE = "site"; + private static final String CONTEXT = "context"; private static final String BIDREQUEST = "bidrequest"; private static final String TARGETING = "targeting"; private static final String UNKNOWN_REFERER = "unknown referer"; @@ -69,11 +73,11 @@ public class OrtbTypesResolver { } private final JacksonMapper jacksonMapper; - private final JsonMergeUtil jsonMergeUtil; + private final JsonMerger jsonMerger; - public OrtbTypesResolver(JacksonMapper jacksonMapper) { + public OrtbTypesResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) { this.jacksonMapper = Objects.requireNonNull(jacksonMapper); - this.jsonMergeUtil = new JsonMergeUtil(jacksonMapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); } /** @@ -81,16 +85,19 @@ public OrtbTypesResolver(JacksonMapper jacksonMapper) { * and bidderconfig. * Mutates both parameters, {@param fpdContainerNode} and {@param warnings}. */ - void normalizeBidRequest(JsonNode bidRequest, List warnings, String referer) { + public void normalizeBidRequest(JsonNode bidRequest, List warnings, String referer) { final List resolverWarnings = new ArrayList<>(); final String rowOriginBidRequest = getOriginalRowContainerNode(bidRequest); normalizeRequestFpdFields(bidRequest, resolverWarnings); final JsonNode bidderConfigs = bidRequest.path("ext").path("prebid").path("bidderconfig"); if (!bidderConfigs.isMissingNode() && bidderConfigs.isArray()) { for (JsonNode bidderConfig : bidderConfigs) { - final JsonNode config = bidderConfig.path("config").path("fpd"); - if (!config.isMissingNode()) { - normalizeStandardFpdFields(config, resolverWarnings, "bidrequest.ext.prebid.bidderconfig"); + + mergeFpdFieldsToOrtb2(bidderConfig); + + final JsonNode ortb2Config = bidderConfig.path("config").path("ortb2"); + if (!ortb2Config.isMissingNode()) { + normalizeStandardFpdFields(ortb2Config, resolverWarnings, "bidrequest.ext.prebid.bidderconfig"); } } } @@ -106,11 +113,57 @@ private String getOriginalRowContainerNode(JsonNode bidRequest) { } } + /** + * Merges fpd fields into ortb2: + * config.fpd.context -> config.ortb2.site + * config.fpd.user -> config.ortb2.user + */ + private void mergeFpdFieldsToOrtb2(JsonNode bidderConfig) { + final JsonNode config = bidderConfig.path("config"); + final JsonNode configFpd = config.path("fpd"); + + if (configFpd.isMissingNode()) { + return; + } + + final JsonNode configOrtb = config.path("ortb2"); + + final JsonNode fpdContext = configFpd.get(CONTEXT); + final JsonNode ortbSite = configOrtb.get(SITE); + final JsonNode updatedOrtbSite = ortbSite == null + ? fpdContext + : fpdContext != null ? jsonMerger.merge(fpdContext, ortbSite) : null; + + final JsonNode fpdUser = configFpd.get(USER); + final JsonNode ortbUser = configOrtb.get(USER); + final JsonNode updatedOrtbUser = ortbUser == null + ? fpdUser + : fpdUser != null ? jsonMerger.merge(fpdUser, ortbUser) : null; + + if (updatedOrtbUser == null && updatedOrtbSite == null) { + return; + } + + final ObjectNode ortbObjectNode = configOrtb.isMissingNode() + ? jacksonMapper.mapper().createObjectNode() + : (ObjectNode) configOrtb; + + if (updatedOrtbSite != null) { + ortbObjectNode.set(SITE, updatedOrtbSite); + } + + if (updatedOrtbUser != null) { + ortbObjectNode.set(USER, updatedOrtbUser); + } + + ((ObjectNode) config).set("ortb2", ortbObjectNode); + } + /** * Resolves fields types inconsistency to ortb2 protocol for {@param targeting}. * Mutates both parameters, {@param targeting} and {@param warnings}. */ - void normalizeTargeting(JsonNode targeting, List warnings, String referer) { + public void normalizeTargeting(JsonNode targeting, List warnings, String referer) { final List resolverWarnings = new ArrayList<>(); final String rowOriginTargeting = getOriginalRowContainerNode(targeting); normalizeStandardFpdFields(targeting, resolverWarnings, TARGETING); @@ -245,16 +298,16 @@ private JsonNode toCommaSeparatedTextNode(ObjectNode containerNode, String field } } - public void normalizeDataExtension(ObjectNode containerNode, String containerName, String nodePrefix, - List warnings) { + private void normalizeDataExtension(ObjectNode containerNode, String containerName, String nodePrefix, + List warnings) { final JsonNode data = containerNode.get(DATA); - if (data == null || data.isNull()) { + if (data == null || !data.isObject()) { return; } final JsonNode extData = containerNode.path(EXT).path(DATA); final JsonNode ext = containerNode.get(EXT); if (!extData.isNull() && !extData.isMissingNode()) { - final JsonNode resolvedExtData = jsonMergeUtil.merge(extData, data); + final JsonNode resolvedExtData = jsonMerger.merge(data, extData); ((ObjectNode) ext).set(DATA, resolvedExtData); } else { copyDataToExtData(containerNode, containerName, nodePrefix, warnings, data); @@ -293,12 +346,10 @@ private void processWarnings(List resolverWarning, List warnings if (CollectionUtils.isNotEmpty(resolverWarning)) { warnings.addAll(updateWithWarningPrefix(resolverWarning)); // log only 1% of cases - if (System.currentTimeMillis() % 100 == 0) { - logger.info(String.format("WARNINGS: %s. \n Referer = %s and %s = %s", - String.join("\n", resolverWarning), - StringUtils.isNotBlank(referer) ? referer : UNKNOWN_REFERER, - containerName, containerValue)); - } + ORTB_TYPES_RESOLVING_LOGGER.warn(String.format("WARNINGS: %s. \n Referer = %s and %s = %s", + String.join("\n", resolverWarning), + StringUtils.isNotBlank(referer) ? referer : UNKNOWN_REFERER, + containerName, containerValue), 0.01); } } diff --git a/src/main/java/org/prebid/server/auction/PreBidRequestContextFactory.java b/src/main/java/org/prebid/server/auction/PreBidRequestContextFactory.java deleted file mode 100644 index c88a9d364c0..00000000000 --- a/src/main/java/org/prebid/server/auction/PreBidRequestContextFactory.java +++ /dev/null @@ -1,258 +0,0 @@ -package org.prebid.server.auction; - -import com.fasterxml.jackson.core.type.TypeReference; -import io.vertx.core.CompositeFuture; -import io.vertx.core.Future; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.web.RoutingContext; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AdUnitBid; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.cookie.UidsCookieService; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.request.AdUnit; -import org.prebid.server.proto.request.Bid; -import org.prebid.server.proto.request.PreBidRequest; -import org.prebid.server.proto.response.MediaType; -import org.prebid.server.settings.ApplicationSettings; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Random; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Used in legacy request processing. - */ -public class PreBidRequestContextFactory { - - private static final Logger logger = LoggerFactory.getLogger(PreBidRequestContextFactory.class); - - private static final TypeReference> LIST_OF_BIDS_TYPE_REFERENCE = new TypeReference>() { - }; - - private final TimeoutResolver timeoutResolver; - private final ImplicitParametersExtractor paramsExtractor; - private final IpAddressHelper ipAddressHelper; - private final ApplicationSettings applicationSettings; - private final UidsCookieService uidsCookieService; - private final TimeoutFactory timeoutFactory; - private final JacksonMapper mapper; - - private final Random rand = new Random(); - - public PreBidRequestContextFactory(TimeoutResolver timeoutResolver, - ImplicitParametersExtractor paramsExtractor, - IpAddressHelper ipAddressHelper, - ApplicationSettings applicationSettings, - UidsCookieService uidsCookieService, - TimeoutFactory timeoutFactory, - JacksonMapper mapper) { - - this.timeoutResolver = Objects.requireNonNull(timeoutResolver); - this.paramsExtractor = Objects.requireNonNull(paramsExtractor); - this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); - this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.uidsCookieService = Objects.requireNonNull(uidsCookieService); - this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - this.mapper = Objects.requireNonNull(mapper); - } - - /** - * Creates a new instances of {@link PreBidRequestContext} wrapped into {@link Future} which - * can be be eventually completed with success or error result. - */ - public Future fromRequest(RoutingContext context) { - final Buffer body = context.getBody(); - - if (body == null) { - return Future.failedFuture(new PreBidException("Incoming request has no body")); - } - - final PreBidRequest preBidRequest; - try { - preBidRequest = mapper.decodeValue(body, PreBidRequest.class); - } catch (DecodeException e) { - return Future.failedFuture(new PreBidException(e.getMessage(), e.getCause())); - } - - final List adUnits = preBidRequest.getAdUnits(); - if (adUnits == null || adUnits.isEmpty()) { - return Future.failedFuture(new PreBidException("No ad units specified")); - } - - final PreBidRequest adjustedRequest = adjustRequestTimeout(preBidRequest); - final Timeout timeout = timeout(adjustedRequest); - - return extractBidders(adjustedRequest, timeout) - .map(bidders -> PreBidRequestContext.builder().adapterRequests(bidders)) - .map(builder -> - populatePreBidRequestContextBuilder(context, adjustedRequest, context.request(), builder)) - .map(builder -> builder.timeout(timeout)) - .map(PreBidRequestContext.PreBidRequestContextBuilder::build); - } - - private PreBidRequestContext.PreBidRequestContextBuilder populatePreBidRequestContextBuilder( - RoutingContext context, PreBidRequest preBidRequest, HttpServerRequest httpRequest, - PreBidRequestContext.PreBidRequestContextBuilder builder) { - - builder - .preBidRequest(preBidRequest) - .ip(findIpFromRequest(httpRequest)) - .secure(paramsExtractor.secureFrom(httpRequest)) - .isDebug(isDebug(preBidRequest, httpRequest)) - .noLiveUids(false); - - if (preBidRequest.getApp() == null) { - final String referer = paramsExtractor.refererFrom(httpRequest); - if (StringUtils.isBlank(referer)) { - throw new PreBidException("Referer cannot be null or empty"); - } - - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(context); - - builder.uidsCookie(uidsCookie) - .noLiveUids(!uidsCookie.hasLiveUids()) - .ua(paramsExtractor.uaFrom(httpRequest)) - .referer(referer) - // next method could throw exception which will cause future to become failed - .domain(paramsExtractor.domainFrom(referer)) - .build(); - } - return builder; - } - - private Future> extractBidders(PreBidRequest preBidRequest, Timeout timeout) { - // this is a List>> actually - final List adUnitBidFutures = preBidRequest.getAdUnits().stream() - .filter(PreBidRequestContextFactory::isValidAdUnit) - .map(unit -> resolveUnitBids(unit, timeout) - .map(bids -> bids.stream().map(bid -> toAdUnitBid(unit, bid)))) - .collect(Collectors.toList()); - - return CompositeFuture.join(adUnitBidFutures) - .map(future -> future.>list().stream() - .flatMap(Function.identity()) - .collect(Collectors.groupingBy(AdUnitBid::getBidderCode)) - .entrySet().stream() - .map(e -> AdapterRequest.of(e.getKey(), e.getValue())) - .collect(Collectors.toList())); - } - - private static boolean isValidAdUnit(AdUnit adUnit) { - return Objects.nonNull(adUnit.getCode()) && CollectionUtils.isNotEmpty(adUnit.getSizes()); - } - - private Future> resolveUnitBids(AdUnit unit, Timeout timeout) { - final Future> result; - - final String configId = unit.getConfigId(); - if (StringUtils.isNotBlank(configId)) { - result = applicationSettings.getAdUnitConfigById(configId, timeout) - .map(config -> toBids(config, configId)) - .otherwise(exception -> fallbackResult(exception, configId)); - } else { - result = Future.succeededFuture(unit.getBids()); - } - - return result; - } - - private List fallbackResult(Throwable exception, String configId) { - if (exception instanceof PreBidException) { - logger.warn("AdUnit config not found by id ''{0}'' from cache", configId); - } else { - logger.warn("Failed to load config ''{0}'' from cache", exception, configId); - } - return Collections.emptyList(); - } - - private List toBids(String config, String configId) { - try { - return mapper.decodeValue(config, LIST_OF_BIDS_TYPE_REFERENCE); - } catch (DecodeException e) { - throw new PreBidException(String.format("Cannot parse AdUnit config for id: %s", configId), e); - } - } - - private AdUnitBid toAdUnitBid(AdUnit unit, Bid bid) { - return AdUnitBid.builder() - .bidderCode(bid.getBidder()) - .sizes(unit.getSizes()) - .topframe(unit.getTopframe()) - .instl(unit.getInstl()) - .adUnitCode(unit.getCode()) - .bidId(StringUtils.defaultIfBlank(bid.getBidId(), Long.toUnsignedString(rand.nextLong()))) - .params(bid.getParams()) - .video(unit.getVideo()) - .mediaTypes(makeBidMediaTypes(unit.getMediaTypes())) - .build(); - } - - private Set makeBidMediaTypes(List mediaTypes) { - final Set result; - - if (mediaTypes != null && !mediaTypes.isEmpty()) { - result = new HashSet<>(); - for (String mediaType : mediaTypes) { - try { - result.add(MediaType.valueOf(mediaType.toLowerCase())); - } catch (IllegalArgumentException e) { - logger.warn("Invalid mediaType: {0}", mediaType); - } - } - if (result.isEmpty()) { - result.add(MediaType.banner); - } - } else { - result = Collections.singleton(MediaType.banner); - } - - return result; - } - - private PreBidRequest adjustRequestTimeout(PreBidRequest preBidRequest) { - final Long requestTimeout = preBidRequest.getTimeoutMillis(); - final long resolvedTimeout = timeoutResolver.resolve(requestTimeout); - final long timeout = timeoutResolver.adjustTimeout(resolvedTimeout); - - // check, do we really need to update request? - return !Objects.equals(requestTimeout, timeout) - ? preBidRequest.toBuilder().timeoutMillis(timeout).build() - : preBidRequest; - } - - private Timeout timeout(PreBidRequest preBidRequest) { - return timeoutFactory.create(preBidRequest.getTimeoutMillis()); - } - - private static boolean isDebug(PreBidRequest preBidRequest, HttpServerRequest httpRequest) { - return Objects.equals(preBidRequest.getIsDebug(), Boolean.TRUE) - || Objects.equals(httpRequest.getParam("debug"), "1"); - } - - private String findIpFromRequest(HttpServerRequest request) { - return paramsExtractor.ipFrom(request).stream() - .map(ipAddressHelper::toIpAddress) - .filter(Objects::nonNull) - .map(IpAddress::getIp) - .findFirst() - .orElse(null); - } -} diff --git a/src/main/java/org/prebid/server/auction/PriceGranularity.java b/src/main/java/org/prebid/server/auction/PriceGranularity.java index 2dc4c22e06a..e262d4052e1 100644 --- a/src/main/java/org/prebid/server/auction/PriceGranularity.java +++ b/src/main/java/org/prebid/server/auction/PriceGranularity.java @@ -1,5 +1,6 @@ package org.prebid.server.auction; +import lombok.NoArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.EnumUtils; import org.prebid.server.exception.PreBidException; @@ -10,10 +11,12 @@ import java.util.Arrays; import java.util.EnumMap; import java.util.List; +import java.util.Objects; /** * Describes the behavior for price granularity feature. */ +@NoArgsConstructor public class PriceGranularity { enum PriceGranularityType { @@ -39,7 +42,7 @@ enum PriceGranularityType { range(20, 0.5)); } - static final PriceGranularity DEFAULT = STRING_TO_CUSTOM_PRICE_GRANULARITY.get(PriceGranularityType.med); + public static final PriceGranularity DEFAULT = STRING_TO_CUSTOM_PRICE_GRANULARITY.get(PriceGranularityType.med); private List ranges; private BigDecimal rangesMax; @@ -61,7 +64,7 @@ static PriceGranularity createFromExtPriceGranularity(ExtPriceGranularity extPri /** * Returns {@link PriceGranularity} by string representation if it is present in map, otherwise returns null. */ - static PriceGranularity createFromString(String stringPriceGranularity) { + public static PriceGranularity createFromString(String stringPriceGranularity) { if (isValidStringPriceGranularityType(stringPriceGranularity)) { return STRING_TO_CUSTOM_PRICE_GRANULARITY.get(PriceGranularityType.valueOf(stringPriceGranularity)); } else { @@ -105,15 +108,15 @@ private static void putStringPriceGranularity(PriceGranularityType type, Integer * Creates {@link PriceGranularity} from list of {@link ExtGranularityRange}s and validates it. */ private static PriceGranularity createFromRanges(Integer precision, List ranges) { - if (CollectionUtils.isEmpty(ranges)) { - throw new IllegalArgumentException("Ranges list cannot be null or empty"); - } - final BigDecimal rangeMax = ranges.stream() + final BigDecimal rangeMax = CollectionUtils.emptyIfNull(ranges).stream() + .filter(Objects::nonNull) .map(ExtGranularityRange::getMax) + .filter(Objects::nonNull) .max(BigDecimal::compareTo) .orElseThrow(() -> new IllegalArgumentException( - "Max value among all ranges was not found. Please check if ranges are valid")); + "Price granularity error: " + + "Max value among all ranges was not found. Please check if ranges are valid")); return new PriceGranularity(ranges, rangeMax, precision); } diff --git a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java index f9f3cb6f1c3..215f554693b 100644 --- a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java +++ b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java @@ -3,14 +3,18 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Site; import com.iab.openrtb.request.User; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import io.vertx.core.http.HttpServerRequest; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; -import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; -import org.prebid.server.auction.model.PreBidRequestContext; +import org.prebid.server.auction.model.IpAddress; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.execution.Timeout; import org.prebid.server.metric.MetricName; @@ -20,6 +24,7 @@ import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.privacy.gdpr.VendorIdResolver; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; +import org.prebid.server.privacy.gdpr.model.RequestLogInfo; import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.gdpr.model.TcfResponse; import org.prebid.server.privacy.model.Privacy; @@ -29,7 +34,10 @@ import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.request.CookieSyncRequest; import org.prebid.server.settings.model.Account; -import org.prebid.server.util.HttpUtil; +import org.prebid.server.settings.model.AccountCcpaConfig; +import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; +import org.prebid.server.settings.model.EnabledForRequestType; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; @@ -59,66 +67,85 @@ public class PrivacyEnforcementService { private final BidderCatalog bidderCatalog; private final PrivacyExtractor privacyExtractor; private final TcfDefinerService tcfDefinerService; + private final ImplicitParametersExtractor implicitParametersExtractor; private final IpAddressHelper ipAddressHelper; private final Metrics metrics; private final boolean ccpaEnforce; + private final boolean lmtEnforce; public PrivacyEnforcementService(BidderCatalog bidderCatalog, PrivacyExtractor privacyExtractor, TcfDefinerService tcfDefinerService, + ImplicitParametersExtractor implicitParametersExtractor, IpAddressHelper ipAddressHelper, Metrics metrics, - boolean ccpaEnforce) { + boolean ccpaEnforce, + boolean lmtEnforce) { this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.privacyExtractor = Objects.requireNonNull(privacyExtractor); this.tcfDefinerService = Objects.requireNonNull(tcfDefinerService); + this.implicitParametersExtractor = Objects.requireNonNull(implicitParametersExtractor); this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); this.metrics = Objects.requireNonNull(metrics); this.ccpaEnforce = ccpaEnforce; + this.lmtEnforce = lmtEnforce; } - Future contextFromBidRequest( - BidRequest bidRequest, Account account, MetricName requestType, Timeout timeout) { + public Future contextFromBidRequest(AuctionContext auctionContext) { + final BidRequest bidRequest = auctionContext.getBidRequest(); + final List errors = auctionContext.getPrebidErrors(); + final Account account = auctionContext.getAccount(); + final MetricName requestType = auctionContext.getRequestTypeMetric(); + final Timeout timeout = auctionContext.getTimeout(); + final List debugWarnings = auctionContext.getDebugWarnings(); - final Privacy privacy = privacyExtractor.validPrivacyFrom(bidRequest); + final Privacy privacy = privacyExtractor.validPrivacyFrom(bidRequest, errors); final Device device = bidRequest.getDevice(); - final String ipAddress = device != null ? device.getIp() : null; final Geo geo = device != null ? device.getGeo() : null; final String country = geo != null ? geo.getCountry() : null; - if (isCoppaMaskingRequired(privacy)) { - return Future.succeededFuture(PrivacyContext.of( - privacy, TcfContext.empty(), ipAddressHelper.maskIpv4(ipAddress))); - } + final String effectiveIpAddress = resolveIpAddress(device, privacy); - final String effectiveIpAddress = isLmtEnabled(device) ? ipAddressHelper.maskIpv4(ipAddress) : ipAddress; + final AccountGdprConfig accountGdpr = accountGdprConfig(account); + final String accountId = account.getId(); + final RequestLogInfo requestLogInfo = requestLogInfo(requestType, bidRequest, accountId); return tcfDefinerService.resolveTcfContext( - privacy, country, effectiveIpAddress, account.getGdpr(), requestType, timeout) + privacy, country, effectiveIpAddress, accountGdpr, requestType, requestLogInfo, timeout, + debugWarnings) .map(tcfContext -> PrivacyContext.of(privacy, tcfContext, tcfContext.getIpAddress())); } - public Future contextFromLegacyRequest(PreBidRequestContext preBidRequestContext, Account account) { - final Privacy privacy = privacyExtractor.validPrivacyFrom(preBidRequestContext.getPreBidRequest()); + private String resolveIpAddress(Device device, Privacy privacy) { + final boolean shouldBeMasked = isCoppaMaskingRequired(privacy) || isLmtEnabled(device); - return tcfDefinerService.resolveTcfContext( - privacy, - preBidRequestContext.getIp(), - account.getGdpr(), - preBidRequestContext.getTimeout()) - .map(tcfContext -> PrivacyContext.of(privacy, tcfContext)); + final String ipV4Address = device != null ? device.getIp() : null; + if (StringUtils.isNotBlank(ipV4Address)) { + return shouldBeMasked ? ipAddressHelper.maskIpv4(ipV4Address) : ipV4Address; + } + + final String ipV6Address = device != null ? device.getIpv6() : null; + if (StringUtils.isNotBlank(ipV6Address)) { + return shouldBeMasked ? ipAddressHelper.anonymizeIpv6(ipV6Address) : ipV6Address; + } + + return null; } public Future contextFromSetuidRequest( HttpServerRequest httpRequest, Account account, Timeout timeout) { final Privacy privacy = privacyExtractor.validPrivacyFromSetuidRequest(httpRequest); - final String ipAddress = HttpUtil.ipFrom(httpRequest); + final String ipAddress = resolveIpFromRequest(httpRequest); + final AccountGdprConfig accountGdpr = accountGdprConfig(account); + final String accountId = account.getId(); + final RequestLogInfo requestLogInfo = requestLogInfo(MetricName.setuid, null, accountId); - return tcfDefinerService.resolveTcfContext(privacy, ipAddress, account.getGdpr(), timeout) + return tcfDefinerService.resolveTcfContext( + privacy, ipAddress, accountGdpr, MetricName.setuid, requestLogInfo, timeout) .map(tcfContext -> PrivacyContext.of(privacy, tcfContext)); } @@ -126,12 +153,38 @@ public Future contextFromCookieSyncRequest( CookieSyncRequest cookieSyncRequest, HttpServerRequest httpRequest, Account account, Timeout timeout) { final Privacy privacy = privacyExtractor.validPrivacyFrom(cookieSyncRequest); - final String ipAddress = HttpUtil.ipFrom(httpRequest); + final String ipAddress = resolveIpFromRequest(httpRequest); + final AccountGdprConfig accountGdpr = accountGdprConfig(account); + final String accountId = account.getId(); + final RequestLogInfo requestLogInfo = requestLogInfo(MetricName.cookiesync, null, accountId); - return tcfDefinerService.resolveTcfContext(privacy, ipAddress, account.getGdpr(), timeout) + return tcfDefinerService.resolveTcfContext( + privacy, ipAddress, accountGdpr, MetricName.cookiesync, requestLogInfo, timeout) .map(tcfContext -> PrivacyContext.of(privacy, tcfContext)); } + private String resolveIpFromRequest(HttpServerRequest request) { + final MultiMap headers = request.headers(); + final String host = request.remoteAddress().host(); + final List requestIps = implicitParametersExtractor.ipFrom(headers, host); + return requestIps.stream() + .map(ipAddressHelper::toIpAddress) + .filter(Objects::nonNull) + .map(IpAddress::getIp) + .findFirst() + .orElse(null); + } + + private static RequestLogInfo requestLogInfo(MetricName requestType, BidRequest bidRequest, String accountId) { + if (Objects.equals(requestType, MetricName.openrtb2web)) { + final Site site = bidRequest != null ? bidRequest.getSite() : null; + final String refUrl = site != null ? site.getRef() : null; + return RequestLogInfo.of(requestType, refUrl, accountId); + } + + return RequestLogInfo.of(requestType, null, accountId); + } + Future> mask(AuctionContext auctionContext, Map bidderToUser, List bidders, @@ -152,28 +205,35 @@ Future> mask(AuctionContext auctionContext, updateCcpaMetrics(privacy.getCcpa()); final Map ccpaResult = - ccpaResult(bidRequest, account, bidders, aliases, device, bidderToUser, privacy); + ccpaResult(bidRequest, account, bidders, aliases, device, bidderToUser, privacy, requestType); final Set biddersToApplyTcf = new HashSet<>(bidders); biddersToApplyTcf.removeAll(ccpaResult.keySet()); return getBidderToEnforcementAction(privacyContext.getTcfContext(), biddersToApplyTcf, aliases, account) .map(bidderToEnforcement -> updatePrivacyMetrics( - bidderToEnforcement, aliases, requestType, device)) + bidderToEnforcement, aliases, requestType, bidderToUser, device)) .map(bidderToEnforcement -> getBidderToPrivacyResult( - biddersToApplyTcf, bidderToUser, device, bidderToEnforcement)) + bidderToEnforcement, biddersToApplyTcf, bidderToUser, device)) .map(gdprResult -> merge(ccpaResult, gdprResult)); } + public Future> resultForVendorIds(Set vendorIds, + TcfContext tcfContext) { + return tcfDefinerService.resultForVendorIds(vendorIds, tcfContext) + .map(TcfResponse::getActions); + } + private Map ccpaResult(BidRequest bidRequest, Account account, List bidders, BidderAliases aliases, Device device, Map bidderToUser, - Privacy privacy) { + Privacy privacy, + MetricName requestType) { - if (isCcpaEnforced(privacy.getCcpa(), account)) { + if (isCcpaEnforced(privacy.getCcpa(), account, requestType)) { return maskCcpa(extractCcpaEnforcedBidders(bidders, bidRequest, aliases), device, bidderToUser); } @@ -181,11 +241,46 @@ private Map ccpaResult(BidRequest bidRequest, } public boolean isCcpaEnforced(Ccpa ccpa, Account account) { - final boolean shouldEnforceCcpa = BooleanUtils.toBooleanDefaultIfNull(account.getEnforceCcpa(), ccpaEnforce); + final boolean shouldEnforceCcpa = isCcpaEnabled(account); return shouldEnforceCcpa && ccpa.isEnforced(); } + private boolean isCcpaEnforced(Ccpa ccpa, Account account, MetricName requestType) { + final boolean shouldEnforceCcpa = isCcpaEnabled(account, requestType); + return shouldEnforceCcpa && ccpa.isEnforced(); + } + + private Boolean isCcpaEnabled(Account account) { + final AccountPrivacyConfig accountPrivacyConfig = account.getPrivacy(); + final AccountCcpaConfig accountCcpaConfig = + accountPrivacyConfig != null ? accountPrivacyConfig.getCcpa() : null; + final Boolean accountEnforceCcpa = accountPrivacyConfig != null ? accountPrivacyConfig.getEnforceCcpa() : null; + final Boolean accountCcpaEnabled = accountCcpaConfig != null ? accountCcpaConfig.getEnabled() : null; + + return ObjectUtils.firstNonNull(accountCcpaEnabled, accountEnforceCcpa, ccpaEnforce); + } + + private boolean isCcpaEnabled(Account account, MetricName requestType) { + final AccountPrivacyConfig accountPrivacyConfig = account.getPrivacy(); + final AccountCcpaConfig accountCcpaConfig = + accountPrivacyConfig != null ? accountPrivacyConfig.getCcpa() : null; + final Boolean accountEnforceCcpa = accountPrivacyConfig != null ? accountPrivacyConfig.getEnforceCcpa() : null; + final Boolean accountCcpaEnabled = accountCcpaConfig != null ? accountCcpaConfig.getEnabled() : null; + if (requestType == null) { + return ObjectUtils.firstNonNull(accountCcpaEnabled, accountEnforceCcpa, ccpaEnforce); + } + + final EnabledForRequestType enabledForRequestType = accountCcpaConfig != null + ? accountCcpaConfig.getEnabledForRequestType() + : null; + + final Boolean enabledForType = enabledForRequestType != null + ? enabledForRequestType.isEnabledFor(requestType) + : null; + return ObjectUtils.firstNonNull(enabledForType, accountCcpaEnabled, accountEnforceCcpa, ccpaEnforce); + } + private Map maskCcpa( Set biddersToMask, Device device, Map bidderToUser) { @@ -286,11 +381,15 @@ private Future> getBidderToEnforcementActi TcfContext tcfContext, Set bidders, BidderAliases aliases, Account account) { return tcfDefinerService.resultForBidderNames( - new HashSet<>(bidders), VendorIdResolver.of(aliases), tcfContext, account.getGdpr()) + Collections.unmodifiableSet(bidders), + VendorIdResolver.of(aliases, bidderCatalog), + tcfContext, + accountGdprConfig(account)) .map(tcfResponse -> mapTcfResponseToEachBidder(tcfResponse, bidders)); } - private Set extractCcpaEnforcedBidders(List bidders, BidRequest bidRequest, BidderAliases aliases) { + private Set extractCcpaEnforcedBidders(List bidders, BidRequest bidRequest, BidderAliases + aliases) { final Set ccpaEnforcedBidders = new HashSet<>(bidders); final ExtRequest extBidRequest = bidRequest.getExt(); @@ -311,8 +410,8 @@ private Set extractCcpaEnforcedBidders(List bidders, BidRequest return ccpaEnforcedBidders; } - private Map mapTcfResponseToEachBidder( - TcfResponse tcfResponse, Set bidders) { + private static Map mapTcfResponseToEachBidder(TcfResponse tcfResponse, + Set bidders) { final Map bidderNameToAction = tcfResponse.getActions(); return bidders.stream().collect(Collectors.toMap(Function.identity(), bidderNameToAction::get)); @@ -326,39 +425,78 @@ private Map updatePrivacyMetrics( Map bidderToEnforcement, BidderAliases aliases, MetricName requestType, + Map bidderToUser, Device device) { + // Metrics should represent real picture of the bidding process, so if bidder request is blocked + // by privacy then no reason to increment another metrics, like geo masked, etc. for (final Map.Entry bidderEnforcement : bidderToEnforcement.entrySet()) { - final String bidder = aliases.resolveBidder(bidderEnforcement.getKey()); + final String bidder = bidderEnforcement.getKey(); final PrivacyEnforcementAction enforcement = bidderEnforcement.getValue(); + final boolean requestBlocked = enforcement.isBlockBidderRequest(); + + final User user = bidderToUser.get(bidder); + boolean userIdRemoved = enforcement.isRemoveUserIds(); + if (requestBlocked || (userIdRemoved && !shouldMaskUser(user))) { + userIdRemoved = false; + } + + boolean geoMasked = enforcement.isMaskGeo(); + if (requestBlocked || (geoMasked && !shouldMaskGeo(user, device))) { + geoMasked = false; + } + + final boolean analyticsBlocked = !requestBlocked && enforcement.isBlockAnalyticsReport(); + metrics.updateAuctionTcfMetrics( - bidder, + aliases.resolveBidder(bidder), requestType, - enforcement.isRemoveUserIds(), - enforcement.isMaskGeo(), - enforcement.isBlockBidderRequest(), - enforcement.isBlockAnalyticsReport()); + userIdRemoved, + geoMasked, + analyticsBlocked, + requestBlocked); } - if (isLmtEnabled(device)) { + if (lmtEnforce && isLmtEnabled(device)) { metrics.updatePrivacyLmtMetric(); } return bidderToEnforcement; } + /** + * Returns true if {@link User} has sensitive privacy information that can be masked. + */ + private static boolean shouldMaskUser(User user) { + if (user == null) { + return false; + } + if (user.getId() != null || user.getBuyeruid() != null) { + return true; + } + final ExtUser extUser = user.getExt(); + return extUser != null && (CollectionUtils.isNotEmpty(extUser.getEids())); + } + + /** + * Returns true if {@link User} or {@link Device} has {@link Geo} information that can be masked. + */ + private static boolean shouldMaskGeo(User user, Device device) { + return (user != null && user.getGeo() != null) || (device != null && device.getGeo() != null); + } + /** * Returns {@link Map}<{@link String}, {@link BidderPrivacyResult}>, where bidder name mapped to masked * {@link BidderPrivacyResult}. Masking depends on GDPR and COPPA. */ private List getBidderToPrivacyResult( + Map bidderToEnforcement, Set bidders, Map bidderToUser, - Device device, - Map bidderToEnforcement) { + Device device) { - final boolean isLmtEnabled = isLmtEnabled(device); + final boolean isLmtEnabled = lmtEnforce && isLmtEnabled(device); return bidderToUser.entrySet().stream() .filter(entry -> bidders.contains(entry.getKey())) .map(bidderUserEntry -> createBidderPrivacyResult( @@ -477,9 +615,9 @@ private static Float maskGeoCoordinate(Float coordinate) { } /** - * Returns masked digitrust and eids of user ext. + * Returns masked eids of user ext. */ - private ExtUser maskUserExt(ExtUser userExt) { + private static ExtUser maskUserExt(ExtUser userExt) { return userExt != null ? nullIfEmpty(userExt.toBuilder().eids(null).digitrust(null).build()) : null; @@ -510,4 +648,10 @@ private static List merge( result.addAll(gdprResult); return result; } + + private static AccountGdprConfig accountGdprConfig(Account account) { + final AccountPrivacyConfig privacyConfig = account.getPrivacy(); + + return privacyConfig != null ? privacyConfig.getGdpr() : null; + } } diff --git a/src/main/java/org/prebid/server/auction/SchainResolver.java b/src/main/java/org/prebid/server/auction/SchainResolver.java new file mode 100644 index 00000000000..2b06df94047 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/SchainResolver.java @@ -0,0 +1,129 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Source; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchainNode; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class SchainResolver { + + private static final Logger logger = LoggerFactory.getLogger(SchainResolver.class); + + private final ExtRequestPrebidSchainSchainNode globalNode; + + private SchainResolver(ExtRequestPrebidSchainSchainNode globalNode) { + this.globalNode = globalNode; + } + + public static SchainResolver create(String globalNodeString, JacksonMapper mapper) { + return new SchainResolver(globalNodeOrNull(globalNodeString, Objects.requireNonNull(mapper))); + } + + public ExtRequestPrebidSchainSchain resolveForBidder(String bidder, BidRequest bidRequest) { + final ExtRequest requestExt = bidRequest.getExt(); + final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); + final List schains = prebid == null ? null : prebid.getSchains(); + + ExtRequestPrebidSchainSchain bidderSchain = null; + ExtRequestPrebidSchainSchain catchAllSchain = null; + for (final ExtRequestPrebidSchain schain : ListUtils.emptyIfNull(schains)) { + catchAllSchain = existingSchainOrNull("*", catchAllSchain, schain); + bidderSchain = existingSchainOrNull(bidder, bidderSchain, schain); + } + + return enrich(ObjectUtils.defaultIfNull(bidderSchain, catchAllSchain), bidRequest); + } + + private static ExtRequestPrebidSchainSchainNode globalNodeOrNull(String globalNodeString, JacksonMapper mapper) { + if (StringUtils.isBlank(globalNodeString)) { + return null; + } + + try { + return mapper.decodeValue(globalNodeString, ExtRequestPrebidSchainSchainNode.class); + } catch (DecodeException e) { + throw new IllegalArgumentException("Exception occurred while parsing global schain node", e); + } + } + + private ExtRequestPrebidSchainSchain existingSchainOrNull(String bidder, + ExtRequestPrebidSchainSchain existingSchain, + ExtRequestPrebidSchain schainEntry) { + + if (schainEntry == null + || CollectionUtils.isEmpty(schainEntry.getBidders()) + || !schainEntry.getBidders().contains(bidder)) { + + return existingSchain; + } + + if (existingSchain != null) { + logger.debug("Schain bidder {0} is rejected since it was defined more than once", bidder); + return null; + } + + return schainEntry.getSchain(); + } + + private ExtRequestPrebidSchainSchain enrich(ExtRequestPrebidSchainSchain bidderSpecificSchain, + BidRequest bidRequest) { + + if (globalNode == null) { + return bidderSpecificSchain; + } + + if (bidderSpecificSchain != null) { + return enrichSchainWithGlobalNode(bidderSpecificSchain); + } + + final ExtRequestPrebidSchainSchain requestSchain = requestSchain(bidRequest); + if (requestSchain != null) { + return enrichSchainWithGlobalNode(requestSchain); + } + + return newSchainWithGlobalNode(); + } + + private ExtRequestPrebidSchainSchain requestSchain(BidRequest bidRequest) { + final Source source = bidRequest.getSource(); + final ExtSource extSource = source != null ? source.getExt() : null; + + return extSource != null ? extSource.getSchain() : null; + } + + private ExtRequestPrebidSchainSchain enrichSchainWithGlobalNode(ExtRequestPrebidSchainSchain requestSchain) { + return ExtRequestPrebidSchainSchain.of( + requestSchain.getVer(), + requestSchain.getComplete(), + appendGlobalNode(requestSchain.getNodes()), + requestSchain.getExt()); + } + + private List appendGlobalNode(List nodes) { + final ArrayList updatedNodes = new ArrayList<>(ListUtils.emptyIfNull(nodes)); + updatedNodes.add(globalNode); + + return updatedNodes; + } + + private ExtRequestPrebidSchainSchain newSchainWithGlobalNode() { + return ExtRequestPrebidSchainSchain.of(null, null, Collections.singletonList(globalNode), null); + } +} diff --git a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java index 86a74d7d218..83020d801de 100644 --- a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java @@ -5,12 +5,15 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Video; import io.vertx.core.Future; +import io.vertx.core.file.FileSystem; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.identity.IdGenerator; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; @@ -20,7 +23,6 @@ import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.VideoStoredDataResult; -import org.prebid.server.util.JsonMergeUtil; import java.util.ArrayList; import java.util.Collections; @@ -34,30 +36,65 @@ import java.util.stream.Collectors; /** - * Executes stored request processing + * Executes stored request processing. */ public class StoredRequestProcessor { + private static final String OVERRIDE_BID_REQUEST_ID_TEMPLATE = "{{UUID}}"; + private final long defaultTimeout; + private final boolean generateBidRequestId; + private final BidRequest defaultBidRequest; private final ApplicationSettings applicationSettings; - private final TimeoutFactory timeoutFactory; + private final IdGenerator idGenerator; private final Metrics metrics; + private final TimeoutFactory timeoutFactory; private final JacksonMapper mapper; - private JsonMergeUtil jsonMergeUtil; - - public StoredRequestProcessor(long defaultTimeout, - ApplicationSettings applicationSettings, - Metrics metrics, - TimeoutFactory timeoutFactory, - JacksonMapper mapper) { + private final JsonMerger jsonMerger; + + private StoredRequestProcessor(long defaultTimeout, + BidRequest defaultBidRequest, + boolean generateBidRequestId, + ApplicationSettings applicationSettings, + IdGenerator idGenerator, + Metrics metrics, + TimeoutFactory timeoutFactory, + JacksonMapper mapper, + JsonMerger jsonMerger) { this.defaultTimeout = defaultTimeout; - this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - this.metrics = Objects.requireNonNull(metrics); - this.mapper = Objects.requireNonNull(mapper); + this.defaultBidRequest = defaultBidRequest; + this.generateBidRequestId = generateBidRequestId; + this.applicationSettings = applicationSettings; + this.timeoutFactory = timeoutFactory; + this.idGenerator = idGenerator; + this.metrics = metrics; + this.mapper = mapper; + this.jsonMerger = jsonMerger; + } - jsonMergeUtil = new JsonMergeUtil(mapper); + public static StoredRequestProcessor create(long defaultTimeout, + String defaultBidRequestPath, + boolean generateBidRequestId, + FileSystem fileSystem, + ApplicationSettings applicationSettings, + IdGenerator idGenerator, + Metrics metrics, + TimeoutFactory timeoutFactory, + JacksonMapper mapper, + JsonMerger jsonMerger) { + + return new StoredRequestProcessor( + defaultTimeout, + readBidRequest( + defaultBidRequestPath, Objects.requireNonNull(fileSystem), Objects.requireNonNull(mapper)), + generateBidRequestId, + Objects.requireNonNull(applicationSettings), + Objects.requireNonNull(idGenerator), + Objects.requireNonNull(metrics), + Objects.requireNonNull(timeoutFactory), + Objects.requireNonNull(mapper), + Objects.requireNonNull(jsonMerger)); } /** @@ -66,7 +103,7 @@ public StoredRequestProcessor(long defaultTimeout, * fetched jsons from source. In case any error happen during the process, returns failedFuture with * InvalidRequestException {@link InvalidRequestException} as cause. */ - Future processStoredRequests(BidRequest bidRequest) { + public Future processStoredRequests(String accountId, BidRequest bidRequest) { final Map bidRequestToStoredRequestId; final Map impToStoredRequestId; try { @@ -86,53 +123,64 @@ Future processStoredRequests(BidRequest bidRequest) { } final Future storedDataFuture = - applicationSettings.getStoredData(requestIds, impIds, timeout(bidRequest)) + applicationSettings.getStoredData(accountId, requestIds, impIds, timeout(bidRequest)) .compose(storedDataResult -> updateMetrics(storedDataResult, requestIds, impIds)); - return storedRequestsToBidRequest(storedDataFuture, bidRequest, - bidRequestToStoredRequestId.get(bidRequest), impToStoredRequestId); - } - - private Future updateMetrics(StoredDataResult storedDataResult, Set requestIds, - Set impIds) { - requestIds.forEach( - id -> metrics.updateStoredRequestMetric(storedDataResult.getStoredIdToRequest().containsKey(id))); - impIds.forEach(id -> metrics.updateStoredImpsMetric(storedDataResult.getStoredIdToImp().containsKey(id))); - - return Future.succeededFuture(storedDataResult); + return storedRequestsToBidRequest( + storedDataFuture, bidRequest, bidRequestToStoredRequestId.get(bidRequest), impToStoredRequestId) + .map(this::generateBidRequestIdForApp); } /** * Fetches AMP request from the source. */ - Future processAmpRequest(String ampRequestId) { - final BidRequest bidRequest = BidRequest.builder().build(); + public Future processAmpRequest(String accountId, String ampRequestId, BidRequest bidRequest) { final Future ampStoredDataFuture = applicationSettings.getAmpStoredData( - Collections.singleton(ampRequestId), Collections.emptySet(), timeout(bidRequest)) + accountId, Collections.singleton(ampRequestId), Collections.emptySet(), timeout(bidRequest)) .compose(storedDataResult -> updateMetrics( storedDataResult, Collections.singleton(ampRequestId), Collections.emptySet())); - return storedRequestsToBidRequest(ampStoredDataFuture, bidRequest, ampRequestId, Collections.emptyMap()); + return storedRequestsToBidRequest(ampStoredDataFuture, bidRequest, ampRequestId, Collections.emptyMap()) + .map(this::generateBidRequestId); } /** * Fetches stored request.video and map existing values to imp.id. */ - Future videoStoredDataResult(List imps, List errors, Timeout timeout) { + Future videoStoredDataResult(String accountId, List imps, List errors, + Timeout timeout) { final Map storedIdToImpId = mapStoredRequestHolderToStoredRequestId(imps, this::getStoredRequestFromImp) .entrySet().stream() .collect(Collectors.toMap(Map.Entry::getValue, impIdToStoredId -> impIdToStoredId.getKey().getId())); - return applicationSettings.getStoredData(Collections.emptySet(), storedIdToImpId.keySet(), timeout) + return applicationSettings.getStoredData(accountId, Collections.emptySet(), storedIdToImpId.keySet(), timeout) .map(storedDataResult -> makeVideoStoredDataResult(storedDataResult, storedIdToImpId, errors)); } + private Future updateMetrics(StoredDataResult storedDataResult, Set requestIds, + Set impIds) { + requestIds.forEach( + id -> metrics.updateStoredRequestMetric(storedDataResult.getStoredIdToRequest().containsKey(id))); + impIds.forEach(id -> metrics.updateStoredImpsMetric(storedDataResult.getStoredIdToImp().containsKey(id))); + + return Future.succeededFuture(storedDataResult); + } + + private static BidRequest readBidRequest(String defaultBidRequestPath, FileSystem fileSystem, + JacksonMapper mapper) { + + return StringUtils.isNotBlank(defaultBidRequestPath) + ? mapper.decodeValue(fileSystem.readFileBlocking(defaultBidRequestPath), BidRequest.class) + : null; + } + private VideoStoredDataResult makeVideoStoredDataResult(StoredDataResult storedDataResult, Map storedIdToImpId, List errors) { + final Map storedIdToStoredImp = storedDataResult.getStoredIdToImp(); final Map impIdToStoredVideo = new HashMap<>(); @@ -170,8 +218,10 @@ private Video parseVideoFromImp(String storedJson) { } private Future storedRequestsToBidRequest(Future storedDataFuture, - BidRequest bidRequest, String storedBidRequestId, + BidRequest bidRequest, + String storedBidRequestId, Map impsToStoredRequestId) { + return storedDataFuture .recover(exception -> Future.failedFuture(new InvalidRequestException( String.format("Stored request fetching failed: %s", exception.getMessage())))) @@ -185,21 +235,32 @@ private Future storedRequestsToBidRequest(Future s /** * Runs {@link BidRequest} and {@link Imp}s merge processes. */ - private BidRequest mergeBidRequestAndImps(BidRequest bidRequest, String storedRequestId, - Map impToStoredId, StoredDataResult storedDataResult) { - return mergeBidRequestImps(mergeBidRequest(bidRequest, storedRequestId, storedDataResult), - impToStoredId, storedDataResult); + private BidRequest mergeBidRequestAndImps(BidRequest bidRequest, + String storedRequestId, + Map impToStoredId, + StoredDataResult storedDataResult) { + + return mergeBidRequestImps( + mergeBidRequest(mergeDefaultRequest(bidRequest), storedRequestId, storedDataResult), + impToStoredId, + storedDataResult); + } + + private BidRequest mergeDefaultRequest(BidRequest bidRequest) { + return jsonMerger.merge(bidRequest, defaultBidRequest, BidRequest.class); } /** * Merges original request with request from stored request source. Values from original request * has higher priority than stored request values. */ - private BidRequest mergeBidRequest(BidRequest originalRequest, String storedRequestId, + private BidRequest mergeBidRequest(BidRequest originalRequest, + String storedRequestId, StoredDataResult storedDataResult) { + final String storedRequest = storedDataResult.getStoredIdToRequest().get(storedRequestId); return StringUtils.isNotBlank(storedRequestId) - ? jsonMergeUtil.merge(originalRequest, storedRequest, storedRequestId, BidRequest.class) + ? jsonMerger.merge(originalRequest, storedRequest, storedRequestId, BidRequest.class) : originalRequest; } @@ -209,6 +270,7 @@ private BidRequest mergeBidRequest(BidRequest originalRequest, String storedRequ */ private BidRequest mergeBidRequestImps(BidRequest bidRequest, Map impToStoredId, StoredDataResult storedDataResult) { + if (impToStoredId.isEmpty()) { return bidRequest; } @@ -218,18 +280,30 @@ private BidRequest mergeBidRequestImps(BidRequest bidRequest, Map i final String storedRequestId = impToStoredId.get(imp); if (storedRequestId != null) { final String storedImp = storedDataResult.getStoredIdToImp().get(storedRequestId); - final Imp mergedImp = jsonMergeUtil.merge(imp, storedImp, storedRequestId, Imp.class); + final Imp mergedImp = jsonMerger.merge(imp, storedImp, storedRequestId, Imp.class); mergedImps.set(i, mergedImp); } } return bidRequest.toBuilder().imp(mergedImps).build(); } + private BidRequest generateBidRequestIdForApp(BidRequest bidRequest) { + return bidRequest.getApp() != null + ? generateBidRequestId(bidRequest) + : bidRequest; + } + + private BidRequest generateBidRequestId(BidRequest bidRequest) { + return generateBidRequestId || Objects.equals(bidRequest.getId(), OVERRIDE_BID_REQUEST_ID_TEMPLATE) + ? bidRequest.toBuilder().id(idGenerator.generateId()).build() + : bidRequest; + } + /** - * Maps object to its StoredRequestId if exists. If object's extension contains storedRequest field, expected that - * it includes id too, in another case error about missed id in stored request will be added to error list. - * Gathers all errors into list, and in case if it is not empty, throws {@link InvalidRequestException} with - * list of errors. + * Maps object to its StoredRequestId if exists. If object's extension contains storedRequest field, expected + * that it includes id too, in another case error about missed id in stored request will be added to error list. + * Gathers all errors into list, and in case if it is not empty, throws {@link InvalidRequestException} with list + * of errors. */ private Map mapStoredRequestHolderToStoredRequestId( List storedRequestHolders, Function storedRequestExtractor) { @@ -263,8 +337,7 @@ private Map mapStoredRequestHolderToStoredRequestId( } /** - * Extracts {@link ExtStoredRequest} from {@link BidRequest} if exists. In case when Extension has invalid - * format throws {@link InvalidRequestException} + * Extracts {@link ExtStoredRequest} from {@link BidRequest} if exists. */ private ExtStoredRequest getStoredRequestFromBidRequest(BidRequest bidRequest) { final ExtRequest ext = bidRequest.getExt(); @@ -279,7 +352,7 @@ private ExtStoredRequest getStoredRequestFromBidRequest(BidRequest bidRequest) { /** * Extracts {@link ExtStoredRequest} from {@link Imp} if exists. In case when Extension has invalid - * format throws {@link InvalidRequestException} + * format throws {@link InvalidRequestException}. */ private ExtStoredRequest getStoredRequestFromImp(Imp imp) { if (imp.getExt() != null) { diff --git a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java index 63a827f100f..6604efec0b6 100644 --- a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java +++ b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java @@ -11,7 +11,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.StoredResponseResult; -import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.auction.model.Tuple2; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.exception.InvalidRequestException; @@ -30,17 +30,13 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; /** * Resolves stored response data retrieving and BidderResponse merging processes. @@ -48,55 +44,68 @@ public class StoredResponseProcessor { private static final String PREBID_EXT = "prebid"; - private static final String CONTEXT_EXT = "context"; private static final String DEFAULT_BID_CURRENCY = "USD"; - private static final TypeReference> SEATBID_LIST_TYPEREFERENCE = new TypeReference>() { + + private static final TypeReference> SEATBID_LIST_TYPE = new TypeReference>() { }; private final ApplicationSettings applicationSettings; - private final BidderCatalog bidderCatalog; private final JacksonMapper mapper; public StoredResponseProcessor(ApplicationSettings applicationSettings, - BidderCatalog bidderCatalog, JacksonMapper mapper) { + this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.mapper = Objects.requireNonNull(mapper); } - Future getStoredResponseResult( - List imps, BidderAliases aliases, Timeout timeout) { + Future getStoredResponseResult(List imps, Timeout timeout) { + final Map impExtPrebids = getImpsExtPrebid(imps); + final Map auctionStoredResponseToImpId = getAuctionStoredResponses(impExtPrebids); + final List requiredRequestImps = excludeStoredAuctionResponseImps(imps, auctionStoredResponseToImpId); - final List requiredRequestImps = new ArrayList<>(); - final Map storedResponseIdToImpId = new HashMap<>(); + final Map> impToBidderToStoredBidResponseId = getStoredBidResponses(impExtPrebids, + requiredRequestImps); - try { - fillStoredResponseIdsAndRequestingImps(imps, requiredRequestImps, storedResponseIdToImpId, aliases); - } catch (InvalidRequestException e) { - return Future.failedFuture(e); - } + final Set storedIds = new HashSet<>(auctionStoredResponseToImpId.keySet()); + + storedIds.addAll( + impToBidderToStoredBidResponseId.values().stream() + .flatMap(bidderToId -> bidderToId.values().stream()) + .collect(Collectors.toSet())); - if (storedResponseIdToImpId.isEmpty()) { - return Future.succeededFuture(StoredResponseResult.of(imps, Collections.emptyList())); + if (storedIds.isEmpty()) { + return Future.succeededFuture(StoredResponseResult.of(imps, Collections.emptyList(), + Collections.emptyMap())); } - return applicationSettings.getStoredResponses(storedResponseIdToImpId.keySet(), timeout) + return applicationSettings.getStoredResponses(storedIds, timeout) .recover(exception -> Future.failedFuture(new InvalidRequestException( String.format("Stored response fetching failed with reason: %s", exception.getMessage())))) - .map(storedResponseDataResult -> convertToSeatBid(storedResponseDataResult, storedResponseIdToImpId)) - .map(storedResponse -> StoredResponseResult.of(requiredRequestImps, storedResponse)); + .map(storedResponseDataResult -> StoredResponseResult.of( + requiredRequestImps, + convertToSeatBid(storedResponseDataResult, auctionStoredResponseToImpId), + mapStoredBidResponseIdsToValues(storedResponseDataResult.getIdToStoredResponses(), + impToBidderToStoredBidResponseId))); } - List mergeWithBidderResponses(List bidderResponses, List storedResponses, + private List excludeStoredAuctionResponseImps(List imps, + Map auctionStoredResponseToImpId) { + return imps.stream() + .filter(imp -> !auctionStoredResponseToImpId.containsValue(imp.getId())) + .collect(Collectors.toList()); + } + + List mergeWithBidderResponses(List bidderResponses, + List storedAuctionResponses, List imps) { - if (CollectionUtils.isEmpty(storedResponses)) { + if (CollectionUtils.isEmpty(storedAuctionResponses)) { return bidderResponses; } final Map bidderToResponse = bidderResponses.stream() .collect(Collectors.toMap(BidderResponse::getBidder, Function.identity())); - final Map bidderToSeatBid = storedResponses.stream() + final Map bidderToSeatBid = storedAuctionResponses.stream() .collect(Collectors.toMap(SeatBid::getSeat, Function.identity())); final Map impIdToBidType = imps.stream() .collect(Collectors.toMap(Imp::getId, this::resolveBidType)); @@ -109,129 +118,85 @@ List mergeWithBidderResponses(List bidderRespons .collect(Collectors.toList()); } - private void fillStoredResponseIdsAndRequestingImps(List imps, List requiredRequestImps, - Map storedResponseIdToImpId, - BidderAliases aliases) { - for (final Imp imp : imps) { - final String impId = imp.getId(); - final ObjectNode extImpNode = imp.getExt(); - final ExtImp extImp = getExtImp(extImpNode, impId); - final ExtImpPrebid extImpPrebid = extImp != null ? extImp.getPrebid() : null; - if (extImpPrebid == null) { - requiredRequestImps.add(imp); - continue; - } - - final ExtStoredAuctionResponse storedAuctionResponse = extImpPrebid.getStoredAuctionResponse(); - final String storedAuctionResponseId = storedAuctionResponse != null ? storedAuctionResponse.getId() : null; - if (StringUtils.isNotEmpty(storedAuctionResponseId)) { - storedResponseIdToImpId.put(storedAuctionResponseId, impId); - continue; - } - - resolveStoredBidResponse(requiredRequestImps, storedResponseIdToImpId, aliases, imp, impId, extImpNode, - extImpPrebid); - } + private Map getImpsExtPrebid(List imps) { + return imps.stream() + .collect(Collectors.toMap(Imp::getId, imp -> getExtImp(imp.getExt(), imp.getId()).getPrebid())); } - private ExtImp getExtImp(ObjectNode extImpNode, String impId) { - try { - return mapper.mapper().treeToValue(extImpNode, ExtImp.class); - } catch (JsonProcessingException e) { - throw new InvalidRequestException(String.format("Error decoding bidRequest.imp.ext for impId = %s : %s", - impId, e.getMessage())); - } + private Map getAuctionStoredResponses(Map extImpPrebids) { + return extImpPrebids.entrySet().stream() + .map(impIdToExtPrebid -> Tuple2.of(impIdToExtPrebid.getKey(), + extractAuctionStoredResponseId(impIdToExtPrebid.getValue()))) + .filter(impIdToStoredResponseId -> impIdToStoredResponseId.getRight() != null) + .collect(Collectors.toMap(Tuple2::getRight, Tuple2::getLeft)); } - private void resolveStoredBidResponse(List requiredRequestImps, Map storedResponseIdToImpId, - BidderAliases aliases, Imp imp, String impId, - ObjectNode extImpNode, ExtImpPrebid extImpPrebid) { - - final List storedBidResponse = extImpPrebid.getStoredBidResponse(); - final Map bidderToStoredId = storedBidResponse != null - ? getBidderToStoredResponseId(storedBidResponse, impId) - : Collections.emptyMap(); - if (!bidderToStoredId.isEmpty()) { - final Imp resolvedBiddersImp = removeStoredResponseBidders(imp, extImpNode, bidderToStoredId.keySet()); - if (hasValidBidder(aliases, resolvedBiddersImp)) { - requiredRequestImps.add(resolvedBiddersImp); - } - storedResponseIdToImpId.putAll(bidderToStoredId.values().stream() - .collect(Collectors.toMap(Function.identity(), id -> impId))); - } else { - requiredRequestImps.add(imp); - } + private String extractAuctionStoredResponseId(ExtImpPrebid extImpPrebid) { + final ExtStoredAuctionResponse storedAuctionResponse = extImpPrebid.getStoredAuctionResponse(); + return storedAuctionResponse != null ? storedAuctionResponse.getId() : null; } - private Map getBidderToStoredResponseId(List extStoredBidResponses, - String impId) { - final Map bidderToStoredResponseId = new HashMap<>(); - for (final ExtStoredBidResponse extStoredBidResponse : extStoredBidResponses) { - final String bidder = extStoredBidResponse.getBidder(); - if (StringUtils.isEmpty(bidder)) { - throw new InvalidRequestException(String.format( - "Bidder was not defined for imp.ext.prebid.storedBidResponse for imp with id %s", impId)); - } - - final String id = extStoredBidResponse.getId(); - if (StringUtils.isEmpty(id)) { - throw new InvalidRequestException(String.format( - "Id was not defined for imp.ext.prebid.storedBidResponse for imp with id %s", impId)); - } - - bidderToStoredResponseId.put(bidder, id); + private Map> getStoredBidResponses(Map extImpPrebids, + List imps) { + // PBS supports stored bid response only for requests with single impression, but it can be changed in future + if (imps.size() != 1) { + return Collections.emptyMap(); } - return bidderToStoredResponseId; - } - private Imp removeStoredResponseBidders(Imp imp, ObjectNode extImp, Set bidders) { - boolean isUpdated = false; - for (final String bidder : bidders) { - if (extImp.hasNonNull(bidder)) { - extImp.remove(bidder); - isUpdated = true; - } - } - return isUpdated ? imp.toBuilder().ext(extImp).build() : imp; - } + final Set impsIds = imps.stream().map(Imp::getId).collect(Collectors.toSet()); - private boolean hasValidBidder(BidderAliases aliases, Imp resolvedBiddersImp) { - return asStream(resolvedBiddersImp.getExt().fieldNames()) - .anyMatch(bidder -> !Objects.equals(bidder, PREBID_EXT) && !Objects.equals(bidder, CONTEXT_EXT) - && isValidBidder(bidder, aliases)); + return extImpPrebids.entrySet().stream() + .filter(impIdToExtPrebid -> impsIds.contains(impIdToExtPrebid.getKey())) + .filter(impIdToExtPrebid -> CollectionUtils + .isNotEmpty(impIdToExtPrebid.getValue().getStoredBidResponse())) + .collect(Collectors.toMap(Map.Entry::getKey, + impIdToStoredResponses -> + resolveStoredBidResponse(impIdToStoredResponses.getValue().getStoredBidResponse()))); } - private Stream asStream(Iterator iterator) { - final Iterable iterable = () -> iterator; - return StreamSupport.stream(iterable.spliterator(), false); + private ExtImp getExtImp(ObjectNode extImpNode, String impId) { + try { + return mapper.mapper().treeToValue(extImpNode, ExtImp.class); + } catch (JsonProcessingException e) { + throw new InvalidRequestException(String.format( + "Error decoding bidRequest.imp.ext for impId = %s : %s", impId, e.getMessage())); + } } - private boolean isValidBidder(String bidder, BidderAliases aliases) { - return bidderCatalog.isValidName(bidder) || aliases.isAliasDefined(bidder); + private Map resolveStoredBidResponse(List storedBidResponse) { + return storedBidResponse.stream() + .collect(Collectors.toMap(ExtStoredBidResponse::getBidder, ExtStoredBidResponse::getId)); } private List convertToSeatBid(StoredResponseDataResult storedResponseDataResult, - Map storedResponseIdToImpId) { + Map auctionStoredResponses) { final List resolvedSeatBids = new ArrayList<>(); - for (final Map.Entry idToRowSeatBid : storedResponseDataResult.getStoredSeatBid().entrySet()) { - final String id = idToRowSeatBid.getKey(); - final String impId = storedResponseIdToImpId.get(id); - final String rowSeatBid = idToRowSeatBid.getValue(); - try { - final List seatBids = mapper.mapper().readValue(rowSeatBid, SEATBID_LIST_TYPEREFERENCE); - - validateStoredSeatBid(seatBids); - - resolvedSeatBids.addAll(seatBids.stream() - .map(seatBid -> updateSeatBidBids(seatBid, impId)) - .collect(Collectors.toList())); - } catch (IOException e) { - throw new InvalidRequestException(String.format("Can't parse Json for stored response with id %s", id)); + final Map idToStoredResponses = storedResponseDataResult.getIdToStoredResponses(); + for (final Map.Entry storedIdToImpId : auctionStoredResponses.entrySet()) { + final String id = storedIdToImpId.getKey(); + final String impId = storedIdToImpId.getValue(); + final String rowSeatBid = idToStoredResponses.get(id); + if (rowSeatBid == null) { + throw new InvalidRequestException(String.format("Failed to fetch stored auction response for" + + " impId = %s and storedAuctionResponse id = %s.", impId, id)); } + final List seatBids = parseSeatBid(id, rowSeatBid); + validateStoredSeatBid(seatBids); + resolvedSeatBids.addAll(seatBids.stream() + .map(seatBid -> updateSeatBidBids(seatBid, impId)) + .collect(Collectors.toList())); } return mergeSameBidderSeatBid(resolvedSeatBids); } + private List parseSeatBid(String id, String rowSeatBid) { + try { + return mapper.mapper().readValue(rowSeatBid, SEATBID_LIST_TYPE); + } catch (IOException e) { + throw new InvalidRequestException(String.format("Can't parse Json for stored response with id %s", id)); + } + } + private SeatBid updateSeatBidBids(SeatBid seatBid, String impId) { return seatBid.toBuilder().bid(updateBidsWithImpId(seatBid.getBid(), impId)).build(); } @@ -271,6 +236,18 @@ private SeatBid makeMergedSeatBid(String seat, List storedSeatBids) { .build(); } + private Map> mapStoredBidResponseIdsToValues( + Map idToStoredResponses, + Map> impToBidderToStoredBidResponseId) { + + return impToBidderToStoredBidResponseId.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> entry.getValue().entrySet().stream() + .filter(bidderToId -> idToStoredResponses.containsKey(bidderToId.getValue())) + .collect(Collectors.toMap(Map.Entry::getKey, + bidderToId -> idToStoredResponses.get(bidderToId.getValue()))))); + } + private BidderResponse makeBidderResponse(BidderResponse bidderResponse, SeatBid seatBid, Map impIdToBidType) { if (bidderResponse != null) { diff --git a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java index 8ea9ea42d92..50f459142a2 100644 --- a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java +++ b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java @@ -1,11 +1,8 @@ package org.prebid.server.auction; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; +import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.exception.PreBidException; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; -import org.prebid.server.proto.response.Bid; import java.math.BigDecimal; import java.util.ArrayList; @@ -30,8 +27,6 @@ */ public class TargetingKeywordsCreator { - private static final Logger logger = LoggerFactory.getLogger(TargetingKeywordsCreator.class); - /** * Exists to support the Prebid Universal Creative. If it exists, the only legal value is mobile-app. * It will exist only if the incoming bidRequest defiend request.app instead of request.site. @@ -76,9 +71,17 @@ public class TargetingKeywordsCreator { */ private static final String HB_CACHE_PATH_KEY = "hb_cache_path"; + /** + * Stores bid's format. For example "video" or "banner". + */ + private static final String HB_FORMAT_KEY = "hb_format"; + + private static final String DEFAULT_CPM = "0.0"; + private final PriceGranularity priceGranularity; private final boolean includeWinners; private final boolean includeBidderKeys; + private final boolean includeFormat; private final boolean isApp; private final int truncateAttrChars; private final String cacheHost; @@ -88,6 +91,7 @@ public class TargetingKeywordsCreator { private TargetingKeywordsCreator(PriceGranularity priceGranularity, boolean includeWinners, boolean includeBidderKeys, + boolean includeFormat, boolean isApp, int truncateAttrChars, String cacheHost, @@ -97,6 +101,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, this.priceGranularity = priceGranularity; this.includeWinners = includeWinners; this.includeBidderKeys = includeBidderKeys; + this.includeFormat = includeFormat; this.isApp = isApp; this.truncateAttrChars = truncateAttrChars; this.cacheHost = cacheHost; @@ -110,6 +115,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranularity, boolean includeWinners, boolean includeBidderKeys, + boolean includeFormat, boolean isApp, int truncateAttrChars, String cacheHost, @@ -120,6 +126,7 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul PriceGranularity.createFromExtPriceGranularity(extPriceGranularity), includeWinners, includeBidderKeys, + includeFormat, isApp, truncateAttrChars, cacheHost, @@ -127,78 +134,25 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul resolver); } - /** - * Creates {@link TargetingKeywordsCreator} for string price granularity representation. - */ - public static TargetingKeywordsCreator create(String stringPriceGranularity, - boolean includeWinners, - boolean includeBidderKeys, - boolean isApp, - int truncateAttrChars) { - - return new TargetingKeywordsCreator( - convertToCustomPriceGranularity(stringPriceGranularity), - includeWinners, - includeBidderKeys, - isApp, - truncateAttrChars, - null, - null, - null); - } - - /** - * Converts string price granularity value to custom view. - * In case of invalid string value returns null. In case of null, returns default custom value. - */ - private static PriceGranularity convertToCustomPriceGranularity(String stringPriceGranularity) { - if (stringPriceGranularity == null) { - return PriceGranularity.DEFAULT; - } - - try { - return PriceGranularity.createFromString(stringPriceGranularity); - } catch (PreBidException e) { - logger.error("Price range granularity error: ''{0}'' is not a recognized granularity", - stringPriceGranularity); - } - return null; - } - /** * Creates map of keywords for the given {@link Bid}. */ - public Map makeFor(Bid bid, boolean winningBid) { - return truncateKeys(makeFor( - bid.getBidder(), - winningBid, - bid.getPrice(), - StringUtils.EMPTY, - bid.getWidth(), - bid.getHeight(), - bid.getCacheId(), - null, - bid.getDealId())); - } - - /** - * Creates map of keywords for the given {@link com.iab.openrtb.response.Bid}. - */ - Map makeFor(com.iab.openrtb.response.Bid bid, + Map makeFor(Bid bid, String bidder, boolean winningBid, String cacheId, + String format, String vastCacheId) { final Map keywords = makeFor( bidder, winningBid, bid.getPrice(), - "0.0", bid.getW(), bid.getH(), cacheId, vastCacheId, + format, bid.getDealid()); if (resolver == null) { @@ -217,17 +171,17 @@ Map makeFor(com.iab.openrtb.response.Bid bid, private Map makeFor(String bidder, boolean winningBid, BigDecimal price, - String defaultCpm, Integer width, Integer height, String cacheId, String vastCacheId, + String format, String dealId) { final KeywordMap keywordMap = new KeywordMap(bidder, winningBid, includeWinners, includeBidderKeys, Collections.emptySet()); - final String roundedCpm = isPriceGranularityValid() ? CpmRange.fromCpm(price, priceGranularity) : defaultCpm; + final String roundedCpm = isPriceGranularityValid() ? CpmRange.fromCpm(price, priceGranularity) : DEFAULT_CPM; keywordMap.put(HB_PB_KEY, roundedCpm); keywordMap.put(HB_BIDDER_KEY, bidder); @@ -247,6 +201,11 @@ private Map makeFor(String bidder, keywordMap.put(HB_CACHE_HOST_KEY, cacheHost); keywordMap.put(HB_CACHE_PATH_KEY, cachePath); } + if (StringUtils.isNotBlank(format) && includeFormat) { + keywordMap.put(HB_FORMAT_KEY, format); + } + + // get Line Item by dealId if (StringUtils.isNotBlank(dealId)) { keywordMap.put(HB_DEAL_KEY, dealId); } @@ -278,7 +237,8 @@ private static String sizeFrom(Integer width, Integer height) { private Map truncateKeys(Map keyValues) { return truncateAttrChars > 0 ? keyValues.entrySet().stream() - .collect(Collectors.toMap(keyValue -> truncateKey(keyValue.getKey()), Map.Entry::getValue)) + .collect(Collectors + .toMap(keyValue -> truncateKey(keyValue.getKey()), Map.Entry::getValue, (key1, key2) -> key1)) : keyValues; } diff --git a/src/main/java/org/prebid/server/auction/TimeoutResolver.java b/src/main/java/org/prebid/server/auction/TimeoutResolver.java index 29b7042dee7..575a6809b78 100644 --- a/src/main/java/org/prebid/server/auction/TimeoutResolver.java +++ b/src/main/java/org/prebid/server/auction/TimeoutResolver.java @@ -24,7 +24,7 @@ public TimeoutResolver(long defaultTimeout, long maxTimeout, long timeoutAdjustm /** * Resolves timeout according to given in request and pre-configured default and max values. */ - long resolve(Long requestTimeout) { + public long resolve(Long requestTimeout) { final long result; if (requestTimeout == null) { diff --git a/src/main/java/org/prebid/server/auction/VideoRequestFactory.java b/src/main/java/org/prebid/server/auction/VideoRequestFactory.java deleted file mode 100644 index 9970725e58a..00000000000 --- a/src/main/java/org/prebid/server/auction/VideoRequestFactory.java +++ /dev/null @@ -1,156 +0,0 @@ -package org.prebid.server.auction; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.video.BidRequestVideo; -import com.iab.openrtb.request.video.Pod; -import com.iab.openrtb.request.video.PodError; -import com.iab.openrtb.request.video.Podconfig; -import io.vertx.core.Future; -import io.vertx.core.buffer.Buffer; -import io.vertx.ext.web.RoutingContext; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.auction.model.WithPodErrors; -import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.MetricName; -import org.prebid.server.util.HttpUtil; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -public class VideoRequestFactory { - - private final int maxRequestSize; - private final boolean enforceStoredRequest; - - private final VideoStoredRequestProcessor storedRequestProcessor; - private final AuctionRequestFactory auctionRequestFactory; - private final TimeoutResolver timeoutResolver; - private final JacksonMapper mapper; - - public VideoRequestFactory(int maxRequestSize, boolean enforceStoredRequest, - VideoStoredRequestProcessor storedRequestProcessor, - AuctionRequestFactory auctionRequestFactory, TimeoutResolver timeoutResolver, - JacksonMapper mapper) { - this.enforceStoredRequest = enforceStoredRequest; - this.maxRequestSize = maxRequestSize; - this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); - this.auctionRequestFactory = Objects.requireNonNull(auctionRequestFactory); - this.timeoutResolver = Objects.requireNonNull(timeoutResolver); - this.mapper = Objects.requireNonNull(mapper); - } - - /** - * Creates {@link AuctionContext} and {@link List} of {@link PodError} based on {@link RoutingContext}. - */ - public Future> fromRequest(RoutingContext routingContext, long startTime) { - - final BidRequestVideo incomingBidRequest; - try { - incomingBidRequest = parseRequest(routingContext); - } catch (InvalidRequestException e) { - return Future.failedFuture(e); - } - - final String storedRequestId = incomingBidRequest.getStoredrequestid(); - if (StringUtils.isBlank(storedRequestId) && enforceStoredRequest) { - return Future.failedFuture(new InvalidRequestException("Unable to find required stored request id")); - } - - final Set podConfigIds = podConfigIds(incomingBidRequest); - return createBidRequest(routingContext, incomingBidRequest, storedRequestId, podConfigIds) - .compose(bidRequestToPodError -> auctionRequestFactory.toAuctionContext( - routingContext, - bidRequestToPodError.getData(), - MetricName.video, - new ArrayList<>(), - startTime, - timeoutResolver) - .map(auctionContext -> WithPodErrors.of(auctionContext, bidRequestToPodError.getPodErrors()))); - } - - /** - * Parses request body to {@link BidRequestVideo}. - *

- * Throws {@link InvalidRequestException} if body is empty, exceeds max request size or couldn't be deserialized. - */ - private BidRequestVideo parseRequest(RoutingContext context) { - final Buffer body = context.getBody(); - if (body == null) { - throw new InvalidRequestException("Incoming request has no body"); - } - - if (body.length() > maxRequestSize) { - throw new InvalidRequestException( - String.format("Request size exceeded max size of %d bytes.", maxRequestSize)); - } - - try { - final BidRequestVideo bidRequestVideo = mapper.decodeValue(body, BidRequestVideo.class); - return insertDeviceUa(context, bidRequestVideo); - } catch (DecodeException e) { - throw new InvalidRequestException(e.getMessage()); - } - } - - private BidRequestVideo insertDeviceUa(RoutingContext context, BidRequestVideo bidRequestVideo) { - final Device device = bidRequestVideo.getDevice(); - final String deviceUa = device != null ? device.getUa() : null; - if (StringUtils.isBlank(deviceUa)) { - final String userAgentHeader = context.request().getHeader(HttpUtil.USER_AGENT_HEADER); - if (StringUtils.isEmpty(userAgentHeader)) { - throw new InvalidRequestException("Device.UA and User-Agent Header is not presented"); - } - final Device.DeviceBuilder deviceBuilder = device == null ? Device.builder() : device.toBuilder(); - - return bidRequestVideo.toBuilder() - .device(deviceBuilder - .ua(userAgentHeader) - .build()) - .build(); - } - return bidRequestVideo; - } - - private static Set podConfigIds(BidRequestVideo incomingBidRequest) { - final Podconfig podconfig = incomingBidRequest.getPodconfig(); - if (podconfig != null && CollectionUtils.isNotEmpty(podconfig.getPods())) { - return podconfig.getPods().stream() - .map(Pod::getConfigId) - .filter(Objects::nonNull) - .map(String::valueOf) - .collect(Collectors.toSet()); - } else { - return Collections.emptySet(); - } - } - - private Future> createBidRequest(RoutingContext routingContext, - BidRequestVideo bidRequestVideo, - String storedVideoId, - Set podConfigIds) { - return storedRequestProcessor.processVideoRequest(storedVideoId, podConfigIds, bidRequestVideo) - .map(bidRequestToErrors -> fillImplicitParameters(routingContext, bidRequestToErrors)) - .map(this::validateRequest); - } - - private WithPodErrors validateRequest(WithPodErrors requestToPodErrors) { - final BidRequest bidRequest = auctionRequestFactory.validateRequest(requestToPodErrors.getData()); - return WithPodErrors.of(bidRequest, requestToPodErrors.getPodErrors()); - } - - private WithPodErrors fillImplicitParameters(RoutingContext routingContext, - WithPodErrors bidRequestToErrors) { - final BidRequest bidRequest = auctionRequestFactory - .fillImplicitParameters(bidRequestToErrors.getData(), routingContext, timeoutResolver); - return WithPodErrors.of(bidRequest, bidRequestToErrors.getPodErrors()); - } -} diff --git a/src/main/java/org/prebid/server/auction/VideoResponseFactory.java b/src/main/java/org/prebid/server/auction/VideoResponseFactory.java index 2a9083d2385..d81fe03766c 100644 --- a/src/main/java/org/prebid/server/auction/VideoResponseFactory.java +++ b/src/main/java/org/prebid/server/auction/VideoResponseFactory.java @@ -1,25 +1,25 @@ package org.prebid.server.auction; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.video.PodError; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.math.NumberUtils; +import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtAdPod; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; +import org.prebid.server.proto.openrtb.ext.response.ExtModules; import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; import org.prebid.server.proto.openrtb.ext.response.ExtResponseVideoTargeting; +import org.prebid.server.proto.response.ExtAmpVideoPrebid; +import org.prebid.server.proto.response.ExtAmpVideoResponse; import org.prebid.server.proto.response.VideoResponse; import java.util.ArrayList; @@ -32,12 +32,7 @@ public class VideoResponseFactory { - private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = - new TypeReference>() { - }; - private static final TypeReference EXT_BID_RESPONSE_TYPE_REFERENCE = - new TypeReference() { - }; + public static final String PREBID_EXT = "prebid"; private final JacksonMapper mapper; @@ -45,7 +40,11 @@ public VideoResponseFactory(JacksonMapper mapper) { this.mapper = mapper; } - public VideoResponse toVideoResponse(BidRequest bidRequest, BidResponse bidResponse, List podErrors) { + public VideoResponse toVideoResponse( + AuctionContext auctionContext, + BidResponse bidResponse, + List podErrors) { + final List bids = bidsFrom(bidResponse); final boolean anyBidsReturned = CollectionUtils.isNotEmpty(bids); final List adPods = adPodsWithTargetingFrom(bids); @@ -59,16 +58,16 @@ public VideoResponse toVideoResponse(BidRequest bidRequest, BidResponse bidRespo final ExtResponseDebug extResponseDebug; final Map> errors; // Fetch debug and errors information from response if requested - if (isDebugEnabled(bidRequest)) { - final ExtBidResponse extBidResponse = extResponseFrom(bidResponse); + if (auctionContext.getDebugContext().isDebugEnabled()) { + final ExtBidResponse extBidResponse = bidResponse.getExt(); - extResponseDebug = extResponseDebugFrom(extBidResponse); - errors = errorsFrom(extBidResponse); + extResponseDebug = extBidResponse != null ? extBidResponse.getDebug() : null; + errors = extBidResponse != null ? extBidResponse.getErrors() : null; } else { extResponseDebug = null; errors = null; } - return VideoResponse.of(adPods, extResponseDebug, errors, null); + return VideoResponse.of(adPods, extResponseDebug, errors, extResponseFrom(bidResponse)); } private static List bidsFrom(BidResponse bidResponse) { @@ -88,7 +87,7 @@ private List adPodsWithTargetingFrom(List bids) { final List adPods = new ArrayList<>(); for (Bid bid : bids) { final Map targeting = targeting(bid); - if (targeting.get("hb_uuid") == null) { + if (findByPrefix(targeting, "hb_uuid") == null) { continue; } final String impId = bid.getImpid(); @@ -99,9 +98,9 @@ private List adPodsWithTargetingFrom(List bids) { final Integer podId = Integer.parseInt(podIdString); final ExtResponseVideoTargeting videoTargeting = ExtResponseVideoTargeting.of( - targeting.get("hb_pb"), - targeting.get("hb_pb_cat_dur"), - targeting.get("hb_uuid")); + findByPrefix(targeting, "hb_pb"), + findByPrefix(targeting, "hb_pb_cat_dur"), + findByPrefix(targeting, "hb_uuid")); ExtAdPod adPod = adPods.stream() .filter(extAdPod -> extAdPod.getPodid().equals(podId)) @@ -118,50 +117,41 @@ private List adPodsWithTargetingFrom(List bids) { } private Map targeting(Bid bid) { - final ExtPrebid extBid; + final ObjectNode bidExt = bid.getExt(); + if (bidExt == null || !bidExt.hasNonNull(PREBID_EXT)) { + return Collections.emptyMap(); + } + + final ExtBidPrebid extBidPrebid; try { - extBid = mapper.mapper().convertValue(bid.getExt(), EXT_PREBID_TYPE_REFERENCE); + extBidPrebid = mapper.mapper().convertValue(bidExt.get(PREBID_EXT), ExtBidPrebid.class); } catch (IllegalArgumentException e) { return Collections.emptyMap(); } - final ExtBidPrebid extBidPrebid = extBid != null ? extBid.getPrebid() : null; final Map targeting = extBidPrebid != null ? extBidPrebid.getTargeting() : null; return targeting != null ? targeting : Collections.emptyMap(); } + private static String findByPrefix(Map keyToValue, String prefix) { + return keyToValue.entrySet().stream() + .filter(keyAndValue -> keyAndValue.getKey().startsWith(prefix)) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + } + private static List adPodsWithErrors(List podErrors) { return podErrors.stream() .map(podError -> ExtAdPod.of(podError.getPodId(), null, podError.getPodErrors())) .collect(Collectors.toList()); } - /** - * Determines debug flag from {@link BidRequest}. - */ - private boolean isDebugEnabled(BidRequest bidRequest) { - if (Objects.equals(bidRequest.getTest(), 1)) { - return true; - } - final ExtRequest extRequest = bidRequest.getExt(); - final ExtRequestPrebid extRequestPrebid = extRequest != null ? extRequest.getPrebid() : null; - return extRequestPrebid != null && Objects.equals(extRequestPrebid.getDebug(), 1); - } - - private ExtBidResponse extResponseFrom(BidResponse bidResponse) { - try { - return mapper.mapper().convertValue(bidResponse.getExt(), EXT_BID_RESPONSE_TYPE_REFERENCE); - } catch (IllegalArgumentException e) { - throw new PreBidException( - String.format("Critical error while unpacking Video bid response: %s", e.getMessage()), e); - } - } - - private static ExtResponseDebug extResponseDebugFrom(ExtBidResponse extBidResponse) { - return extBidResponse != null ? extBidResponse.getDebug() : null; - } + private static ExtAmpVideoResponse extResponseFrom(BidResponse bidResponse) { + final ExtBidResponse ext = bidResponse.getExt(); + final ExtBidResponsePrebid extPrebid = ext != null ? ext.getPrebid() : null; + final ExtModules extModules = extPrebid != null ? extPrebid.getModules() : null; - private static Map> errorsFrom(ExtBidResponse extBidResponse) { - return extBidResponse != null ? extBidResponse.getErrors() : null; + return extModules != null ? ExtAmpVideoResponse.of(ExtAmpVideoPrebid.of(extModules)) : null; } } diff --git a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java index 1a82530aa56..83f3c957393 100644 --- a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java @@ -16,6 +16,7 @@ import com.iab.openrtb.request.video.PodError; import com.iab.openrtb.request.video.Podconfig; import io.vertx.core.Future; +import io.vertx.core.file.FileSystem; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; @@ -27,6 +28,7 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -36,13 +38,11 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.StoredDataResult; -import org.prebid.server.util.JsonMergeUtil; import org.prebid.server.validation.VideoRequestValidator; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -50,78 +50,128 @@ import java.util.stream.Collectors; /** - * Executes stored request processing. + * Executes stored request processing for video. */ public class VideoStoredRequestProcessor { private static final Logger logger = LoggerFactory.getLogger(VideoStoredRequestProcessor.class); private static final String DEFAULT_CURRENCY = "USD"; - private static final String DEFAULT_BUYERUID = "appnexus"; - private final ApplicationSettings applicationSettings; - private final VideoRequestValidator validator; private final boolean enforceStoredRequest; private final List blacklistedAccounts; + private final long defaultTimeout; + private final String currency; private final BidRequest defaultBidRequest; + private final ApplicationSettings applicationSettings; + private final VideoRequestValidator validator; private final Metrics metrics; private final TimeoutResolver timeoutResolver; private final TimeoutFactory timeoutFactory; - private final long defaultTimeout; - private final String currency; private final JacksonMapper mapper; - private JsonMergeUtil jsonMergeUtil; - - public VideoStoredRequestProcessor(ApplicationSettings applicationSettings, VideoRequestValidator validator, - boolean enforceStoredRequest, List blacklistedAccounts, - BidRequest defaultBidRequest, Metrics metrics, TimeoutFactory timeoutFactory, - TimeoutResolver timeoutResolver, long defaultTimeout, String adServerCurrency, - JacksonMapper mapper) { - this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.validator = Objects.requireNonNull(validator); + private final JsonMerger jsonMerger; + + private VideoStoredRequestProcessor(boolean enforceStoredRequest, + List blacklistedAccounts, + long defaultTimeout, + String adServerCurrency, + BidRequest defaultBidRequest, + ApplicationSettings applicationSettings, + VideoRequestValidator validator, + Metrics metrics, + TimeoutFactory timeoutFactory, + TimeoutResolver timeoutResolver, + JacksonMapper mapper, + JsonMerger jsonMerger) { + this.enforceStoredRequest = enforceStoredRequest; this.blacklistedAccounts = blacklistedAccounts; - this.defaultBidRequest = Objects.requireNonNull(defaultBidRequest); - this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - this.timeoutResolver = Objects.requireNonNull(timeoutResolver); - this.metrics = Objects.requireNonNull(metrics); this.defaultTimeout = defaultTimeout; this.currency = StringUtils.isBlank(adServerCurrency) ? DEFAULT_CURRENCY : adServerCurrency; - this.mapper = Objects.requireNonNull(mapper); + this.defaultBidRequest = defaultBidRequest; + this.applicationSettings = applicationSettings; + this.validator = validator; + this.metrics = metrics; + this.timeoutFactory = timeoutFactory; + this.timeoutResolver = timeoutResolver; + this.mapper = mapper; + this.jsonMerger = jsonMerger; + } - jsonMergeUtil = new JsonMergeUtil(mapper); + public static VideoStoredRequestProcessor create(boolean enforceStoredRequest, + List blacklistedAccounts, + long defaultTimeout, + String adServerCurrency, + String defaultBidRequestPath, + FileSystem fileSystem, + ApplicationSettings applicationSettings, + VideoRequestValidator validator, + Metrics metrics, + TimeoutFactory timeoutFactory, + TimeoutResolver timeoutResolver, + JacksonMapper mapper, + JsonMerger jsonMerger) { + + return new VideoStoredRequestProcessor( + enforceStoredRequest, + Objects.requireNonNull(blacklistedAccounts), + defaultTimeout, + adServerCurrency, + readBidRequest( + defaultBidRequestPath, Objects.requireNonNull(fileSystem), Objects.requireNonNull(mapper)), + Objects.requireNonNull(applicationSettings), + Objects.requireNonNull(validator), + Objects.requireNonNull(metrics), + Objects.requireNonNull(timeoutFactory), + Objects.requireNonNull(timeoutResolver), + Objects.requireNonNull(mapper), + Objects.requireNonNull(jsonMerger)); } /** * Fetches ParsedStoredDataResult<BidRequestVideo, Imp> from stored request. */ - Future> processVideoRequest(String storedBidRequestId, Set podIds, - BidRequestVideo receivedRequest) { - final Set storedRequestIds = new HashSet<>(); - if (StringUtils.isNotBlank(storedBidRequestId)) { - storedRequestIds.add(storedBidRequestId); - } - return applicationSettings.getVideoStoredData(storedRequestIds, podIds, timeoutFactory.create(defaultTimeout)) + public Future> processVideoRequest(String accountId, + String storedBidRequestId, + Set podIds, + BidRequestVideo videoRequest) { + + final Set storedRequestIds = StringUtils.isNotBlank(storedBidRequestId) + ? Collections.singleton(storedBidRequestId) + : Collections.emptySet(); + + return applicationSettings.getVideoStoredData(accountId, storedRequestIds, podIds, + timeoutFactory.create(defaultTimeout)) .compose(storedDataResult -> updateMetrics(storedDataResult, storedRequestIds, podIds)) - .map(storedData -> mergeToBidRequest(storedData, receivedRequest, storedBidRequestId)) + .map(storedData -> mergeToBidRequest(storedData, videoRequest, storedBidRequestId)) .recover(exception -> Future.failedFuture(new InvalidRequestException( String.format("Stored request fetching failed: %s", exception.getMessage())))); } - private Future updateMetrics(StoredDataResult storedDataResult, Set requestIds, + private static BidRequest readBidRequest(String defaultBidRequestPath, FileSystem fileSystem, + JacksonMapper mapper) { + + return StringUtils.isNotBlank(defaultBidRequestPath) + ? mapper.decodeValue(fileSystem.readFileBlocking(defaultBidRequestPath), BidRequest.class) + : null; + } + + private Future updateMetrics(StoredDataResult storedDataResult, + Set requestIds, Set impIds) { requestIds.forEach( id -> metrics.updateStoredRequestMetric(storedDataResult.getStoredIdToRequest().containsKey(id))); - impIds.forEach(id -> metrics.updateStoredImpsMetric(storedDataResult.getStoredIdToImp().containsKey(id))); + impIds.forEach( + id -> metrics.updateStoredImpsMetric(storedDataResult.getStoredIdToImp().containsKey(id))); return Future.succeededFuture(storedDataResult); } private WithPodErrors mergeToBidRequest(StoredDataResult storedResult, - BidRequestVideo receivedRequest, + BidRequestVideo videoRequest, String storedBidRequestId) { - final BidRequestVideo mergedStoredRequest = mergeBidRequest(receivedRequest, storedBidRequestId, storedResult); + final BidRequestVideo mergedStoredRequest = mergeBidRequest(videoRequest, storedBidRequestId, storedResult); validator.validateStoredBidRequest(mergedStoredRequest, enforceStoredRequest, blacklistedAccounts); final Podconfig podconfig = mergedStoredRequest.getPodconfig(); @@ -134,20 +184,24 @@ private WithPodErrors mergeToBidRequest(StoredDataResult storedResul return WithPodErrors.of(bidRequest, impsToPodErrors.getPodErrors()); } - private BidRequestVideo mergeBidRequest(BidRequestVideo originalRequest, String storedRequestId, + private BidRequestVideo mergeBidRequest(BidRequestVideo originalRequest, + String storedRequestId, StoredDataResult storedDataResult) { + final String storedRequest = storedDataResult.getStoredIdToRequest().get(storedRequestId); if (enforceStoredRequest && StringUtils.isBlank(storedRequest)) { throw new InvalidRequestException("Stored request is enforced but not found"); } return StringUtils.isNotBlank(storedRequest) - ? jsonMergeUtil.merge(originalRequest, storedRequest, storedRequestId, BidRequestVideo.class) + ? jsonMerger.merge(originalRequest, storedRequest, storedRequestId, BidRequestVideo.class) : originalRequest; } - private WithPodErrors> mergeStoredImps(Podconfig podconfig, Video video, + private WithPodErrors> mergeStoredImps(Podconfig podconfig, + Video video, Map storedImpIdToJsonImp) { + final Map storedImpIdToImp = storedIdToStoredImp(storedImpIdToJsonImp); final WithPodErrors> validPodsToPodErrors = validator.validPods(podconfig, storedImpIdToImp.keySet()); final List validPods = validPodsToPodErrors.getData(); @@ -179,11 +233,14 @@ private Map storedIdToStoredImp(Map storedIdToImp) return idToImps; } - private static List createImps(Map idToImps, List validPods, Podconfig podconfig, + private static List createImps(Map idToImps, + List validPods, + Podconfig podconfig, Video video) { + final List durationRangeSec = podconfig.getDurationRangeSec(); final Boolean requireExactDuration = podconfig.getRequireExactDuration(); - final Tuple2 maxMin = maxMin(durationRangeSec); + final Tuple2 maxMin = minMax(durationRangeSec); final ArrayList imps = new ArrayList<>(); for (Pod pod : validPods) { @@ -203,7 +260,7 @@ private static List createImps(Map idToImps, List validPo Integer maxDuration; Integer minDuration = null; if (BooleanUtils.isTrue(requireExactDuration)) { - int durationIndex = (i + 1) / impDivNumber; + int durationIndex = i / impDivNumber; if (durationIndex > durationRangeSec.size() - 1) { durationIndex = durationRangeSec.size() - 1; } @@ -222,7 +279,7 @@ private static List createImps(Map idToImps, List validPo } final Imp imp = storedImp.toBuilder() .id(String.format("%d_%d", pod.getPodId(), i)) - .video(updateVideo(video, maxDuration, minDuration)) + .video(updateVideo(video, minDuration, maxDuration)) .build(); imps.add(imp); } @@ -230,29 +287,32 @@ private static List createImps(Map idToImps, List validPo return imps; } - private static Tuple2 maxMin(List values) { - int max = Integer.MIN_VALUE; + private static Tuple2 minMax(List values) { int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; for (Integer value : values) { - max = Math.max(max, value); min = Math.min(min, value); + max = Math.max(max, value); } - return Tuple2.of(max, min); + return Tuple2.of(min, max); } - private static Video updateVideo(Video video, Integer maxDuration, Integer minDuration) { + private static Video updateVideo(Video video, Integer minDuration, Integer maxDuration) { return video.toBuilder() - .maxduration(maxDuration) .minduration(minDuration) + .maxduration(maxDuration) .build(); } - private BidRequest mergeWithDefaultBidRequest(BidRequestVideo validatedVideoRequest, List imps) { - final BidRequest.BidRequestBuilder bidRequestBuilder = defaultBidRequest.toBuilder(); - final Site site = validatedVideoRequest.getSite(); + private BidRequest mergeWithDefaultBidRequest(BidRequestVideo videoRequest, List imps) { + final BidRequest.BidRequestBuilder bidRequestBuilder = defaultBidRequest != null + ? defaultBidRequest.toBuilder() + : BidRequest.builder(); + + final Site site = videoRequest.getSite(); if (site != null) { final Site.SiteBuilder siteBuilder = site.toBuilder(); - final Content content = validatedVideoRequest.getContent(); + final Content content = videoRequest.getContent(); if (content != null) { siteBuilder.content(content); } @@ -260,45 +320,42 @@ private BidRequest mergeWithDefaultBidRequest(BidRequestVideo validatedVideoRequ bidRequestBuilder.site(siteBuilder.build()); } - final App app = validatedVideoRequest.getApp(); + final App app = videoRequest.getApp(); if (app != null) { final App.AppBuilder appBuilder = app.toBuilder(); - final Content content = validatedVideoRequest.getContent(); + final Content content = videoRequest.getContent(); if (content != null) { appBuilder.content(content); } bidRequestBuilder.app(appBuilder.build()); } - final Device device = validatedVideoRequest.getDevice(); + final Device device = videoRequest.getDevice(); if (device != null) { bidRequestBuilder.device(device); } - final User user = validatedVideoRequest.getUser(); + final User user = videoRequest.getUser(); if (user != null) { - final User updatedUser = user.toBuilder() - .buyeruid(DEFAULT_BUYERUID) - .build(); - bidRequestBuilder.user(updatedUser); + bidRequestBuilder.user(user); } - final List bcat = validatedVideoRequest.getBcat(); + final List bcat = videoRequest.getBcat(); if (CollectionUtils.isNotEmpty(bcat)) { bidRequestBuilder.bcat(bcat); } - final List badv = validatedVideoRequest.getBadv(); + final List badv = videoRequest.getBadv(); if (CollectionUtils.isNotEmpty(badv)) { bidRequestBuilder.badv(badv); } - final Regs regs = validatedVideoRequest.getRegs(); + final Regs regs = videoRequest.getRegs(); if (regs != null) { bidRequestBuilder.regs(regs); } - final Long videoTmax = timeoutResolver.resolve(validatedVideoRequest.getTmax()); + final Long videoTmax = timeoutResolver.resolve(videoRequest.getTmax()); bidRequestBuilder.tmax(videoTmax); addRequiredOpenRtbFields(bidRequestBuilder); @@ -306,8 +363,8 @@ private BidRequest mergeWithDefaultBidRequest(BidRequestVideo validatedVideoRequ return bidRequestBuilder .id("bid_id") .imp(imps) - .ext(createBidExtension(validatedVideoRequest)) - .test(validatedVideoRequest.getTest()) + .ext(createExtRequest(videoRequest)) + .test(videoRequest.getTest()) .build(); } @@ -315,9 +372,9 @@ private void addRequiredOpenRtbFields(BidRequest.BidRequestBuilder bidRequestBui bidRequestBuilder.cur(Collections.singletonList(currency)); } - private ExtRequest createBidExtension(BidRequestVideo videoRequest) { - final IncludeBrandCategory includebrandcategory = videoRequest.getIncludebrandcategory(); + private ExtRequest createExtRequest(BidRequestVideo videoRequest) { final ExtIncludeBrandCategory extIncludeBrandCategory; + final IncludeBrandCategory includebrandcategory = videoRequest.getIncludebrandcategory(); if (includebrandcategory != null) { extIncludeBrandCategory = ExtIncludeBrandCategory.of( includebrandcategory.getPrimaryAdserver(), includebrandcategory.getPublisher(), true); @@ -330,14 +387,14 @@ private ExtRequest createBidExtension(BidRequestVideo videoRequest) { durationRangeSec = videoRequest.getPodconfig().getDurationRangeSec(); } - PriceGranularity updatedPriceGranularity = PriceGranularity.createFromString("med"); final PriceGranularity priceGranularity = videoRequest.getPriceGranularity(); - if (priceGranularity != null) { - final Integer precision = priceGranularity.getPrecision(); - if (precision != null && precision != 0) { - updatedPriceGranularity = priceGranularity; - } - } + final Integer precision = priceGranularity != null + ? priceGranularity.getPrecision() + : null; + + PriceGranularity updatedPriceGranularity = precision != null && precision != 0 + ? priceGranularity + : PriceGranularity.createFromString("med"); final ExtRequestTargeting targeting = ExtRequestTargeting.builder() .pricegranularity(mapper.mapper().valueToTree(updatedPriceGranularity)) diff --git a/src/main/java/org/prebid/server/auction/WinningBidComparatorFactory.java b/src/main/java/org/prebid/server/auction/WinningBidComparatorFactory.java new file mode 100644 index 00000000000..350ae19bf0d --- /dev/null +++ b/src/main/java/org/prebid/server/auction/WinningBidComparatorFactory.java @@ -0,0 +1,129 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Pmp; +import com.iab.openrtb.response.Bid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.auction.model.BidInfo; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * Creates {@link Comparator} that compares two {@link BidInfo} arguments for order. + */ +public class WinningBidComparatorFactory { + + private static final Comparator WINNING_BID_PRICE_COMPARATOR = new WinningBidPriceComparator(); + private static final Comparator WINNING_BID_DEAL_COMPARATOR = new WinningBidDealComparator(); + private static final Comparator WINNING_BID_PG_COMPARATOR = new WinningBidPgComparator(); + + private static final Comparator BID_INFO_COMPARATOR = WINNING_BID_PG_COMPARATOR + .thenComparing(WINNING_BID_DEAL_COMPARATOR) + .thenComparing(WINNING_BID_PRICE_COMPARATOR); + + private static final Comparator PREFER_PRICE_COMPARATOR = WINNING_BID_PG_COMPARATOR + .thenComparing(WINNING_BID_PRICE_COMPARATOR); + + public Comparator create(boolean preferDeals) { + return preferDeals + ? BID_INFO_COMPARATOR + : PREFER_PRICE_COMPARATOR; + } + + /** + * Compares two {@link BidInfo} arguments for order based on dealId presence. + * Returns negative integer when first does not have a deal and second has. + * Return positive integer when first has deal and second does not + * Returns zero when both have deals, or both don't have a deal + */ + private static class WinningBidDealComparator implements Comparator { + + @Override + public int compare(BidInfo bidInfo1, BidInfo bidInfo2) { + final boolean isPresentBidDealId1 = bidInfo1.getBid().getDealid() != null; + final boolean isPresentBidDealId2 = bidInfo2.getBid().getDealid() != null; + + if (!Boolean.logicalXor(isPresentBidDealId1, isPresentBidDealId2)) { + return 0; + } + + return isPresentBidDealId1 ? 1 : -1; + } + } + + /** + * Compares two {@link BidInfo} arguments for order based on PG deal priority. + * Returns negative integer when first does not have a pg deal and second has, or when both have a pg deal, + * but first has higher index in deals array that means lower priority. + * Returns positive integer when first has a pg deal and second does not, or when both have a pg deal, + * but first has lower index in deals array that means higher priority. + * Returns zero when both dont have pg deals. + */ + private static class WinningBidPgComparator implements Comparator { + + private final Comparator dealIndexComparator = Comparator.comparingInt(Integer::intValue).reversed(); + + @Override + public int compare(BidInfo bidInfo1, BidInfo bidInfo2) { + final Imp imp = bidInfo1.getCorrespondingImp(); + final Pmp pmp = imp.getPmp(); + final List impDeals = pmp != null ? pmp.getDeals() : null; + + if (CollectionUtils.isEmpty(impDeals)) { + return 0; + } + + final Bid bid1 = bidInfo1.getBid(); + final Bid bid2 = bidInfo2.getBid(); + + int indexOfBidDealId1 = -1; + int indexOfBidDealId2 = -1; + + // search for indexes of deals + for (int i = 0; i < impDeals.size(); i++) { + final String dealId = impDeals.get(i).getId(); + if (Objects.equals(dealId, bid1.getDealid())) { + indexOfBidDealId1 = i; + } + if (Objects.equals(dealId, bid2.getDealid())) { + indexOfBidDealId2 = i; + } + } + + final boolean isPresentImpDealId1 = indexOfBidDealId1 != -1; + final boolean isPresentImpDealId2 = indexOfBidDealId2 != -1; + + final boolean isOneOrBothDealIdNotPresent = !isPresentImpDealId1 || !isPresentImpDealId2; + return isOneOrBothDealIdNotPresent + ? isPresentImpDealId1 ? 1 : -1 // case when no deal IDs found is covered by response validator + : dealIndexComparator.compare(indexOfBidDealId1, indexOfBidDealId2); + } + } + + /** + * Compares two {@link BidInfo} arguments for order based on price. + * Returns negative integer when first has lower price. + * Returns positive integer when first has higher price. + * Returns zero when both have equal price. + */ + private static class WinningBidPriceComparator implements Comparator { + + private static final Comparator PRICE_COMPARATOR = Comparator.comparing(o -> o.getBid().getPrice()); + + @Override + public int compare(BidInfo bidInfo1, BidInfo bidInfo2) { + final Imp imp = bidInfo1.getCorrespondingImp(); + // this should never happen + if (!Objects.equals(imp, bidInfo2.getCorrespondingImp())) { + throw new IllegalStateException( + String.format("Error while determining winning bid: " + + "Multiple bids was found for impId: %s", imp.getId())); + } + + return PRICE_COMPARATOR.compare(bidInfo1, bidInfo2); + } + } +} diff --git a/src/main/java/org/prebid/server/auction/model/AdUnitBid.java b/src/main/java/org/prebid/server/auction/model/AdUnitBid.java deleted file mode 100644 index eab3d588995..00000000000 --- a/src/main/java/org/prebid/server/auction/model/AdUnitBid.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.prebid.server.auction.model; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Format; -import lombok.Builder; -import lombok.Value; -import org.prebid.server.proto.request.Video; -import org.prebid.server.proto.response.MediaType; - -import java.util.List; -import java.util.Set; - -@Builder(toBuilder = true) -@Value -public class AdUnitBid { - - /* Unique code for an adapter to call. */ - String bidderCode; - - List sizes; - - /* Whether this ad will render in the top IFRAME. */ - Integer topframe; // ... really just a boolean 0|1. - - /* 1 = the ad is interstitial or full screen, 0 = not interstitial. */ - Integer instl; // ... really just a boolean 0|1. - - /* Unique code of the ad unit on the page. */ - String adUnitCode; - - String bidId; - - ObjectNode params; - - Video video; - - Set mediaTypes; -} diff --git a/src/main/java/org/prebid/server/auction/model/AdapterRequest.java b/src/main/java/org/prebid/server/auction/model/AdapterRequest.java deleted file mode 100644 index 39168688ab6..00000000000 --- a/src/main/java/org/prebid/server/auction/model/AdapterRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.auction.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class AdapterRequest { - - String bidderCode; - - List adUnitBids; -} diff --git a/src/main/java/org/prebid/server/auction/model/AdapterResponse.java b/src/main/java/org/prebid/server/auction/model/AdapterResponse.java deleted file mode 100644 index 0b0321e51e1..00000000000 --- a/src/main/java/org/prebid/server/auction/model/AdapterResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.prebid.server.auction.model; - -import lombok.AllArgsConstructor; -import lombok.Value; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.BidderStatus; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class AdapterResponse { - - BidderStatus bidderStatus; - - List bids; - - BidderError error; -} diff --git a/src/main/java/org/prebid/server/auction/model/AmpRequest.java b/src/main/java/org/prebid/server/auction/model/AmpRequest.java deleted file mode 100644 index d2eaf5bdab1..00000000000 --- a/src/main/java/org/prebid/server/auction/model/AmpRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.auction.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class AmpRequest { - - String tagId; -} diff --git a/src/main/java/org/prebid/server/auction/model/AuctionContext.java b/src/main/java/org/prebid/server/auction/model/AuctionContext.java index 1870839a7a5..2a9fe99df4f 100644 --- a/src/main/java/org/prebid/server/auction/model/AuctionContext.java +++ b/src/main/java/org/prebid/server/auction/model/AuctionContext.java @@ -2,14 +2,17 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.iab.openrtb.request.BidRequest; -import io.vertx.ext.web.RoutingContext; import lombok.Builder; import lombok.Value; import org.prebid.server.cache.model.DebugHttpCall; import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.deals.model.DeepDebugLog; +import org.prebid.server.deals.model.TxnLog; import org.prebid.server.execution.Timeout; import org.prebid.server.geolocation.model.GeoInfo; +import org.prebid.server.hooks.execution.model.HookExecutionContext; import org.prebid.server.metric.MetricName; +import org.prebid.server.model.HttpRequestContext; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.settings.model.Account; @@ -20,8 +23,7 @@ @Value public class AuctionContext { - @JsonIgnore - RoutingContext routingContext; + HttpRequestContext httpRequest; @JsonIgnore UidsCookie uidsCookie; @@ -37,9 +39,54 @@ public class AuctionContext { List prebidErrors; + List debugWarnings; + Map> debugHttpCalls; PrivacyContext privacyContext; GeoInfo geoInfo; + + HookExecutionContext hookExecutionContext; + + DebugContext debugContext; + + boolean requestRejected; + + @JsonIgnore + TxnLog txnLog; + + @JsonIgnore + DeepDebugLog deepDebugLog; + + public AuctionContext with(Account account) { + return this.toBuilder().account(account).build(); + } + + public AuctionContext with(BidRequest bidRequest) { + return this.toBuilder().bidRequest(bidRequest).build(); + } + + public AuctionContext with(BidRequest bidRequest, List errors) { + return this.toBuilder().bidRequest(bidRequest).prebidErrors(errors).build(); + } + + public AuctionContext with(PrivacyContext privacyContext) { + return this.toBuilder() + .privacyContext(privacyContext) + .geoInfo(privacyContext.getTcfContext().getGeoInfo()) + .build(); + } + + public AuctionContext with(MetricName requestTypeMetric) { + return this.toBuilder() + .requestTypeMetric(requestTypeMetric) + .build(); + } + + public AuctionContext withRequestRejected() { + return this.toBuilder() + .requestRejected(true) + .build(); + } } diff --git a/src/main/java/org/prebid/server/auction/model/BidInfo.java b/src/main/java/org/prebid/server/auction/model/BidInfo.java new file mode 100644 index 00000000000..c31ca788c08 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/BidInfo.java @@ -0,0 +1,42 @@ +package org.prebid.server.auction.model; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.cache.model.CacheInfo; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +@Builder(toBuilder = true) +@Value +public class BidInfo { + + Bid bid; + + Imp correspondingImp; + + String bidCurrency; + + String bidder; + + BidType bidType; + + CacheInfo cacheInfo; + + String lineItemId; + + String lineItemSource; + + TargetingInfo targetingInfo; + + public String getBidId() { + final ObjectNode extNode = bid != null ? bid.getExt() : null; + final JsonNode bidIdNode = extNode != null ? extNode.path("prebid").path("bidid") : null; + final String generatedBidId = bidIdNode != null && bidIdNode.isTextual() ? bidIdNode.textValue() : null; + final String bidId = bid != null ? bid.getId() : null; + + return generatedBidId != null ? generatedBidId : bidId; + } +} diff --git a/src/main/java/org/prebid/server/auction/model/BidderRequest.java b/src/main/java/org/prebid/server/auction/model/BidderRequest.java index 35ffaf64829..560837cfd7a 100644 --- a/src/main/java/org/prebid/server/auction/model/BidderRequest.java +++ b/src/main/java/org/prebid/server/auction/model/BidderRequest.java @@ -11,5 +11,11 @@ public class BidderRequest { String bidder; + String storedResponse; + BidRequest bidRequest; + + public BidderRequest with(BidRequest bidRequest) { + return of(this.bidder, this.storedResponse, bidRequest); + } } diff --git a/src/main/java/org/prebid/server/auction/model/BidderResponseInfo.java b/src/main/java/org/prebid/server/auction/model/BidderResponseInfo.java new file mode 100644 index 00000000000..f02a9af5b72 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/BidderResponseInfo.java @@ -0,0 +1,20 @@ +package org.prebid.server.auction.model; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.bidder.model.BidderSeatBidInfo; + +@AllArgsConstructor(staticName = "of") +@Value +public class BidderResponseInfo { + + String bidder; + + BidderSeatBidInfo seatBid; + + int responseTime; + + public BidderResponseInfo with(BidderSeatBidInfo seatBid) { + return of(this.bidder, seatBid, this.responseTime); + } +} diff --git a/src/main/java/org/prebid/server/auction/model/ConsentType.java b/src/main/java/org/prebid/server/auction/model/ConsentType.java new file mode 100644 index 00000000000..a35297fb3f7 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/ConsentType.java @@ -0,0 +1,9 @@ +package org.prebid.server.auction.model; + +/** + * Describes consent types that can be present in `consent_type` amp query param + */ +public enum ConsentType { + + tcfV1, tcfV2, usPrivacy, unknown; +} diff --git a/src/main/java/org/prebid/server/auction/model/CookieSyncContext.java b/src/main/java/org/prebid/server/auction/model/CookieSyncContext.java new file mode 100644 index 00000000000..96760cdf529 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/CookieSyncContext.java @@ -0,0 +1,35 @@ +package org.prebid.server.auction.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.vertx.ext.web.RoutingContext; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.bidder.UsersyncMethodChooser; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.execution.Timeout; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.request.CookieSyncRequest; +import org.prebid.server.settings.model.Account; + +@Builder(toBuilder = true) +@Value +public class CookieSyncContext { + + @JsonIgnore + RoutingContext routingContext; + + @JsonIgnore + UidsCookie uidsCookie; + + CookieSyncRequest cookieSyncRequest; + + @JsonIgnore + UsersyncMethodChooser usersyncMethodChooser; + + @JsonIgnore + Timeout timeout; + + Account account; + + PrivacyContext privacyContext; +} diff --git a/src/main/java/org/prebid/server/auction/model/DebugContext.java b/src/main/java/org/prebid/server/auction/model/DebugContext.java new file mode 100644 index 00000000000..61b2560136d --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/DebugContext.java @@ -0,0 +1,18 @@ +package org.prebid.server.auction.model; + +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; + +@Value(staticConstructor = "of") +public class DebugContext { + + private static final DebugContext EMPTY = DebugContext.of(false, null); + + boolean debugEnabled; + + TraceLevel traceLevel; + + public static DebugContext empty() { + return EMPTY; + } +} diff --git a/src/main/java/org/prebid/server/auction/model/Endpoint.java b/src/main/java/org/prebid/server/auction/model/Endpoint.java new file mode 100644 index 00000000000..00261aedf66 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/Endpoint.java @@ -0,0 +1,18 @@ +package org.prebid.server.auction.model; + +public enum Endpoint { + + openrtb2_auction("/openrtb2/auction"), + openrtb2_amp("/openrtb2/amp"), + openrtb2_video("/openrtb2/video"); + + private final String value; + + Endpoint(String value) { + this.value = value; + } + + public String value() { + return value; + } +} diff --git a/src/main/java/org/prebid/server/auction/model/MultiBidConfig.java b/src/main/java/org/prebid/server/auction/model/MultiBidConfig.java new file mode 100644 index 00000000000..4f09ca885d8 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/MultiBidConfig.java @@ -0,0 +1,13 @@ +package org.prebid.server.auction.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class MultiBidConfig { + + String bidder; + + Integer maxBids; + + String targetBidderCodePrefix; +} diff --git a/src/main/java/org/prebid/server/auction/model/PreBidRequestContext.java b/src/main/java/org/prebid/server/auction/model/PreBidRequestContext.java deleted file mode 100644 index fba4e72b551..00000000000 --- a/src/main/java/org/prebid/server/auction/model/PreBidRequestContext.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.prebid.server.auction.model; - -import lombok.Builder; -import lombok.Value; -import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.execution.Timeout; -import org.prebid.server.proto.request.PreBidRequest; - -import java.util.List; - -@Builder -@Value -public class PreBidRequestContext { - - List adapterRequests; - - PreBidRequest preBidRequest; - - UidsCookie uidsCookie; - - Timeout timeout; - - String domain; - - String referer; - - String ip; - - String ua; - - Integer secure; - - boolean isDebug; - - boolean noLiveUids; -} diff --git a/src/main/java/org/prebid/server/auction/model/SetuidContext.java b/src/main/java/org/prebid/server/auction/model/SetuidContext.java new file mode 100644 index 00000000000..6fa4bb6e8de --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/SetuidContext.java @@ -0,0 +1,33 @@ +package org.prebid.server.auction.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.vertx.ext.web.RoutingContext; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.execution.Timeout; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.settings.model.Account; + +@Builder(toBuilder = true) +@Value +public class SetuidContext { + + @JsonIgnore + RoutingContext routingContext; + + @JsonIgnore + UidsCookie uidsCookie; + + @JsonIgnore + Timeout timeout; + + Account account; + + String cookieName; + + @JsonIgnore + String syncType; + + PrivacyContext privacyContext; +} diff --git a/src/main/java/org/prebid/server/auction/model/StoredResponseResult.java b/src/main/java/org/prebid/server/auction/model/StoredResponseResult.java index 5808f9890db..4b3dfa736f3 100644 --- a/src/main/java/org/prebid/server/auction/model/StoredResponseResult.java +++ b/src/main/java/org/prebid/server/auction/model/StoredResponseResult.java @@ -6,6 +6,7 @@ import lombok.Value; import java.util.List; +import java.util.Map; @AllArgsConstructor(staticName = "of") @Value @@ -13,5 +14,7 @@ public class StoredResponseResult { List requiredRequestImps; - List storedResponse; + List auctionStoredResponse; + + Map> impBidderToStoredBidResponse; } diff --git a/src/main/java/org/prebid/server/auction/model/TargetingBidInfo.java b/src/main/java/org/prebid/server/auction/model/TargetingBidInfo.java new file mode 100644 index 00000000000..319dca3f272 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/TargetingBidInfo.java @@ -0,0 +1,21 @@ +package org.prebid.server.auction.model; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class TargetingBidInfo { + + BidInfo bidInfo; + + String bidderCode; + + boolean isTargetingEnabled; + + boolean isWinningBid; + + boolean isBidderWinningBid; + + boolean isAddTargetBidderCode; +} diff --git a/src/main/java/org/prebid/server/auction/model/TargetingInfo.java b/src/main/java/org/prebid/server/auction/model/TargetingInfo.java new file mode 100644 index 00000000000..a85b1041971 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/TargetingInfo.java @@ -0,0 +1,19 @@ +package org.prebid.server.auction.model; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class TargetingInfo { + + String bidderCode; + + boolean isTargetingEnabled; + + boolean isWinningBid; + + boolean isBidderWinningBid; + + boolean isAddTargetBidderCode; +} diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java new file mode 100644 index 00000000000..26cffe9dd1a --- /dev/null +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -0,0 +1,728 @@ +package org.prebid.server.auction.requestfactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import io.vertx.core.Future; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import io.vertx.ext.web.RoutingContext; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.FpdResolver; +import org.prebid.server.auction.ImplicitParametersExtractor; +import org.prebid.server.auction.OrtbTypesResolver; +import org.prebid.server.auction.PriceGranularity; +import org.prebid.server.auction.PrivacyEnforcementService; +import org.prebid.server.auction.StoredRequestProcessor; +import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.ConsentType; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.MetricName; +import org.prebid.server.model.Endpoint; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.privacy.ccpa.Ccpa; +import org.prebid.server.privacy.gdpr.TcfDefinerService; +import org.prebid.server.proto.openrtb.ext.request.ConsentedProvidersSettings; +import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; +import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAmp; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheBids; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheVastxml; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; +import org.prebid.server.proto.openrtb.ext.request.ExtSite; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.request.Targeting; +import org.prebid.server.settings.model.Account; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class AmpRequestFactory { + + private static final Logger logger = LoggerFactory.getLogger(AmpRequestFactory.class); + + private static final String TAG_ID_REQUEST_PARAM = "tag_id"; + private static final String TARGETING_REQUEST_PARAM = "targeting"; + private static final String DEBUG_REQUEST_PARAM = "debug"; + private static final String OW_REQUEST_PARAM = "ow"; + private static final String OH_REQUEST_PARAM = "oh"; + private static final String W_REQUEST_PARAM = "w"; + private static final String H_REQUEST_PARAM = "h"; + private static final String MS_REQUEST_PARAM = "ms"; + private static final String CURL_REQUEST_PARAM = "curl"; + private static final String ACCOUNT_REQUEST_PARAM = "account"; + private static final String SLOT_REQUEST_PARAM = "slot"; + private static final String TIMEOUT_REQUEST_PARAM = "timeout"; + private static final String GDPR_CONSENT_PARAM = "gdpr_consent"; + private static final String CONSENT_PARAM = "consent_string"; + private static final String GDPR_APPLIES_PARAM = "gdpr_applies"; + private static final String CONSENT_TYPE_PARAM = "consent_type"; + private static final String ATTL_CONSENT_PARAM = "attl_consent"; + + private static final int NO_LIMIT_SPLIT_MODE = -1; + private static final String AMP_CHANNEL = "amp"; + private static final String ENDPOINT = Endpoint.openrtb2_amp.value(); + + private final Ortb2RequestFactory ortb2RequestFactory; + private final StoredRequestProcessor storedRequestProcessor; + private final OrtbTypesResolver ortbTypesResolver; + private final ImplicitParametersExtractor implicitParametersExtractor; + private final Ortb2ImplicitParametersResolver paramsResolver; + private final FpdResolver fpdResolver; + private final PrivacyEnforcementService privacyEnforcementService; + private final TimeoutResolver timeoutResolver; + private final JacksonMapper mapper; + + public AmpRequestFactory(StoredRequestProcessor storedRequestProcessor, + Ortb2RequestFactory ortb2RequestFactory, + OrtbTypesResolver ortbTypesResolver, + ImplicitParametersExtractor implicitParametersExtractor, + Ortb2ImplicitParametersResolver paramsResolver, + FpdResolver fpdResolver, + PrivacyEnforcementService privacyEnforcementService, + TimeoutResolver timeoutResolver, + JacksonMapper mapper) { + + this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); + this.ortbTypesResolver = Objects.requireNonNull(ortbTypesResolver); + this.implicitParametersExtractor = Objects.requireNonNull(implicitParametersExtractor); + this.paramsResolver = Objects.requireNonNull(paramsResolver); + this.fpdResolver = Objects.requireNonNull(fpdResolver); + this.timeoutResolver = Objects.requireNonNull(timeoutResolver); + this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); + this.mapper = Objects.requireNonNull(mapper); + } + + /** + * Creates {@link AuctionContext} based on {@link RoutingContext}. + */ + public Future fromRequest(RoutingContext routingContext, long startTime) { + final String body = routingContext.getBodyAsString(); + + final AuctionContext initialAuctionContext = ortb2RequestFactory.createAuctionContext( + Endpoint.openrtb2_amp, MetricName.amp); + + return ortb2RequestFactory.executeEntrypointHooks(routingContext, body, initialAuctionContext) + .compose(httpRequest -> parseBidRequest(httpRequest, initialAuctionContext) + .map(bidRequest -> ortb2RequestFactory.enrichAuctionContext( + initialAuctionContext, httpRequest, bidRequest, startTime))) + + .compose(auctionContext -> ortb2RequestFactory.fetchAccount(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> updateBidRequest(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> privacyEnforcementService.contextFromBidRequest(auctionContext) + .map(auctionContext::with)) + + .map(auctionContext -> auctionContext.with( + ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext))) + + .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) + .map(auctionContext::with)) + + .compose(ortb2RequestFactory::populateDealsInfo) + + .recover(ortb2RequestFactory::restoreResultFromRejection); + } + + /** + * Creates {@link BidRequest} and sets properties which were not set explicitly by the client, but can be + * updated by values derived from headers and other request attributes. + */ + private Future parseBidRequest(HttpRequestContext httpRequest, AuctionContext auctionContext) { + final String tagId = httpRequest.getQueryParams().get(TAG_ID_REQUEST_PARAM); + if (StringUtils.isBlank(tagId)) { + return Future.failedFuture(new InvalidRequestException("AMP requests require an AMP tag_id")); + } + + final ConsentType consentType = consentTypeFromQueryStringParams(httpRequest); + final String consentString = consentStringFromQueryStringParams(httpRequest); + final String attlConsent = attlConsentFromQueryStringParams(httpRequest); + final Integer gdpr = gdprFromQueryStringParams(httpRequest); + final Integer debug = debugFromQueryStringParam(httpRequest); + final Long timeout = timeoutFromQueryString(httpRequest); + + final BidRequest bidRequest = BidRequest.builder() + .site(createSite(httpRequest)) + .user(createUser(consentType, consentString, attlConsent)) + .regs(createRegs(consentString, consentType, gdpr)) + .test(debug) + .tmax(timeout) + .ext(createExt(httpRequest, tagId, debug)) + .build(); + + validateOriginalBidRequest(bidRequest, consentString, auctionContext); + + return Future.succeededFuture(bidRequest); + } + + private static Site createSite(HttpRequestContext httpRequest) { + final String accountId = StringUtils.trimToNull(httpRequest.getQueryParams().get(ACCOUNT_REQUEST_PARAM)); + final String canonicalUrl = StringUtils.trimToNull(canonicalUrl(httpRequest)); + final String domain = StringUtils.trimToNull(HttpUtil.getHostFromUrl(canonicalUrl)); + + return !StringUtils.isAllBlank(accountId, canonicalUrl, domain) + ? Site.builder() + .publisher(Publisher.builder().id(accountId).build()) + .page(canonicalUrl) + .domain(domain) + .build() + : null; + } + + private static User createUser(ConsentType consentType, String consentString, String attlConsent) { + final boolean tcfV2ConsentProvided = (StringUtils.isNotBlank(consentString) + && TcfDefinerService.isConsentStringValid(consentString)) + && (consentType == null || consentType == ConsentType.tcfV2); + + if (StringUtils.isNotBlank(attlConsent) || tcfV2ConsentProvided) { + final ExtUser.ExtUserBuilder userExtBuilder = ExtUser.builder(); + if (tcfV2ConsentProvided) { + userExtBuilder.consent(consentString); + } + if (StringUtils.isNotBlank(attlConsent)) { + userExtBuilder.consentedProvidersSettings(ConsentedProvidersSettings.of(attlConsent)); + } + return User.builder().ext(userExtBuilder.build()).build(); + } + + return null; + } + + private static Regs createRegs(String consentString, ConsentType consentType, Integer gdpr) { + final boolean ccpaProvided = Ccpa.isValid(consentString) + && (consentType == null || consentType == ConsentType.usPrivacy); + if (ccpaProvided || gdpr != null) { + return Regs.of(null, ExtRegs.of(gdpr, ccpaProvided ? consentString : null)); + } + + return null; + } + + private static ExtRequest createExt(HttpRequestContext httpRequest, String tagId, Integer debug) { + return ExtRequest.of(ExtRequestPrebid.builder() + .storedrequest(ExtStoredRequest.of(tagId)) + .amp(ExtRequestPrebidAmp.of(ampDataFromQueryString(httpRequest))) + .debug(debug) + .build()); + } + + /** + * Returns debug flag from request query string if it is equal to either 0 or 1, or null if otherwise. + */ + private static Integer debugFromQueryStringParam(HttpRequestContext httpRequest) { + final String debug = httpRequest.getQueryParams().get(DEBUG_REQUEST_PARAM); + return Objects.equals(debug, "1") ? Integer.valueOf(1) : Objects.equals(debug, "0") ? 0 : null; + } + + private static String canonicalUrl(HttpRequestContext httpRequest) { + try { + return HttpUtil.decodeUrl(httpRequest.getQueryParams().get(CURL_REQUEST_PARAM)); + } catch (IllegalArgumentException e) { + return null; + } + } + + private static ConsentType consentTypeFromQueryStringParams(HttpRequestContext httpRequest) { + final String consentTypeParam = httpRequest.getQueryParams().get(CONSENT_TYPE_PARAM); + if (consentTypeParam == null) { + return null; + } + switch (consentTypeParam) { + case "1": + return ConsentType.tcfV1; + case "2": + return ConsentType.tcfV2; + case "3": + return ConsentType.usPrivacy; + default: + return ConsentType.unknown; + } + } + + private static String consentStringFromQueryStringParams(HttpRequestContext httpRequest) { + final String requestConsentParam = httpRequest.getQueryParams().get(CONSENT_PARAM); + final String requestGdprConsentParam = httpRequest.getQueryParams().get(GDPR_CONSENT_PARAM); + + return ObjectUtils.firstNonNull(requestConsentParam, requestGdprConsentParam); + } + + private static String attlConsentFromQueryStringParams(HttpRequestContext httpRequest) { + return httpRequest.getQueryParams().get(ATTL_CONSENT_PARAM); + } + + private static Integer gdprFromQueryStringParams(HttpRequestContext httpRequest) { + final String gdprAppliesParam = httpRequest.getQueryParams().get(GDPR_APPLIES_PARAM); + if (StringUtils.equals(gdprAppliesParam, "true")) { + return 1; + } else if (StringUtils.equals(gdprAppliesParam, "false")) { + return 0; + } + + return null; + } + + private static Long timeoutFromQueryString(HttpRequestContext httpRequest) { + final String timeoutQueryParam = httpRequest.getQueryParams().get(TIMEOUT_REQUEST_PARAM); + if (timeoutQueryParam == null) { + return null; + } + + final long timeout; + try { + timeout = Long.parseLong(timeoutQueryParam); + } catch (NumberFormatException e) { + return null; + } + + return timeout > 0 ? timeout : null; + } + + private static Map ampDataFromQueryString(HttpRequestContext httpRequest) { + return httpRequest.getQueryParams().entries().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (value1, value2) -> value1)); + } + + private static void validateOriginalBidRequest( + BidRequest bidRequest, + String requestConsentString, + AuctionContext auctionContext) { + + final User user = bidRequest.getUser(); + final ExtUser extUser = user != null ? user.getExt() : null; + final String gdprConsentString = extUser != null ? extUser.getConsent() : null; + + final Regs regs = bidRequest.getRegs(); + final ExtRegs extRegs = regs != null ? regs.getExt() : null; + final String usPrivacy = extRegs != null ? extRegs.getUsPrivacy() : null; + + if (StringUtils.isAllBlank(gdprConsentString, usPrivacy)) { + final String message = String.format( + "Amp request parameter %s or %s have invalid format: %s", + CONSENT_PARAM, + GDPR_CONSENT_PARAM, + requestConsentString); + logger.debug(message); + auctionContext.getPrebidErrors().add(message); + } + } + + /** + * Creates {@link BidRequest} and sets properties which were not set explicitly by the client, but can be + * updated by values derived from headers and other request attributes. + */ + private Future updateBidRequest(AuctionContext auctionContext) { + final BidRequest receivedBidRequest = auctionContext.getBidRequest(); + final String storedRequestId = storedRequestId(receivedBidRequest); + if (StringUtils.isBlank(storedRequestId)) { + return Future.failedFuture( + new InvalidRequestException("AMP requests require the stored request id in AMP tag_id")); + } + + final Account account = auctionContext.getAccount(); + final String accountId = account != null ? account.getId() : null; + + final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); + + return storedRequestProcessor.processAmpRequest(accountId, storedRequestId, receivedBidRequest) + .map(bidRequest -> validateStoredBidRequest(storedRequestId, bidRequest)) + .map(this::fillExplicitParameters) + .map(bidRequest -> overrideParameters(bidRequest, httpRequest, auctionContext.getPrebidErrors())) + .map(bidRequest -> paramsResolver.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT)) + .map(ortb2RequestFactory::validateRequest); + } + + private static String storedRequestId(BidRequest receivedBidRequest) { + final ExtRequest requestExt = receivedBidRequest != null ? receivedBidRequest.getExt() : null; + final ExtRequestPrebid prebid = requestExt != null ? requestExt.getPrebid() : null; + final ExtStoredRequest storedRequest = prebid != null ? prebid.getStoredrequest() : null; + return storedRequest != null ? storedRequest.getId() : null; + } + + /** + * Throws {@link InvalidRequestException} in case of invalid {@link BidRequest}. + */ + private static BidRequest validateStoredBidRequest(String tagId, BidRequest bidRequest) { + final List imps = bidRequest.getImp(); + if (CollectionUtils.isEmpty(imps)) { + throw new InvalidRequestException( + String.format("data for tag_id='%s' does not define the required imp array.", tagId)); + } + + final int impSize = imps.size(); + if (impSize > 1) { + throw new InvalidRequestException( + String.format("data for tag_id '%s' includes %d imp elements. Only one is allowed", tagId, + impSize)); + } + + if (bidRequest.getApp() != null) { + throw new InvalidRequestException("request.app must not exist in AMP stored requests."); + } + + return bidRequest; + } + + /** + * - Updates {@link BidRequest}.ext.prebid.targeting and {@link BidRequest}.ext.prebid.cache.bids with default + * values if it was not included by user + * - Updates {@link Imp} security if required to ensure that amp always uses + * https protocol + * - Sets {@link BidRequest}.test = 1 if it was passed in {@link RoutingContext} + * - Updates {@link BidRequest}.ext.prebid.amp.data with all query parameters + */ + private BidRequest fillExplicitParameters(BidRequest bidRequest) { + final List imps = bidRequest.getImp(); + // Force HTTPS as AMP requires it, but pubs can forget to set it. + final Imp imp = imps.get(0); + final Integer secure = imp.getSecure(); + final boolean setSecure = secure == null || secure != 1; + + final ExtRequestPrebid prebid = bidRequest.getExt().getPrebid(); + + // AMP won't function unless ext.prebid.targeting and ext.prebid.cache.bids are defined. + // If the user didn't include them, default those here. + final boolean setDefaultTargeting; + final boolean setDefaultCache; + + final boolean setChannel; + + if (prebid == null) { + setDefaultTargeting = true; + setDefaultCache = true; + setChannel = true; + } else { + final ExtRequestTargeting targeting = prebid.getTargeting(); + setDefaultTargeting = targeting == null + || targeting.getIncludewinners() == null + || targeting.getIncludebidderkeys() == null + || targeting.getPricegranularity() == null || targeting.getPricegranularity().isNull(); + + final ExtRequestPrebidCache cache = prebid.getCache(); + setDefaultCache = cache == null || cache.equals(ExtRequestPrebidCache.EMPTY); + + setChannel = prebid.getChannel() == null; + } + + final BidRequest result; + if (setSecure + || setDefaultTargeting + || setDefaultCache + || setChannel) { + + result = bidRequest.toBuilder() + .imp(setSecure ? Collections.singletonList(imps.get(0).toBuilder().secure(1).build()) : imps) + .ext(extRequest( + bidRequest, + setDefaultTargeting, + setDefaultCache, + setChannel)) + .build(); + } else { + result = bidRequest; + } + return result; + } + + /** + * Extracts parameters from http request and overrides corresponding attributes in {@link BidRequest}. + */ + private BidRequest overrideParameters(BidRequest bidRequest, HttpRequestContext httpRequest, List errors) { + final String requestTargeting = httpRequest.getQueryParams().get(TARGETING_REQUEST_PARAM); + final ObjectNode targetingNode = readTargeting(requestTargeting); + ortbTypesResolver.normalizeTargeting( + targetingNode, errors, implicitParametersExtractor.refererFrom(httpRequest)); + final Targeting targeting = parseTargeting(targetingNode); + + final Site updatedSite = overrideSite(bidRequest.getSite()); + final Imp updatedImp = overrideImp(bidRequest.getImp().get(0), httpRequest, targetingNode); + final ExtRequest updatedExtBidRequest = overrideExtBidRequest(bidRequest.getExt(), targeting); + + if (ObjectUtils.anyNotNull(updatedSite, updatedImp, updatedExtBidRequest)) { + return bidRequest.toBuilder() + .site(updatedSite != null ? updatedSite : bidRequest.getSite()) + .imp(updatedImp != null ? Collections.singletonList(updatedImp) : bidRequest.getImp()) + .ext(updatedExtBidRequest != null ? updatedExtBidRequest : bidRequest.getExt()) + .build(); + } + + return bidRequest; + } + + private ObjectNode readTargeting(String jsonTargeting) { + try { + final String decodedJsonTargeting = HttpUtil.decodeUrl(jsonTargeting); + final JsonNode jsonNodeTargeting = decodedJsonTargeting != null + ? mapper.mapper().readTree(decodedJsonTargeting) + : null; + return jsonNodeTargeting != null ? validateAndGetTargeting(jsonNodeTargeting) : null; + } catch (JsonProcessingException | IllegalArgumentException e) { + throw new InvalidRequestException(String.format("Error reading targeting json %s", e.getMessage())); + } + } + + private ObjectNode validateAndGetTargeting(JsonNode jsonNodeTargeting) { + if (jsonNodeTargeting.isObject()) { + return (ObjectNode) jsonNodeTargeting; + } else { + throw new InvalidRequestException(String.format("Error decoding targeting, expected type is `object` " + + "but was %s", jsonNodeTargeting.getNodeType().name())); + } + } + + private Targeting parseTargeting(ObjectNode targetingNode) { + try { + return targetingNode == null + ? Targeting.empty() + : mapper.mapper().treeToValue(targetingNode, Targeting.class); + } catch (JsonProcessingException e) { + throw new InvalidRequestException(String.format("Error decoding targeting from url: %s", e.getMessage())); + } + } + + private Site overrideSite(Site site) { + final boolean hasSite = site != null; + final ExtSite siteExt = hasSite ? site.getExt() : null; + final boolean shouldSetExtAmp = siteExt == null || siteExt.getAmp() == null; + + if (shouldSetExtAmp) { + final Site.SiteBuilder siteBuilder = hasSite ? site.toBuilder() : Site.builder(); + + final ObjectNode data = siteExt != null ? siteExt.getData() : null; + siteBuilder.ext(ExtSite.of(1, data)); + + return siteBuilder.build(); + } + + return null; + } + + private Imp overrideImp(Imp imp, HttpRequestContext httpRequest, ObjectNode targetingNode) { + final String tagId = httpRequest.getQueryParams().get(SLOT_REQUEST_PARAM); + final Banner banner = imp.getBanner(); + final List overwrittenFormats = banner != null + ? createOverrideBannerFormats(httpRequest, banner.getFormat()) + : null; + if (StringUtils.isNotBlank(tagId) || CollectionUtils.isNotEmpty(overwrittenFormats) || targetingNode != null) { + return imp.toBuilder() + .tagid(StringUtils.isNotBlank(tagId) ? tagId : imp.getTagid()) + .banner(overrideBanner(imp.getBanner(), overwrittenFormats)) + .ext(fpdResolver.resolveImpExt(imp.getExt(), targetingNode)) + .build(); + } + return null; + } + + /** + * Creates formats from request parameters to override origin amp banner formats. + */ + private static List createOverrideBannerFormats(HttpRequestContext httpRequest, List formats) { + final int overrideWidth = parseIntParamOrZero(httpRequest, OW_REQUEST_PARAM); + final int width = parseIntParamOrZero(httpRequest, W_REQUEST_PARAM); + final int overrideHeight = parseIntParamOrZero(httpRequest, OH_REQUEST_PARAM); + final int height = parseIntParamOrZero(httpRequest, H_REQUEST_PARAM); + final String multiSizeParam = httpRequest.getQueryParams().get(MS_REQUEST_PARAM); + + final List paramsFormats = createFormatsFromParams(overrideWidth, width, overrideHeight, height, + multiSizeParam); + + return CollectionUtils.isNotEmpty(paramsFormats) + ? paramsFormats + : updateFormatsFromParams(formats, width, height); + } + + private static Integer parseIntParamOrZero(HttpRequestContext httpRequest, String name) { + return parseIntOrZero(httpRequest.getQueryParams().get(name)); + } + + private static Integer parseIntOrZero(String param) { + try { + return Integer.parseInt(param); + } catch (NumberFormatException e) { + return 0; + } + } + + /** + * Create new formats from request parameters. + */ + private static List createFormatsFromParams(Integer overrideWidth, Integer width, Integer overrideHeight, + Integer height, String multiSizeParam) { + final List formats = new ArrayList<>(); + + if (overrideWidth != 0 && overrideHeight != 0) { + formats.add(Format.builder().w(overrideWidth).h(overrideHeight).build()); + } else if (overrideWidth != 0 && height != 0) { + formats.add(Format.builder().w(overrideWidth).h(height).build()); + } else if (width != 0 && overrideHeight != 0) { + formats.add(Format.builder().w(width).h(overrideHeight).build()); + } else if (width != 0 && height != 0) { + formats.add(Format.builder().w(width).h(height).build()); + } + + // Append formats from multi-size param if exist + final List multiSizeFormats = StringUtils.isNotBlank(multiSizeParam) + ? parseMultiSizeParam(multiSizeParam) + : Collections.emptyList(); + if (!multiSizeFormats.isEmpty()) { + formats.addAll(multiSizeFormats); + } + + return formats; + } + + /** + * Updates origin amp banner formats from parameters. + */ + private static List updateFormatsFromParams(List formats, Integer width, Integer height) { + final List updatedFormats; + if (width != 0) { + updatedFormats = formats.stream() + .map(format -> Format.builder().w(width).h(format.getH()).build()) + .collect(Collectors.toList()); + } else if (height != 0) { + updatedFormats = formats.stream() + .map(format -> Format.builder().w(format.getW()).h(height).build()) + .collect(Collectors.toList()); + } else { + updatedFormats = Collections.emptyList(); + } + return updatedFormats; + } + + private static Banner overrideBanner(Banner banner, List formats) { + return banner != null && CollectionUtils.isNotEmpty(formats) + ? banner.toBuilder().format(formats).build() + : banner; + } + + /** + * Overrides {@link ExtRequest} with first party data. + */ + private ExtRequest overrideExtBidRequest(ExtRequest extRequest, Targeting targeting) { + return fpdResolver.resolveBidRequestExt(extRequest, targeting); + } + + private static List parseMultiSizeParam(String ms) { + final String[] formatStrings = ms.split(",", NO_LIMIT_SPLIT_MODE); + final List formats = new ArrayList<>(); + for (String format : formatStrings) { + final String[] widthHeight = format.split("x", NO_LIMIT_SPLIT_MODE); + if (widthHeight.length != 2) { + return Collections.emptyList(); + } + + final Integer width = parseIntOrZero(widthHeight[0]); + final Integer height = parseIntOrZero(widthHeight[1]); + + if (width == 0 && height == 0) { + return Collections.emptyList(); + } + + formats.add(Format.builder() + .w(width) + .h(height) + .build()); + } + return formats; + } + + /** + * Creates updated bidrequest.ext {@link ObjectNode}. + */ + private ExtRequest extRequest(BidRequest bidRequest, + boolean setDefaultTargeting, + boolean setDefaultCache, + boolean setChannel) { + + final ExtRequest result; + if (setDefaultTargeting || setDefaultCache || setChannel) { + final ExtRequest requestExt = bidRequest.getExt(); + final ExtRequestPrebid prebid = requestExt != null ? requestExt.getPrebid() : null; + final ExtRequestPrebid.ExtRequestPrebidBuilder prebidBuilder = prebid != null + ? prebid.toBuilder() + : ExtRequestPrebid.builder(); + + if (setDefaultTargeting) { + prebidBuilder.targeting(createTargetingWithDefaults(prebid)); + } + if (setDefaultCache) { + prebidBuilder.cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(null, null), + ExtRequestPrebidCacheVastxml.of(null, null), null)); + } + if (setChannel) { + prebidBuilder.channel(ExtRequestPrebidChannel.of(AMP_CHANNEL)); + } + + final ExtRequest updatedExt = ExtRequest.of(prebidBuilder.build()); + if (requestExt != null) { + updatedExt.addProperties(requestExt.getProperties()); + } + + result = updatedExt; + } else { + result = bidRequest.getExt(); + } + return result; + } + + /** + * Creates updated with default values bidrequest.ext.targeting {@link ExtRequestTargeting} if at least one of it's + * child properties is missed or entire targeting does not exist. + */ + private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) { + final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; + final boolean isTargetingNull = targeting == null; + + final JsonNode priceGranularityNode = isTargetingNull ? null : targeting.getPricegranularity(); + final boolean isPriceGranularityNull = priceGranularityNode == null || priceGranularityNode.isNull(); + final JsonNode outgoingPriceGranularityNode = isPriceGranularityNull + ? mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)) + : priceGranularityNode; + + final ExtMediaTypePriceGranularity mediaTypePriceGranularity = isTargetingNull + ? null : targeting.getMediatypepricegranularity(); + + final boolean includeWinners = isTargetingNull || targeting.getIncludewinners() == null + || targeting.getIncludewinners(); + + final boolean includeBidderKeys = isTargetingNull || targeting.getIncludebidderkeys() == null + || targeting.getIncludebidderkeys(); + + final Boolean includeFormat = !isTargetingNull ? targeting.getIncludeformat() : null; + + return (isTargetingNull ? ExtRequestTargeting.builder() : targeting.toBuilder()) + .pricegranularity(outgoingPriceGranularityNode) + .mediatypepricegranularity(mediaTypePriceGranularity) + .includewinners(includeWinners) + .includebidderkeys(includeBidderKeys) + .includeformat(includeFormat) + .build(); + } +} diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java new file mode 100644 index 00000000000..98f5ddc7a22 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -0,0 +1,172 @@ +package org.prebid.server.auction.requestfactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import io.vertx.ext.web.RoutingContext; +import org.prebid.server.auction.ImplicitParametersExtractor; +import org.prebid.server.auction.InterstitialProcessor; +import org.prebid.server.auction.OrtbTypesResolver; +import org.prebid.server.auction.PrivacyEnforcementService; +import org.prebid.server.auction.StoredRequestProcessor; +import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.MetricName; +import org.prebid.server.model.Endpoint; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.settings.model.Account; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +/** + * Used in OpenRTB request processing. + */ +public class AuctionRequestFactory { + + private final long maxRequestSize; + private final Ortb2RequestFactory ortb2RequestFactory; + private final StoredRequestProcessor storedRequestProcessor; + private final ImplicitParametersExtractor paramsExtractor; + private final Ortb2ImplicitParametersResolver paramsResolver; + private final InterstitialProcessor interstitialProcessor; + private final PrivacyEnforcementService privacyEnforcementService; + private final TimeoutResolver timeoutResolver; + private final JacksonMapper mapper; + private final OrtbTypesResolver ortbTypesResolver; + + private static final String ENDPOINT = Endpoint.openrtb2_auction.value(); + + public AuctionRequestFactory(long maxRequestSize, + Ortb2RequestFactory ortb2RequestFactory, + StoredRequestProcessor storedRequestProcessor, + ImplicitParametersExtractor paramsExtractor, + Ortb2ImplicitParametersResolver paramsResolver, + InterstitialProcessor interstitialProcessor, + OrtbTypesResolver ortbTypesResolver, + PrivacyEnforcementService privacyEnforcementService, + TimeoutResolver timeoutResolver, + JacksonMapper mapper) { + + this.maxRequestSize = maxRequestSize; + this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); + this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.paramsExtractor = Objects.requireNonNull(paramsExtractor); + this.paramsResolver = Objects.requireNonNull(paramsResolver); + this.interstitialProcessor = Objects.requireNonNull(interstitialProcessor); + this.ortbTypesResolver = Objects.requireNonNull(ortbTypesResolver); + this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); + this.timeoutResolver = Objects.requireNonNull(timeoutResolver); + this.mapper = Objects.requireNonNull(mapper); + } + + /** + * Creates {@link AuctionContext} based on {@link RoutingContext}. + */ + public Future fromRequest(RoutingContext routingContext, long startTime) { + final String body; + try { + body = extractAndValidateBody(routingContext); + } catch (InvalidRequestException e) { + return Future.failedFuture(e); + } + + final AuctionContext initialAuctionContext = ortb2RequestFactory.createAuctionContext( + Endpoint.openrtb2_auction, MetricName.openrtb2web); + + return ortb2RequestFactory.executeEntrypointHooks(routingContext, body, initialAuctionContext) + .compose(httpRequest -> parseBidRequest(httpRequest, initialAuctionContext.getPrebidErrors()) + .map(bidRequest -> ortb2RequestFactory + .enrichAuctionContext(initialAuctionContext, httpRequest, bidRequest, startTime) + .with(requestTypeMetric(bidRequest)))) + + .compose(auctionContext -> ortb2RequestFactory.fetchAccount(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> ortb2RequestFactory.executeRawAuctionRequestHooks(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> updateBidRequest(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> privacyEnforcementService.contextFromBidRequest(auctionContext) + .map(auctionContext::with)) + + .map(auctionContext -> auctionContext.with( + ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext))) + + .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) + .map(auctionContext::with)) + + .compose(ortb2RequestFactory::populateDealsInfo) + + .recover(ortb2RequestFactory::restoreResultFromRejection); + } + + private String extractAndValidateBody(RoutingContext routingContext) { + final String body = routingContext.getBodyAsString(); + if (body == null) { + throw new InvalidRequestException("Incoming request has no body"); + } + + if (body.length() > maxRequestSize) { + throw new InvalidRequestException( + String.format("Request size exceeded max size of %d bytes.", maxRequestSize)); + } + + return body; + } + + private Future parseBidRequest(HttpRequestContext httpRequest, List errors) { + try { + final JsonNode bidRequestNode = bodyAsJsonNode(httpRequest.getBody()); + + final String referer = paramsExtractor.refererFrom(httpRequest); + ortbTypesResolver.normalizeBidRequest(bidRequestNode, errors, referer); + + return Future.succeededFuture(jsonNodeAsBidRequest(bidRequestNode)); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + private JsonNode bodyAsJsonNode(String body) { + try { + return mapper.mapper().readTree(body); + } catch (IOException e) { + throw new InvalidRequestException(String.format("Error decoding bidRequest: %s", e.getMessage())); + } + } + + private BidRequest jsonNodeAsBidRequest(JsonNode bidRequestNode) { + try { + return mapper.mapper().treeToValue(bidRequestNode, BidRequest.class); + } catch (JsonProcessingException e) { + throw new InvalidRequestException(String.format("Error decoding bidRequest: %s", e.getMessage())); + } + } + + /** + * Sets {@link BidRequest} properties which were not set explicitly by the client, but can be + * updated by values derived from headers and other request attributes. + */ + private Future updateBidRequest(AuctionContext auctionContext) { + final Account account = auctionContext.getAccount(); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); + + return storedRequestProcessor.processStoredRequests(account.getId(), bidRequest) + .map(resolvedBidRequest -> + paramsResolver.resolve(resolvedBidRequest, httpRequest, timeoutResolver, ENDPOINT)) + .map(ortb2RequestFactory::validateRequest) + .map(interstitialProcessor::process); + } + + private static MetricName requestTypeMetric(BidRequest bidRequest) { + return bidRequest.getApp() != null ? MetricName.openrtb2app : MetricName.openrtb2web; + } +} diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java new file mode 100644 index 00000000000..a57561b2fc8 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java @@ -0,0 +1,753 @@ +package org.prebid.server.auction.requestfactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.ImplicitParametersExtractor; +import org.prebid.server.auction.IpAddressHelper; +import org.prebid.server.auction.PriceGranularity; +import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.model.IpAddress; +import org.prebid.server.exception.BlacklistedAppException; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.identity.IdGenerator; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; +import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidPbs; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; +import org.prebid.server.proto.openrtb.ext.request.ExtSite; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.StreamUtil; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Currency; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class Ortb2ImplicitParametersResolver { + + private static final Logger logger = LoggerFactory.getLogger(Ortb2ImplicitParametersResolver.class); + + private static final String WEB_CHANNEL = "web"; + private static final String APP_CHANNEL = "app"; + + private static final String PREBID_EXT = "prebid"; + private static final String BIDDER_EXT = "bidder"; + + private static final Set IMP_EXT_NON_BIDDER_FIELDS = Collections.unmodifiableSet(new HashSet<>( + Arrays.asList(PREBID_EXT, "context", "all", "general", "skadn", "data"))); + + private final boolean shouldCacheOnlyWinningBids; + private final String adServerCurrency; + private final List blacklistedApps; + private final ImplicitParametersExtractor paramsExtractor; + private final IpAddressHelper ipAddressHelper; + private final IdGenerator sourceIdGenerator; + private final JacksonMapper mapper; + + public Ortb2ImplicitParametersResolver(boolean shouldCacheOnlyWinningBids, + String adServerCurrency, + List blacklistedApps, + ImplicitParametersExtractor paramsExtractor, + IpAddressHelper ipAddressHelper, + IdGenerator sourceIdGenerator, + JacksonMapper mapper) { + + this.shouldCacheOnlyWinningBids = shouldCacheOnlyWinningBids; + this.adServerCurrency = validateCurrency(Objects.requireNonNull(adServerCurrency)); + this.blacklistedApps = Objects.requireNonNull(blacklistedApps); + this.paramsExtractor = Objects.requireNonNull(paramsExtractor); + this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); + this.sourceIdGenerator = Objects.requireNonNull(sourceIdGenerator); + this.mapper = Objects.requireNonNull(mapper); + } + + /** + * Validates ISO-4217 currency code. + */ + private static String validateCurrency(String code) { + try { + Currency.getInstance(code); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(String.format("Currency code supplied is not valid: %s", code), e); + } + return code; + } + + /** + * If needed creates a new {@link BidRequest} which is a copy of original but with some fields set with values + * derived from request parameters (headers, cookie etc.). + *

+ * Note: {@link TimeoutResolver} used here as argument because this method is utilized in AMP processing. + */ + BidRequest resolve(BidRequest bidRequest, + HttpRequestContext httpRequest, + TimeoutResolver timeoutResolver, + String endpoint) { + checkBlacklistedApp(bidRequest); + + final BidRequest result; + + final Device device = bidRequest.getDevice(); + final Device populatedDevice = populateDevice(device, bidRequest.getApp(), httpRequest); + + final Site site = bidRequest.getSite(); + final Site populatedSite = bidRequest.getApp() != null ? null : populateSite(site, httpRequest); + + final Source source = bidRequest.getSource(); + final Source populatedSource = populateSource(source); + + final List imps = bidRequest.getImp(); + final List populatedImps = populateImps(imps, httpRequest); + + final Integer at = bidRequest.getAt(); + final Integer resolvedAt = resolveAt(at); + + final List cur = bidRequest.getCur(); + final List resolvedCurrencies = resolveCurrencies(cur); + + final Long tmax = bidRequest.getTmax(); + final Long resolvedTmax = resolveTmax(tmax, timeoutResolver); + + final ExtRequest ext = bidRequest.getExt(); + final ExtRequest populatedExt = populateRequestExt( + ext, bidRequest, ObjectUtils.defaultIfNull(populatedImps, imps), endpoint); + + if (populatedDevice != null || populatedSite != null || populatedSource != null + || populatedImps != null || resolvedAt != null || resolvedCurrencies != null || resolvedTmax != null + || populatedExt != null) { + + result = bidRequest.toBuilder() + .device(populatedDevice != null ? populatedDevice : device) + .site(populatedSite != null ? populatedSite : site) + .source(populatedSource != null ? populatedSource : source) + .imp(populatedImps != null ? populatedImps : imps) + .at(resolvedAt != null ? resolvedAt : at) + .cur(resolvedCurrencies != null ? resolvedCurrencies : cur) + .tmax(resolvedTmax != null ? resolvedTmax : tmax) + .ext(populatedExt != null ? populatedExt : ext) + .build(); + } else { + result = bidRequest; + } + return result; + } + + private void checkBlacklistedApp(BidRequest bidRequest) { + final App app = bidRequest.getApp(); + final String appId = app != null ? app.getId() : null; + + if (StringUtils.isNotBlank(appId) && blacklistedApps.contains(appId)) { + throw new BlacklistedAppException( + String.format("Prebid-server does not process requests from App ID: %s", appId)); + } + } + + /** + * Populates the request body's 'device' section from the incoming http request if the original is partially filled + * and the request contains necessary info (User-Agent, IP-address). + */ + private Device populateDevice(Device device, App app, HttpRequestContext httpRequest) { + final String deviceIp = device != null ? device.getIp() : null; + final String deviceIpv6 = device != null ? device.getIpv6() : null; + + String resolvedIp = sanitizeIp(deviceIp, IpAddress.IP.v4); + String resolvedIpv6 = sanitizeIp(deviceIpv6, IpAddress.IP.v6); + + if (resolvedIp == null && resolvedIpv6 == null) { + final IpAddress requestIp = findIpFromRequest(httpRequest); + + resolvedIp = getIpIfVersionIs(requestIp, IpAddress.IP.v4); + resolvedIpv6 = getIpIfVersionIs(requestIp, IpAddress.IP.v6); + } + + logWarnIfNoIp(resolvedIp, resolvedIpv6); + + final String ua = device != null ? device.getUa() : null; + final Integer dnt = resolveDntHeader(httpRequest); + final Integer lmt = resolveLmt(device, app); + + if (!Objects.equals(deviceIp, resolvedIp) + || !Objects.equals(deviceIpv6, resolvedIpv6) + || StringUtils.isBlank(ua) + || dnt != null + || lmt != null) { + + final Device.DeviceBuilder builder = device == null ? Device.builder() : device.toBuilder(); + + if (StringUtils.isBlank(ua)) { + builder.ua(paramsExtractor.uaFrom(httpRequest)); + } + if (dnt != null) { + builder.dnt(dnt); + } + + if (lmt != null) { + builder.lmt(lmt); + } + + builder + .ip(resolvedIp) + .ipv6(resolvedIpv6); + + return builder.build(); + } + + return null; + } + + private Integer resolveDntHeader(HttpRequestContext request) { + final String dnt = request.getHeaders().get(HttpUtil.DNT_HEADER.toString()); + return StringUtils.equalsAny(dnt, "0", "1") ? Integer.valueOf(dnt) : null; + } + + private String sanitizeIp(String ip, IpAddress.IP version) { + final IpAddress ipAddress = ip != null ? ipAddressHelper.toIpAddress(ip) : null; + return ipAddress != null && ipAddress.getVersion() == version ? ipAddress.getIp() : null; + } + + private IpAddress findIpFromRequest(HttpRequestContext request) { + final CaseInsensitiveMultiMap headers = request.getHeaders(); + final String remoteHost = request.getRemoteHost(); + final List requestIps = paramsExtractor.ipFrom(headers, remoteHost); + return requestIps.stream() + .map(ipAddressHelper::toIpAddress) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + private static String getIpIfVersionIs(IpAddress requestIp, IpAddress.IP version) { + return requestIp != null && requestIp.getVersion() == version ? requestIp.getIp() : null; + } + + private static void logWarnIfNoIp(String resolvedIp, String resolvedIpv6) { + if (resolvedIp == null && resolvedIpv6 == null) { + logger.warn("No IP address found in OpenRTB request and HTTP request headers."); + } + } + + private static Integer resolveLmt(Device device, App app) { + if (app == null || device == null || !StringUtils.equalsIgnoreCase(device.getOs(), "ios")) { + return null; + } + + final String osv = device.getOsv(); + if (osv == null) { + return null; + } + + // osv format expected: "[major].[minor]". Example: 14.0 + final String[] versionParts = StringUtils.split(osv, '.'); + if (versionParts.length < 2) { + return null; + } + + final Integer versionMajor = tryParseAsNumber(versionParts[0]); + final Integer versionMinor = tryParseAsNumber(versionParts[1]); + if (versionMajor == null || versionMinor == null) { + return null; + } + + return resolveLmtForIos(device, versionMajor, versionMinor); + } + + private static Integer tryParseAsNumber(String number) { + try { + return Integer.parseUnsignedInt(number); + } catch (NumberFormatException e) { + return null; + } + } + + private static Integer resolveLmtForIos(Device device, Integer versionMajor, Integer versionMinor) { + if (versionMajor < 14) { + return null; + } + + if (versionMajor == 14 && (versionMinor == 0 || versionMinor == 1)) { + return resolveLmtForIos14Minor0And1(device); + } + + if (versionMajor > 14 || versionMinor >= 2) { + return resolveLmtForIos14Minor2AndHigher(device); + } + + return null; + } + + private static Integer resolveLmtForIos14Minor0And1(Device device) { + final String ifa = device.getIfa(); + final Integer lmt = device.getLmt(); + + if (StringUtils.isEmpty(ifa) || ifa.equals("00000000-0000-0000-0000-000000000000")) { + return !Objects.equals(lmt, 1) ? 1 : null; + } + + return lmt == null ? 0 : null; + } + + private static Integer resolveLmtForIos14Minor2AndHigher(Device device) { + final Integer lmt = device.getLmt(); + if (lmt != null) { + return null; + } + + final Integer atts = getIfNotNull(device.getExt(), ExtDevice::getAtts); + if (atts == null) { + return null; + } + + if (atts == 1 || atts == 2) { + return 1; + } + + if (atts == 0 || atts == 3) { + return 0; + } + + return null; + } + + /** + * Populates the request body's 'site' section from the incoming http request if the original is partially filled + * and the request contains necessary info (domain, page). + */ + private Site populateSite(Site site, HttpRequestContext httpRequest) { + final String page = site != null ? StringUtils.trimToNull(site.getPage()) : null; + final String updatedPage = page == null ? paramsExtractor.refererFrom(httpRequest) : null; + + final String domain = site != null ? StringUtils.trimToNull(site.getDomain()) : null; + final String updatedDomain = domain == null + ? HttpUtil.getHostFromUrl(ObjectUtils.defaultIfNull(updatedPage, page)) + : null; + + final Publisher publisher = site != null ? site.getPublisher() : null; + final Publisher updatedPublisher = populateSitePublisher( + publisher, ObjectUtils.defaultIfNull(updatedDomain, domain)); + + final ExtSite siteExt = site != null ? site.getExt() : null; + final ExtSite updatedSiteExt = siteExt == null || siteExt.getAmp() == null + ? ExtSite.of(0, getIfNotNull(siteExt, ExtSite::getData)) + : null; + + if (ObjectUtils.anyNotNull(updatedPage, updatedDomain, updatedPublisher, updatedSiteExt)) { + final boolean domainPresent = (publisher != null && publisher.getDomain() != null) + || (updatedPublisher != null && updatedPublisher.getDomain() != null); + + return (site == null ? Site.builder() : site.toBuilder()) + // do not set page if domain was not parsed successfully + .page(domainPresent ? ObjectUtils.defaultIfNull(updatedPage, page) : page) + .domain(ObjectUtils.defaultIfNull(updatedDomain, domain)) + .publisher(ObjectUtils.defaultIfNull(updatedPublisher, publisher)) + .ext(ObjectUtils.defaultIfNull(updatedSiteExt, siteExt)) + .build(); + } + return null; + } + + private Publisher populateSitePublisher(Publisher publisher, String domain) { + final String publisherDomain = publisher != null ? publisher.getDomain() : null; + final String updatedPublisherDomain = publisherDomain == null + ? getDomainOrNull(domain) + : null; + + if (updatedPublisherDomain != null) { + return (publisher == null ? Publisher.builder() : publisher.toBuilder()) + .domain(updatedPublisherDomain) + .build(); + } + + return null; + } + + private String getDomainOrNull(String url) { + try { + return paramsExtractor.domainFrom(url); + } catch (PreBidException e) { + logger.warn("Error occurred while populating bid request: {0}", e.getMessage()); + return null; + } + } + + /** + * Returns {@link Source} with updated source.tid or null if nothing changed. + */ + private Source populateSource(Source source) { + final String tid = source != null ? source.getTid() : null; + if (StringUtils.isEmpty(tid)) { + final String generatedId = sourceIdGenerator.generateId(); + if (StringUtils.isNotEmpty(generatedId)) { + final Source.SourceBuilder builder = source != null ? source.toBuilder() : Source.builder(); + return builder + .tid(generatedId) + .build(); + } + } + return null; + } + + /** + * - Updates imps with security 1, when secured request was received and imp security was not defined. + * - Moves bidder parameters from imp.ext._bidder_ to imp.ext.prebid.bidder._bidder_ + */ + private List populateImps(List imps, HttpRequestContext httpRequest) { + if (CollectionUtils.isEmpty(imps)) { + return null; + } + + final Integer secureFromRequest = paramsExtractor.secureFrom(httpRequest); + + if (!shouldModifyImps(imps, secureFromRequest)) { + return imps; + } + + return imps.stream() + .map(imp -> populateImp(imp, secureFromRequest)) + .collect(Collectors.toList()); + } + + private boolean shouldModifyImps(List imps, Integer secureFromRequest) { + return imps.stream() + .anyMatch(imp -> shouldSetImpSecure(imp, secureFromRequest) || shouldMoveBidderParams(imp)); + } + + private boolean shouldSetImpSecure(Imp imp, Integer secureFromRequest) { + return imp.getSecure() == null && Objects.equals(secureFromRequest, 1); + } + + private boolean shouldMoveBidderParams(Imp imp) { + return imp.getExt() != null + && StreamUtil.asStream(imp.getExt().fieldNames()).anyMatch(this::isImpExtBidderField); + } + + private boolean isImpExtBidderField(String field) { + return !IMP_EXT_NON_BIDDER_FIELDS.contains(field); + } + + private Imp populateImp(Imp imp, Integer secureFromRequest) { + final boolean shouldSetSecure = shouldSetImpSecure(imp, secureFromRequest); + final boolean shouldMoveBidderParams = shouldMoveBidderParams(imp); + + if (shouldSetSecure || shouldMoveBidderParams) { + final ObjectNode impExt = imp.getExt(); + + return imp.toBuilder() + .secure(shouldSetSecure ? Integer.valueOf(1) : imp.getSecure()) + .ext(shouldMoveBidderParams ? populateImpExt(impExt) : impExt) + .build(); + } + + return imp; + } + + private ObjectNode populateImpExt(ObjectNode impExt) { + final ObjectNode modifiedExt = impExt.deepCopy(); + + final ObjectNode modifiedExtPrebid = getOrCreateChildObjectNode(modifiedExt, PREBID_EXT); + modifiedExt.replace(PREBID_EXT, modifiedExtPrebid); + final ObjectNode modifiedExtPrebidBidder = getOrCreateChildObjectNode(modifiedExtPrebid, BIDDER_EXT); + modifiedExtPrebid.replace(BIDDER_EXT, modifiedExtPrebidBidder); + + final Set bidderFields = StreamUtil.asStream(modifiedExt.fieldNames()) + .filter(this::isImpExtBidderField) + .collect(Collectors.toSet()); + + for (final String currentBidderField : bidderFields) { + final ObjectNode modifiedExtPrebidBidderCurrentBidder = + getOrCreateChildObjectNode(modifiedExtPrebidBidder, currentBidderField); + modifiedExtPrebidBidder.replace(currentBidderField, modifiedExtPrebidBidderCurrentBidder); + + final JsonNode extCurrentBidder = modifiedExt.remove(currentBidderField); + if (isObjectNode(extCurrentBidder)) { + modifiedExtPrebidBidderCurrentBidder.setAll((ObjectNode) extCurrentBidder); + } + } + + return modifiedExt; + } + + private static ObjectNode getOrCreateChildObjectNode(ObjectNode parentNode, String fieldName) { + final JsonNode childNode = parentNode.get(fieldName); + + return isObjectNode(childNode) ? (ObjectNode) childNode : parentNode.objectNode(); + } + + private static boolean isObjectNode(JsonNode node) { + return node != null && node.isObject(); + } + + /** + * Returns updated {@link ExtRequest} if required or null otherwise. + */ + private ExtRequest populateRequestExt(ExtRequest ext, BidRequest bidRequest, List imps, String endpoint) { + if (ext == null) { + return null; + } + + final ExtRequestPrebid prebid = ext.getPrebid(); + + final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps); + final ExtRequestPrebidCache updatedCache = cacheOrNull(prebid); + final ExtRequestPrebidChannel updatedChannel = channelOrNull(prebid, bidRequest); + final ExtRequestPrebidPbs updatedPbs = pbsOrNull(bidRequest, endpoint); + + if (updatedTargeting != null || updatedCache != null || updatedChannel != null || updatedPbs != null) { + final ExtRequestPrebid.ExtRequestPrebidBuilder prebidBuilder = prebid != null + ? prebid.toBuilder() + : ExtRequestPrebid.builder(); + + final ExtRequest updatedExt = ExtRequest.of(prebidBuilder + .targeting(ObjectUtils.defaultIfNull(updatedTargeting, + getIfNotNull(prebid, ExtRequestPrebid::getTargeting))) + .cache(ObjectUtils.defaultIfNull(updatedCache, + getIfNotNull(prebid, ExtRequestPrebid::getCache))) + .channel(ObjectUtils.defaultIfNull(updatedChannel, + getIfNotNull(prebid, ExtRequestPrebid::getChannel))) + .pbs(ObjectUtils.defaultIfNull(updatedPbs, + getIfNotNull(prebid, ExtRequestPrebid::getPbs))) + .build()); + updatedExt.addProperties(ext.getProperties()); + + return updatedExt; + } + + return null; + } + + /** + * Iterates through impressions to check what media types each impression has and add them to the resulting set. + * If all four media types are present - no point to look any further. + */ + private static Set getImpMediaTypes(List imps) { + final Set impMediaTypes = new HashSet<>(); + + if (CollectionUtils.isEmpty(imps)) { + return impMediaTypes; + } + + for (Imp imp : imps) { + resolveImpMediaTypes(imp, impMediaTypes); + if (impMediaTypes.size() >= 4) { + break; + } + } + return impMediaTypes; + } + + /** + * Returns populated {@link ExtRequestPrebidPbs} or null if no changes were applied. + */ + private ExtRequestPrebidPbs pbsOrNull(BidRequest bidRequest, String endpoint) { + final String existingEndpoint = getIfNotNull(getIfNotNull(bidRequest.getExt().getPrebid(), + ExtRequestPrebid::getPbs), + ExtRequestPrebidPbs::getEndpoint); + + if (StringUtils.isNotBlank(existingEndpoint)) { + return null; + } + + return ExtRequestPrebidPbs.of(endpoint); + } + + /** + * Adds an existing media type to a set. + */ + private static void resolveImpMediaTypes(Imp imp, Set impsMediaTypes) { + if (imp.getBanner() != null) { + impsMediaTypes.add(BidType.banner); + } + if (imp.getVideo() != null) { + impsMediaTypes.add(BidType.video); + } + if (imp.getAudio() != null) { + impsMediaTypes.add(BidType.audio); + } + if (imp.getXNative() != null) { + impsMediaTypes.add(BidType.xNative); + } + } + + /** + * Returns populated {@link ExtRequestTargeting} or null if no changes were applied. + */ + private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List imps) { + final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; + + final boolean isTargetingNotNull = targeting != null; + final boolean isPriceGranularityNull = isTargetingNotNull + && (targeting.getPricegranularity() == null || targeting.getPricegranularity().isNull()); + final boolean isPriceGranularityTextual = isTargetingNotNull && !isPriceGranularityNull + && targeting.getPricegranularity().isTextual(); + final boolean isIncludeWinnersNull = isTargetingNotNull && targeting.getIncludewinners() == null; + final boolean isIncludeBidderKeysNull = isTargetingNotNull && targeting.getIncludebidderkeys() == null; + + if (isPriceGranularityNull || isPriceGranularityTextual || isIncludeWinnersNull || isIncludeBidderKeysNull) { + return targeting.toBuilder() + .pricegranularity(resolvePriceGranularity(targeting, isPriceGranularityNull, + isPriceGranularityTextual, imps)) + .includewinners(isIncludeWinnersNull || targeting.getIncludewinners()) + .includebidderkeys(isIncludeBidderKeysNull + ? !isWinningOnly(prebid.getCache()) + : targeting.getIncludebidderkeys()) + .build(); + } + return null; + } + + /** + * Returns winning only flag value. + */ + private boolean isWinningOnly(ExtRequestPrebidCache cache) { + final Boolean cacheWinningOnly = cache != null ? cache.getWinningonly() : null; + return ObjectUtils.defaultIfNull(cacheWinningOnly, shouldCacheOnlyWinningBids); + } + + /** + * Populates priceGranularity with converted value. + *

+ * In case of missing Json node and missing media type price granularities - sets default custom value. + * In case of valid string price granularity replaced it with appropriate custom view. + * In case of invalid string value throws {@link InvalidRequestException}. + */ + private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, boolean isPriceGranularityNull, + boolean isPriceGranularityTextual, List imps) { + + final boolean hasAllMediaTypes = checkExistingMediaTypes(targeting.getMediatypepricegranularity()) + .containsAll(getImpMediaTypes(imps)); + + if (isPriceGranularityNull && !hasAllMediaTypes) { + return mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)); + } + + final JsonNode priceGranularityNode = targeting.getPricegranularity(); + if (isPriceGranularityTextual) { + final PriceGranularity priceGranularity; + try { + priceGranularity = PriceGranularity.createFromString(priceGranularityNode.textValue()); + } catch (PreBidException e) { + throw new InvalidRequestException(e.getMessage()); + } + return mapper.mapper().valueToTree(ExtPriceGranularity.from(priceGranularity)); + } + + return priceGranularityNode; + } + + /** + * Checks {@link ExtMediaTypePriceGranularity} object for present media types and returns a set of existing ones. + */ + private static Set checkExistingMediaTypes(ExtMediaTypePriceGranularity mediaTypePriceGranularity) { + if (mediaTypePriceGranularity == null) { + return Collections.emptySet(); + } + final Set priceGranularityTypes = new HashSet<>(); + + final JsonNode banner = mediaTypePriceGranularity.getBanner(); + if (banner != null && !banner.isNull()) { + priceGranularityTypes.add(BidType.banner); + } + final JsonNode video = mediaTypePriceGranularity.getVideo(); + if (video != null && !video.isNull()) { + priceGranularityTypes.add(BidType.video); + } + final JsonNode xNative = mediaTypePriceGranularity.getXNative(); + if (xNative != null && !xNative.isNull()) { + priceGranularityTypes.add(BidType.xNative); + } + return priceGranularityTypes; + } + + /** + * Returns populated {@link ExtRequestPrebidCache} or null if no changes were applied. + */ + private ExtRequestPrebidCache cacheOrNull(ExtRequestPrebid prebid) { + final ExtRequestPrebidCache cache = prebid != null ? prebid.getCache() : null; + final Boolean cacheWinningOnly = cache != null ? cache.getWinningonly() : null; + if (cacheWinningOnly == null && shouldCacheOnlyWinningBids) { + return ExtRequestPrebidCache.of( + getIfNotNull(cache, ExtRequestPrebidCache::getBids), + getIfNotNull(cache, ExtRequestPrebidCache::getVastxml), + true); + } + return null; + } + + /** + * Returns populated {@link ExtRequestPrebidChannel} or null if no changes were applied. + */ + private ExtRequestPrebidChannel channelOrNull(ExtRequestPrebid prebid, BidRequest bidRequest) { + final String existingChannelName = getIfNotNull(getIfNotNull(prebid, + ExtRequestPrebid::getChannel), + ExtRequestPrebidChannel::getName); + + if (StringUtils.isNotBlank(existingChannelName)) { + return null; + } + + if (bidRequest.getApp() != null) { + return ExtRequestPrebidChannel.of(APP_CHANNEL); + } else if (bidRequest.getSite() != null) { + return ExtRequestPrebidChannel.of(WEB_CHANNEL); + } + + return null; + } + + /** + * Returns updated request.at or null if nothing changed. + *

+ * Set the auction type to 1 if it wasn't on the request, since header bidding is generally a first-price auction. + */ + private static Integer resolveAt(Integer at) { + return at == null || at == 0 ? 1 : null; + } + + /** + * Returns default list of currencies if it wasn't on the request, otherwise null. + */ + private List resolveCurrencies(List currencies) { + return CollectionUtils.isEmpty(currencies) && adServerCurrency != null + ? Collections.singletonList(adServerCurrency) + : null; + } + + /** + * Determines request timeout with the help of {@link TimeoutResolver}. + * Returns resolved new value or null if existing request timeout doesn't need to update. + */ + private static Long resolveTmax(Long requestTimeout, TimeoutResolver timeoutResolver) { + final long timeout = timeoutResolver.resolve(requestTimeout); + return !Objects.equals(requestTimeout, timeout) ? timeout : null; + } + + private static R getIfNotNull(T target, Function getter) { + return target != null ? getter.apply(target) : null; + } +} diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java new file mode 100644 index 00000000000..100d4e2d66b --- /dev/null +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -0,0 +1,486 @@ +package org.prebid.server.auction.requestfactory; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import io.vertx.ext.web.RoutingContext; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.IpAddressHelper; +import org.prebid.server.auction.StoredRequestProcessor; +import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.DebugContext; +import org.prebid.server.auction.model.IpAddress; +import org.prebid.server.cookie.UidsCookieService; +import org.prebid.server.deals.DealsProcessor; +import org.prebid.server.deals.model.DeepDebugLog; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.exception.BlacklistedAccountException; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.exception.UnauthorizedAccountException; +import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.geolocation.model.GeoInfo; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.metric.MetricName; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.Endpoint; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; +import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; +import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountStatus; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.validation.RequestValidator; +import org.prebid.server.validation.model.ValidationResult; + +import java.time.Clock; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +public class Ortb2RequestFactory { + + private static final Logger logger = LoggerFactory.getLogger(Ortb2RequestFactory.class); + + private static final ConditionalLogger EMPTY_ACCOUNT_LOGGER = new ConditionalLogger("empty_account", logger); + private static final ConditionalLogger UNKNOWN_ACCOUNT_LOGGER = new ConditionalLogger("unknown_account", logger); + + private final boolean enforceValidAccount; + private final List blacklistedAccounts; + private final UidsCookieService uidsCookieService; + private final RequestValidator requestValidator; + private final TimeoutResolver timeoutResolver; + private final TimeoutFactory timeoutFactory; + private final StoredRequestProcessor storedRequestProcessor; + private final ApplicationSettings applicationSettings; + private final DealsProcessor dealsProcessor; + private final IpAddressHelper ipAddressHelper; + private final HookStageExecutor hookStageExecutor; + private final Clock clock; + + public Ortb2RequestFactory(boolean enforceValidAccount, + List blacklistedAccounts, + UidsCookieService uidsCookieService, + RequestValidator requestValidator, + TimeoutResolver timeoutResolver, + TimeoutFactory timeoutFactory, + StoredRequestProcessor storedRequestProcessor, + ApplicationSettings applicationSettings, + IpAddressHelper ipAddressHelper, + HookStageExecutor hookStageExecutor, + DealsProcessor dealsProcessor, + Clock clock) { + + this.enforceValidAccount = enforceValidAccount; + this.blacklistedAccounts = Objects.requireNonNull(blacklistedAccounts); + this.uidsCookieService = Objects.requireNonNull(uidsCookieService); + this.requestValidator = Objects.requireNonNull(requestValidator); + this.timeoutResolver = Objects.requireNonNull(timeoutResolver); + this.timeoutFactory = Objects.requireNonNull(timeoutFactory); + this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.applicationSettings = Objects.requireNonNull(applicationSettings); + this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); + this.dealsProcessor = dealsProcessor; + this.clock = Objects.requireNonNull(clock); + } + + public AuctionContext createAuctionContext(Endpoint endpoint, MetricName requestTypeMetric) { + return AuctionContext.builder() + .requestTypeMetric(requestTypeMetric) + .prebidErrors(new ArrayList<>()) + .debugWarnings(new ArrayList<>()) + .hookExecutionContext(HookExecutionContext.of(endpoint)) + .debugContext(DebugContext.empty()) + .requestRejected(false) + .txnLog(TxnLog.create()) + .debugHttpCalls(new HashMap<>()) + .build(); + } + + public AuctionContext enrichAuctionContext(AuctionContext auctionContext, + HttpRequestContext httpRequest, + BidRequest bidRequest, + long startTime) { + + return auctionContext.toBuilder() + .httpRequest(httpRequest) + .uidsCookie(uidsCookieService.parseFromRequest(httpRequest)) + .bidRequest(bidRequest) + .timeout(timeout(bidRequest, startTime)) + .debugContext(debugContext(bidRequest)) + .deepDebugLog(createDeepDebugLog(bidRequest)) + .build(); + } + + public Future fetchAccountWithoutStoredRequestLookup(AuctionContext auctionContext) { + return fetchAccount(auctionContext, false); + } + + public Future fetchAccount(AuctionContext auctionContext) { + return fetchAccount(auctionContext, true); + } + + private Future fetchAccount(AuctionContext auctionContext, boolean isLookupStoredRequest) { + final BidRequest bidRequest = auctionContext.getBidRequest(); + final Timeout timeout = auctionContext.getTimeout(); + final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); + + return findAccountIdFrom(bidRequest, isLookupStoredRequest) + .map(this::validateIfAccountBlacklisted) + .compose(accountId -> loadAccount(timeout, httpRequest, accountId)); + } + + /** + * Performs thorough validation of fully constructed {@link BidRequest} that is going to be used to hold an auction. + */ + public BidRequest validateRequest(BidRequest bidRequest) { + final ValidationResult validationResult = requestValidator.validate(bidRequest); + if (validationResult.hasErrors()) { + throw new InvalidRequestException(validationResult.getErrors()); + } + return bidRequest; + } + + public BidRequest enrichBidRequestWithAccountAndPrivacyData(AuctionContext auctionContext) { + final BidRequest bidRequest = auctionContext.getBidRequest(); + final Account account = auctionContext.getAccount(); + final PrivacyContext privacyContext = auctionContext.getPrivacyContext(); + + final ExtRequest requestExt = bidRequest.getExt(); + final ExtRequest enrichedRequestExt = enrichExtRequest(requestExt, account); + + final Device device = bidRequest.getDevice(); + final Device enrichedDevice = enrichDevice(device, privacyContext); + + if (enrichedRequestExt != null || enrichedDevice != null) { + return bidRequest.toBuilder() + .ext(ObjectUtils.defaultIfNull(enrichedRequestExt, requestExt)) + .device(ObjectUtils.defaultIfNull(enrichedDevice, device)) + .build(); + } + + return bidRequest; + } + + public Future executeEntrypointHooks(RoutingContext routingContext, + String body, + AuctionContext auctionContext) { + + return hookStageExecutor.executeEntrypointStage( + toCaseInsensitiveMultiMap(routingContext.queryParams()), + toCaseInsensitiveMultiMap(routingContext.request().headers()), + body, + auctionContext.getHookExecutionContext()) + .map(stageResult -> toHttpRequest(stageResult, routingContext, auctionContext)); + } + + public Future executeRawAuctionRequestHooks(AuctionContext auctionContext) { + return hookStageExecutor.executeRawAuctionRequestStage(auctionContext) + .map(stageResult -> toBidRequest(stageResult, auctionContext)); + } + + public Future executeProcessedAuctionRequestHooks(AuctionContext auctionContext) { + return hookStageExecutor.executeProcessedAuctionRequestStage(auctionContext) + .map(stageResult -> toBidRequest(stageResult, auctionContext)); + } + + public Future restoreResultFromRejection(Throwable throwable) { + if (throwable instanceof RejectedRequestException) { + final AuctionContext auctionContext = ((RejectedRequestException) throwable).getAuctionContext(); + + return Future.succeededFuture(auctionContext.withRequestRejected()); + } + + return Future.failedFuture(throwable); + } + + private static HttpRequestContext toHttpRequest(HookStageExecutionResult stageResult, + RoutingContext routingContext, + AuctionContext auctionContext) { + + if (stageResult.isShouldReject()) { + throw new RejectedRequestException(auctionContext); + } + + return HttpRequestContext.builder() + .absoluteUri(routingContext.request().absoluteURI()) + .queryParams(stageResult.getPayload().queryParams()) + .headers(stageResult.getPayload().headers()) + .body(stageResult.getPayload().body()) + .scheme(routingContext.request().scheme()) + .remoteHost(routingContext.request().remoteAddress().host()) + .build(); + } + + private static BidRequest toBidRequest(HookStageExecutionResult stageResult, + AuctionContext auctionContext) { + + if (stageResult.isShouldReject()) { + throw new RejectedRequestException(auctionContext); + } + + return stageResult.getPayload().bidRequest(); + } + + private static DebugContext debugContext(BidRequest bidRequest) { + final ExtRequestPrebid extRequestPrebid = getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); + + final boolean debugEnabled = Objects.equals(bidRequest.getTest(), 1) + || Objects.equals(getIfNotNull(extRequestPrebid, ExtRequestPrebid::getDebug), 1); + + final TraceLevel traceLevel = getIfNotNull(extRequestPrebid, ExtRequestPrebid::getTrace); + + return DebugContext.of(debugEnabled, traceLevel); + } + + public Future populateDealsInfo(AuctionContext auctionContext) { + return dealsProcessor != null + ? dealsProcessor.populateDealsInfo(auctionContext) + : Future.succeededFuture(auctionContext); + } + + /** + * Returns {@link Timeout} based on request.tmax and adjustment value of {@link TimeoutResolver}. + */ + private Timeout timeout(BidRequest bidRequest, long startTime) { + final long resolvedRequestTimeout = timeoutResolver.resolve(bidRequest.getTmax()); + final long timeout = timeoutResolver.adjustTimeout(resolvedRequestTimeout); + return timeoutFactory.create(startTime, timeout); + } + + private Future findAccountIdFrom(BidRequest bidRequest, boolean isLookupStoredRequest) { + final String accountId = accountIdFrom(bidRequest); + return StringUtils.isNotBlank(accountId) || !isLookupStoredRequest + ? Future.succeededFuture(accountId) + : storedRequestProcessor.processStoredRequests(accountId, bidRequest) + .map(this::accountIdFrom); + } + + private String validateIfAccountBlacklisted(String accountId) { + if (CollectionUtils.isNotEmpty(blacklistedAccounts) + && StringUtils.isNotBlank(accountId) + && blacklistedAccounts.contains(accountId)) { + + throw new BlacklistedAccountException( + String.format("Prebid-server has blacklisted Account ID: %s, please " + + "reach out to the prebid server host.", accountId)); + } + return accountId; + } + + private Future loadAccount(Timeout timeout, + HttpRequestContext httpRequest, + String accountId) { + return StringUtils.isBlank(accountId) + ? responseForEmptyAccount(httpRequest) + : applicationSettings.getAccountById(accountId, timeout) + .compose(this::ensureAccountActive, + exception -> accountFallback(exception, accountId, httpRequest)); + } + + /** + * Extracts publisher id either from {@link BidRequest}.app.publisher or {@link BidRequest}.site.publisher. + * If neither is present returns empty string. + */ + private String accountIdFrom(BidRequest bidRequest) { + final App app = bidRequest.getApp(); + final Publisher appPublisher = app != null ? app.getPublisher() : null; + final Site site = bidRequest.getSite(); + final Publisher sitePublisher = site != null ? site.getPublisher() : null; + + final Publisher publisher = ObjectUtils.defaultIfNull(appPublisher, sitePublisher); + final String publisherId = publisher != null ? resolvePublisherId(publisher) : null; + return ObjectUtils.defaultIfNull(publisherId, StringUtils.EMPTY); + } + + /** + * Resolves what value should be used as a publisher id - either taken from publisher.ext.parentAccount + * or publisher.id in this respective priority. + */ + private String resolvePublisherId(Publisher publisher) { + final String parentAccountId = parentAccountIdFromExtPublisher(publisher.getExt()); + return ObjectUtils.defaultIfNull(parentAccountId, publisher.getId()); + } + + /** + * Parses publisher.ext and returns parentAccount value from it. Returns null if any parsing error occurs. + */ + private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { + final ExtPublisherPrebid extPublisherPrebid = extPublisher != null ? extPublisher.getPrebid() : null; + return extPublisherPrebid != null ? StringUtils.stripToNull(extPublisherPrebid.getParentAccount()) : null; + } + + private Future responseForEmptyAccount(HttpRequestContext httpRequest) { + EMPTY_ACCOUNT_LOGGER.warn(accountErrorMessage("Account not specified", httpRequest), 100); + return responseForUnknownAccount(StringUtils.EMPTY); + } + + private static String accountErrorMessage(String message, HttpRequestContext httpRequest) { + return String.format( + "%s, Url: %s and Referer: %s", + message, + httpRequest.getAbsoluteUri(), + httpRequest.getHeaders().get(HttpUtil.REFERER_HEADER)); + } + + private Future accountFallback(Throwable exception, + String accountId, + HttpRequestContext httpRequest) { + + if (exception instanceof PreBidException) { + UNKNOWN_ACCOUNT_LOGGER.warn(accountErrorMessage(exception.getMessage(), httpRequest), 100); + } else { + logger.warn("Error occurred while fetching account: {0}", exception.getMessage()); + logger.debug("Error occurred while fetching account", exception); + } + + // hide all errors occurred while fetching account + return responseForUnknownAccount(accountId); + } + + private Future responseForUnknownAccount(String accountId) { + return enforceValidAccount + ? Future.failedFuture(new UnauthorizedAccountException( + String.format("Unauthorized account id: %s", accountId), accountId)) + : Future.succeededFuture(Account.empty(accountId)); + } + + private Future ensureAccountActive(Account account) { + final String accountId = account.getId(); + + return account.getStatus() == AccountStatus.inactive + ? Future.failedFuture(new UnauthorizedAccountException( + String.format("Account %s is inactive", accountId), accountId)) + : Future.succeededFuture(account); + } + + private ExtRequest enrichExtRequest(ExtRequest ext, Account account) { + final ExtRequestPrebid prebidExt = getIfNotNull(ext, ExtRequest::getPrebid); + final String integration = getIfNotNull(prebidExt, ExtRequestPrebid::getIntegration); + final String accountDefaultIntegration = accountDefaultIntegration(account); + + if (StringUtils.isBlank(integration) && StringUtils.isNotBlank(accountDefaultIntegration)) { + final ExtRequestPrebid.ExtRequestPrebidBuilder prebidExtBuilder = + prebidExt != null ? prebidExt.toBuilder() : ExtRequestPrebid.builder(); + + prebidExtBuilder.integration(accountDefaultIntegration); + + final ExtRequest updatedExt = ExtRequest.of(prebidExtBuilder.build()); + if (ext != null) { + updatedExt.addProperties(ext.getProperties()); + } + + return updatedExt; + } + + return null; + } + + private Device enrichDevice(Device device, PrivacyContext privacyContext) { + final String ipAddress = privacyContext.getIpAddress(); + final IpAddress ip = ipAddressHelper.toIpAddress(ipAddress); + + final String ipV4InRequest = getIfNotNull(device, Device::getIp); + final String ipV4 = ip != null && ip.getVersion() == IpAddress.IP.v4 ? ipAddress : null; + final boolean shouldUpdateIpV4 = ipV4 != null && !Objects.equals(ipV4InRequest, ipV4); + + final String ipV6InRequest = getIfNotNull(device, Device::getIpv6); + final String ipV6 = ip != null && ip.getVersion() == IpAddress.IP.v6 ? ipAddress : null; + final boolean shouldUpdateIpV6 = ipV6 != null && !Objects.equals(ipV6InRequest, ipV6); + + final Geo geo = getIfNotNull(device, Device::getGeo); + final String countryInRequest = getIfNotNull(geo, Geo::getCountry); + final String country = getIfNotNull(privacyContext.getTcfContext().getGeoInfo(), GeoInfo::getCountry); + final boolean shouldUpdateCountry = country != null && !Objects.equals(countryInRequest, country); + + if (shouldUpdateIpV4 || shouldUpdateIpV6 || shouldUpdateCountry) { + final Device.DeviceBuilder deviceBuilder = device != null ? device.toBuilder() : Device.builder(); + + if (shouldUpdateIpV4) { + deviceBuilder.ip(ipV4); + } + + if (shouldUpdateIpV6) { + deviceBuilder.ipv6(ipV6); + } + + if (shouldUpdateCountry) { + final Geo.GeoBuilder geoBuilder = geo != null ? geo.toBuilder() : Geo.builder(); + geoBuilder.country(country); + deviceBuilder.geo(geoBuilder.build()); + } + + return deviceBuilder.build(); + } + + return null; + } + + private static String accountDefaultIntegration(Account account) { + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + + return accountAuctionConfig != null ? accountAuctionConfig.getDefaultIntegration() : null; + } + + private static CaseInsensitiveMultiMap toCaseInsensitiveMultiMap(MultiMap originalMap) { + final CaseInsensitiveMultiMap.Builder mapBuilder = CaseInsensitiveMultiMap.builder(); + originalMap.entries().forEach(entry -> mapBuilder.add(entry.getKey(), entry.getValue())); + + return mapBuilder.build(); + } + + private DeepDebugLog createDeepDebugLog(BidRequest bidRequest) { + final ExtRequest ext = bidRequest.getExt(); + return DeepDebugLog.create(ext != null && isDeepDebugEnabled(ext), clock); + } + + /** + * Determines deep debug flag from {@link ExtRequest}. + */ + private static boolean isDeepDebugEnabled(ExtRequest extRequest) { + final ExtRequestPrebid extRequestPrebid = extRequest != null ? extRequest.getPrebid() : null; + return extRequestPrebid != null && extRequestPrebid.getTrace() == TraceLevel.verbose; + } + + private static R getIfNotNull(T target, Function getter) { + return target != null ? getter.apply(target) : null; + } + + static class RejectedRequestException extends RuntimeException { + + private final AuctionContext auctionContext; + + RejectedRequestException(AuctionContext auctionContext) { + this.auctionContext = auctionContext; + } + + public AuctionContext getAuctionContext() { + return auctionContext; + } + } +} diff --git a/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java new file mode 100644 index 00000000000..dbf7629ca3f --- /dev/null +++ b/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java @@ -0,0 +1,239 @@ +package org.prebid.server.auction.requestfactory; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.video.BidRequestVideo; +import com.iab.openrtb.request.video.Pod; +import com.iab.openrtb.request.video.PodError; +import com.iab.openrtb.request.video.Podconfig; +import io.vertx.core.Future; +import io.vertx.ext.web.RoutingContext; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.PrivacyEnforcementService; +import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.VideoStoredRequestProcessor; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.WithPodErrors; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.MetricName; +import org.prebid.server.model.Endpoint; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; +import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public class VideoRequestFactory { + + private static final String ENDPOINT = Endpoint.openrtb2_video.value(); + + private final int maxRequestSize; + private final boolean enforceStoredRequest; + + private final Ortb2RequestFactory ortb2RequestFactory; + private final Ortb2ImplicitParametersResolver paramsResolver; + private final VideoStoredRequestProcessor storedRequestProcessor; + private final PrivacyEnforcementService privacyEnforcementService; + private final TimeoutResolver timeoutResolver; + private final JacksonMapper mapper; + + public VideoRequestFactory(int maxRequestSize, + boolean enforceStoredRequest, + Ortb2RequestFactory ortb2RequestFactory, + Ortb2ImplicitParametersResolver paramsResolver, + VideoStoredRequestProcessor storedRequestProcessor, + PrivacyEnforcementService privacyEnforcementService, + TimeoutResolver timeoutResolver, + JacksonMapper mapper) { + + this.enforceStoredRequest = enforceStoredRequest; + this.maxRequestSize = maxRequestSize; + this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); + this.paramsResolver = Objects.requireNonNull(paramsResolver); + this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); + this.timeoutResolver = Objects.requireNonNull(timeoutResolver); + this.mapper = Objects.requireNonNull(mapper); + } + + /** + * Creates {@link AuctionContext} and {@link List} of {@link PodError} based on {@link RoutingContext}. + */ + public Future> fromRequest(RoutingContext routingContext, long startTime) { + final String body; + try { + body = extractAndValidateBody(routingContext); + } catch (InvalidRequestException e) { + return Future.failedFuture(e); + } + + final List podErrors = new ArrayList<>(); + + final AuctionContext initialAuctionContext = ortb2RequestFactory.createAuctionContext( + Endpoint.openrtb2_video, MetricName.video); + + return ortb2RequestFactory.executeEntrypointHooks(routingContext, body, initialAuctionContext) + .compose(httpRequest -> createBidRequest(httpRequest) + .map(bidRequestWithErrors -> populatePodErrors( + bidRequestWithErrors.getPodErrors(), podErrors, bidRequestWithErrors)) + .map(bidRequestWithErrors -> ortb2RequestFactory.enrichAuctionContext( + initialAuctionContext, httpRequest, bidRequestWithErrors.getData(), startTime))) + + .compose(auctionContext -> ortb2RequestFactory.fetchAccountWithoutStoredRequestLookup(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> privacyEnforcementService.contextFromBidRequest(auctionContext) + .map(auctionContext::with)) + + .map(auctionContext -> auctionContext.with( + ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext))) + + .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) + .map(auctionContext::with)) + + .compose(ortb2RequestFactory::populateDealsInfo) + + .recover(ortb2RequestFactory::restoreResultFromRejection) + + .map(auctionContext -> WithPodErrors.of(auctionContext, podErrors)); + } + + private String extractAndValidateBody(RoutingContext routingContext) { + final String body = routingContext.getBodyAsString(); + if (body == null) { + throw new InvalidRequestException("Incoming request has no body"); + } + + if (body.length() > maxRequestSize) { + throw new InvalidRequestException(String.format("Request size exceeded max size of %d bytes.", + maxRequestSize)); + } + + return body; + } + + private Future> createBidRequest(HttpRequestContext httpRequest) { + + final BidRequestVideo bidRequestVideo; + try { + bidRequestVideo = parseRequest(httpRequest); + } catch (InvalidRequestException e) { + return Future.failedFuture(e); + } + + final String storedRequestId = bidRequestVideo.getStoredrequestid(); + if (StringUtils.isBlank(storedRequestId) && enforceStoredRequest) { + return Future.failedFuture(new InvalidRequestException("Unable to find required stored request id")); + } + + final Set podConfigIds = podConfigIds(bidRequestVideo); + + return storedRequestProcessor.processVideoRequest( + accountIdFrom(bidRequestVideo), storedRequestId, podConfigIds, bidRequestVideo) + .map(bidRequestToErrors -> fillImplicitParametersAndValidate(httpRequest, bidRequestToErrors)); + } + + /** + * Parses request body to {@link BidRequestVideo}. + *

+ * Throws {@link InvalidRequestException} if body is empty, exceeds max request size or couldn't be deserialized. + */ + private BidRequestVideo parseRequest(HttpRequestContext httpRequest) { + try { + final BidRequestVideo bidRequestVideo = mapper.decodeValue(httpRequest.getBody(), BidRequestVideo.class); + return insertDeviceUa(httpRequest, bidRequestVideo); + } catch (DecodeException e) { + throw new InvalidRequestException(e.getMessage()); + } + } + + private BidRequestVideo insertDeviceUa(HttpRequestContext httpRequest, BidRequestVideo bidRequestVideo) { + final Device device = bidRequestVideo.getDevice(); + final String deviceUa = device != null ? device.getUa() : null; + if (StringUtils.isBlank(deviceUa)) { + final String userAgentHeader = httpRequest.getHeaders().get(HttpUtil.USER_AGENT_HEADER); + if (StringUtils.isEmpty(userAgentHeader)) { + throw new InvalidRequestException("Device.UA and User-Agent Header is not presented"); + } + final Device.DeviceBuilder deviceBuilder = device == null ? Device.builder() : device.toBuilder(); + + return bidRequestVideo.toBuilder() + .device(deviceBuilder + .ua(userAgentHeader) + .build()) + .build(); + } + return bidRequestVideo; + } + + /** + * Extracts publisher id either from {@link BidRequestVideo}.app.publisher + * or {@link BidRequestVideo}.site.publisher. If neither is present returns empty string. + */ + private String accountIdFrom(BidRequestVideo bidRequestVideo) { + final App app = bidRequestVideo.getApp(); + final Publisher appPublisher = app != null ? app.getPublisher() : null; + final Site site = bidRequestVideo.getSite(); + final Publisher sitePublisher = site != null ? site.getPublisher() : null; + + final Publisher publisher = ObjectUtils.defaultIfNull(appPublisher, sitePublisher); + final String publisherId = publisher != null ? resolvePublisherId(publisher) : null; + return ObjectUtils.defaultIfNull(publisherId, StringUtils.EMPTY); + } + + /** + * Resolves what value should be used as a publisher id - either taken from publisher.ext.parentAccount + * or publisher.id in this respective priority. + */ + private String resolvePublisherId(Publisher publisher) { + final String parentAccountId = parentAccountIdFromExtPublisher(publisher.getExt()); + return ObjectUtils.defaultIfNull(parentAccountId, publisher.getId()); + } + + /** + * Parses publisher.ext and returns parentAccount value from it. Returns null if any parsing error occurs. + */ + private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { + final ExtPublisherPrebid extPublisherPrebid = extPublisher != null ? extPublisher.getPrebid() : null; + return extPublisherPrebid != null ? StringUtils.stripToNull(extPublisherPrebid.getParentAccount()) : null; + } + + private static Set podConfigIds(BidRequestVideo incomingBidRequest) { + final Podconfig podconfig = incomingBidRequest.getPodconfig(); + if (podconfig != null && CollectionUtils.isNotEmpty(podconfig.getPods())) { + return podconfig.getPods().stream() + .map(Pod::getConfigId) + .filter(Objects::nonNull) + .map(String::valueOf) + .collect(Collectors.toSet()); + } + + return Collections.emptySet(); + } + + private static T populatePodErrors(List from, List to, T returnObject) { + to.addAll(from); + return returnObject; + } + + private WithPodErrors fillImplicitParametersAndValidate(HttpRequestContext httpRequest, + WithPodErrors bidRequestToErrors) { + final BidRequest bidRequest = bidRequestToErrors.getData(); + final BidRequest updatedBidRequest = paramsResolver.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT); + final BidRequest validBidRequest = ortb2RequestFactory.validateRequest(updatedBidRequest); + return WithPodErrors.of(validBidRequest, bidRequestToErrors.getPodErrors()); + } +} diff --git a/src/main/java/org/prebid/server/bidder/Adapter.java b/src/main/java/org/prebid/server/bidder/Adapter.java deleted file mode 100644 index 539e3d7a342..00000000000 --- a/src/main/java/org/prebid/server/bidder/Adapter.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.prebid.server.bidder; - -import com.fasterxml.jackson.core.type.TypeReference; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.bidder.model.AdapterHttpRequest; -import org.prebid.server.bidder.model.ExchangeCall; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.proto.response.Bid; - -import java.util.List; - -/** - * Describes the behavior for {@link Adapter} implementations. - *

- * Used by {@link HttpAdapterConnector} while performing requests to exchanges and compose results. - */ -public interface Adapter { - - /** - * Composes list of http request to submit to exchange. - * - * @throws PreBidException if error occurs while adUnitBids validation. - */ - List> makeHttpRequests(AdapterRequest adapterRequest, - PreBidRequestContext preBidRequestContext) throws PreBidException; - - /** - * Extracts bids from exchange response. - * - * @throws PreBidException if error occurs while bids validation. - */ - List extractBids(AdapterRequest adapterRequest, ExchangeCall exchangeCall) - throws PreBidException; - - /** - * If true - {@link org.prebid.server.auction.model.AdapterResponse} will contain bids if at least one valid bid - * exists, otherwise will contain - * error. - *

- * If false - {@link org.prebid.server.auction.model.AdapterResponse} will contain error if at least one error - * occurs during processing. - */ - boolean tolerateErrors(); - - /** - * A class that will be used to parse response from exchange. - */ - TypeReference responseTypeReference(); -} diff --git a/src/main/java/org/prebid/server/bidder/Bidder.java b/src/main/java/org/prebid/server/bidder/Bidder.java index 51acd8c1b2d..089fa2f19d3 100644 --- a/src/main/java/org/prebid/server/bidder/Bidder.java +++ b/src/main/java/org/prebid/server/bidder/Bidder.java @@ -7,6 +7,7 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -34,5 +35,19 @@ public interface Bidder { /** * Extracts targeting from bidder-specific extension. It is safe to assume that {@code ext} is not null. */ - Map extractTargeting(ObjectNode ext); + default Map extractTargeting(ObjectNode ext) { + return Collections.emptyMap(); + } + + /** + * This method is much the same as {@link #makeHttpRequests}, except it is fed the bidder request + * that timed out, and expects that only one notification "request" will be generated. A use case for multiple + * timeout notifications has not been anticipated. + *

+ * Do note that if {@link #makeHttpRequests} returns multiple requests, and more than one of these times out, + * this method will be called once for each timed out request. + */ + default HttpRequest makeTimeoutNotification(HttpRequest httpRequest) { + return null; + } } diff --git a/src/main/java/org/prebid/server/bidder/BidderCatalog.java b/src/main/java/org/prebid/server/bidder/BidderCatalog.java index 483342af873..8ce11cc6e48 100644 --- a/src/main/java/org/prebid/server/bidder/BidderCatalog.java +++ b/src/main/java/org/prebid/server/bidder/BidderCatalog.java @@ -2,6 +2,7 @@ import org.prebid.server.proto.response.BidderInfo; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -12,42 +13,56 @@ import java.util.stream.Collectors; /** - * Provides simple access to all {@link Adapter}s, {@link Bidder}s and {@link Usersyncer}s registered so far. + * Provides simple access to all {@link Bidder}s and {@link Usersyncer}s registered so far. */ public class BidderCatalog { - private static final String ERROR_MESSAGE_TEMPLATE_FOR_DEPRECATED = "%s has been deprecated and is no " - + "longer available. Use %s instead."; + private static final String ERROR_MESSAGE_TEMPLATE_FOR_DEPRECATED = + "%s has been deprecated and is no longer available. Use %s instead."; - private final Map bidderDepsMap; + private final Map bidderDepsMap = new HashMap<>(); private final Map deprecatedNameToError = new HashMap<>(); - private final Map aliases = new HashMap<>(); private final Map vendorIdToBidderName = new HashMap<>(); public BidderCatalog(List bidderDeps) { - bidderDepsMap = Objects.requireNonNull(bidderDeps).stream() - .collect(Collectors.toMap(BidderDeps::getName, Function.identity())); - - for (BidderDeps deps : bidderDeps) { - deprecatedNameToError.putAll(createErrorsForDeprecatedNames(deps.getDeprecatedNames(), deps.getName())); - aliases.putAll(createAliases(deps.getAliases(), deps.getName())); - - final BidderInfo bidderInfo = deps.getBidderInfo(); - final BidderInfo.GdprInfo gdprInfo = bidderInfo != null ? bidderInfo.getGdpr() : null; - final Integer vendorId = gdprInfo != null ? gdprInfo.getVendorId() : null; - if (vendorId != null && vendorId != 0) { - vendorIdToBidderName.put(vendorId, deps.getName()); - } + Objects.requireNonNull(bidderDeps).stream() + .map(BidderDeps::getInstances) + .flatMap(Collection::stream) + .forEach(this::processDeps); + } + + private void processDeps(BidderInstanceDeps deps) { + final String bidderName = deps.getName(); + + validateBidderName(bidderName); + + bidderDepsMap.put(bidderName, deps); + deprecatedNameToError.putAll(createErrorsForDeprecatedNames(deps)); + processVendorId(deps, bidderName); + } + + private void validateBidderName(String bidderName) { + if (bidderDepsMap.containsKey(bidderName)) { + throw new IllegalArgumentException(String.format( + "Duplicate bidder or alias '%s'. Please check the configuration", bidderName)); } } - private Map createErrorsForDeprecatedNames(List deprecatedNames, String name) { - return deprecatedNames.stream().collect(Collectors.toMap(Function.identity(), - deprecatedName -> String.format(ERROR_MESSAGE_TEMPLATE_FOR_DEPRECATED, deprecatedName, name))); + private Map createErrorsForDeprecatedNames(BidderInstanceDeps deps) { + return deps.getDeprecatedNames().stream().collect(Collectors.toMap( + Function.identity(), + deprecatedName -> String.format( + ERROR_MESSAGE_TEMPLATE_FOR_DEPRECATED, deprecatedName, deps.getName()))); } - private Map createAliases(List aliases, String name) { - return aliases.stream().collect(Collectors.toMap(Function.identity(), ignored -> name)); + private void processVendorId(BidderInstanceDeps coreDeps, String bidderName) { + final BidderInfo bidderInfo = coreDeps.getBidderInfo(); + final BidderInfo.GdprInfo gdprInfo = bidderInfo != null ? bidderInfo.getGdpr() : null; + final Integer vendorId = gdprInfo != null ? gdprInfo.getVendorId() : null; + + if (vendorId != null && vendorId != 0) { + vendorIdToBidderName.put(vendorId, bidderName); + } } /** @@ -85,27 +100,6 @@ public String errorForDeprecatedName(String name) { return deprecatedNameToError.get(name); } - /** - * Returns a list of registered bidders' aliases. - */ - public Set aliases() { - return new HashSet<>(aliases.keySet()); - } - - /** - * Tells if given name corresponds to any of the registered bidder's alias. - */ - public boolean isAlias(String name) { - return aliases.containsKey(name); - } - - /** - * Returns original bidder's name for given alias. - */ - public String nameByAlias(String alias) { - return aliases.get(alias); - } - /** * Tells if given bidder is enabled and ready for auction. */ @@ -113,13 +107,6 @@ public boolean isActive(String name) { return bidderDepsMap.containsKey(name) && bidderDepsMap.get(name).getBidderInfo().isEnabled(); } - /** - * Tells if adapter with given name exists. - */ - public boolean isValidAdapterName(String name) { - return bidderDepsMap.containsKey(name) && adapterByName(name) != null; - } - /** * Returns an {@link BidderInfo} registered by the given name or null if there is none. *

@@ -127,7 +114,7 @@ public boolean isValidAdapterName(String name) { * through calling {@link #isValidName(String)}. */ public BidderInfo bidderInfoByName(String name) { - final BidderDeps bidderDeps = bidderDepsMap.get(name); + final BidderInstanceDeps bidderDeps = bidderDepsMap.get(name); return bidderDeps != null ? bidderDeps.getBidderInfo() : null; } @@ -138,7 +125,7 @@ public BidderInfo bidderInfoByName(String name) { * through calling {@link #isValidName(String)}. */ public Integer vendorIdByName(String name) { - final BidderDeps bidderDeps = bidderDepsMap.get(name); + final BidderInstanceDeps bidderDeps = bidderDepsMap.get(name); final BidderInfo bidderInfo = bidderDeps != null ? bidderDeps.getBidderInfo() : null; final BidderInfo.GdprInfo gdprInfo = bidderInfo != null ? bidderInfo.getGdpr() : null; return gdprInfo != null ? gdprInfo.getVendorId() : null; @@ -156,7 +143,7 @@ public String nameByVendorId(Integer vendorId) { */ public Set knownVendorIds() { return bidderDepsMap.values().stream() - .map(BidderDeps::getBidderInfo) + .map(BidderInstanceDeps::getBidderInfo) .map(BidderInfo::getGdpr) .map(BidderInfo.GdprInfo::getVendorId) // TODO change to notNull when migrate from primitives to Object @@ -171,7 +158,7 @@ public Set knownVendorIds() { * through calling {@link #isValidName(String)}. */ public Usersyncer usersyncerByName(String name) { - final BidderDeps bidderDeps = bidderDepsMap.get(name); + final BidderInstanceDeps bidderDeps = bidderDepsMap.get(name); return bidderDeps != null ? bidderDeps.getUsersyncer() : null; } @@ -182,18 +169,7 @@ public Usersyncer usersyncerByName(String name) { * through calling {@link #isValidName(String)}. */ public Bidder bidderByName(String name) { - final BidderDeps bidderDeps = bidderDepsMap.get(name); + final BidderInstanceDeps bidderDeps = bidderDepsMap.get(name); return bidderDeps != null ? bidderDeps.getBidder() : null; } - - /** - * Returns an {@link Adapter} registered by the given name or null if there is none. - *

- * Therefore this method should be called only for names that previously passed validity check - * through calling {@link #isValidName(String)}. - */ - public Adapter adapterByName(String name) { - final BidderDeps bidderDeps = bidderDepsMap.get(name); - return bidderDeps != null ? bidderDeps.getAdapter() : null; - } } diff --git a/src/main/java/org/prebid/server/bidder/BidderDeps.java b/src/main/java/org/prebid/server/bidder/BidderDeps.java index 0464a34b318..6bc506e78c0 100644 --- a/src/main/java/org/prebid/server/bidder/BidderDeps.java +++ b/src/main/java/org/prebid/server/bidder/BidderDeps.java @@ -1,59 +1,14 @@ package org.prebid.server.bidder; -import lombok.Builder; import lombok.Value; -import org.prebid.server.proto.response.BidderInfo; import java.util.List; /** - * Gathers all dependencies for bidder. + * Gathers all dependencies for different instances of the single bidder. */ -@Builder -@Value +@Value(staticConstructor = "of") public class BidderDeps { - /** - * Bidder's name that will be used to determine if a bidder. - *

- * For example, for OpenRTB 2.5 it should participate in auction by - * inspecting bidrequest.imp[i].ext.{bidder} fields. - */ - String name; - - /** - * Bidder's deprecated names. - *

- * Appropriate error message will be added to response.ext.errors.{bidder} - * in case of bidder determined as deprecated. - */ - List deprecatedNames; - - /** - * Bidder's aliases. - *

- * Indicates predefined synonyms for bidder along with regular name. - */ - List aliases; - - /** - * Bidder's meta information is used in {@link org.prebid.server.handler.info.BidderDetailsHandler} handler - */ - BidderInfo bidderInfo; - - /** - * Bidder's user syncer is used in {@link org.prebid.server.handler.CookieSyncHandler} handler and holds cookie - * family name. - */ - Usersyncer usersyncer; - - /** - * Bidder implementation is used in auction handling. - */ - Bidder bidder; - - /** - * Bidder's adapter is used in legacy auction handling. - */ - Adapter adapter; + List instances; } diff --git a/src/main/java/org/prebid/server/bidder/BidderErrorNotifier.java b/src/main/java/org/prebid/server/bidder/BidderErrorNotifier.java new file mode 100644 index 00000000000..57f4330db92 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/BidderErrorNotifier.java @@ -0,0 +1,93 @@ +package org.prebid.server.bidder; + +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.metric.Metrics; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.util.Objects; + +public class BidderErrorNotifier { + + private static final Logger logger = LoggerFactory.getLogger(BidderErrorNotifier.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + + private final int timeoutNotificationTimeoutMs; + private final boolean logTimeoutNotificationResult; + private final boolean logTimeoutNotificationFailureOnly; + private final double logTimeoutNotificationSamplingRate; + private final HttpClient httpClient; + private final Metrics metrics; + + public BidderErrorNotifier(int timeoutNotificationTimeoutMs, + boolean logTimeoutNotificationResult, + boolean logTimeoutNotificationFailureOnly, + double logTimeoutNotificationSamplingRate, + HttpClient httpClient, + Metrics metrics) { + + this.timeoutNotificationTimeoutMs = timeoutNotificationTimeoutMs; + this.logTimeoutNotificationResult = logTimeoutNotificationResult; + this.logTimeoutNotificationFailureOnly = logTimeoutNotificationFailureOnly; + this.logTimeoutNotificationSamplingRate = logTimeoutNotificationSamplingRate; + this.httpClient = Objects.requireNonNull(httpClient); + this.metrics = Objects.requireNonNull(metrics); + } + + public HttpCall processTimeout(HttpCall httpCall, Bidder bidder) { + final BidderError error = httpCall.getError(); + + if (error != null && error.getType() == BidderError.Type.timeout) { + final HttpRequest timeoutNotification = bidder.makeTimeoutNotification(httpCall.getRequest()); + if (timeoutNotification != null) { + httpClient.request( + timeoutNotification.getMethod(), + timeoutNotification.getUri(), + timeoutNotification.getHeaders(), + timeoutNotification.getBody(), + timeoutNotificationTimeoutMs) + .map(response -> handleTimeoutNotificationSuccess(response, timeoutNotification)) + .otherwise(exception -> handleTimeoutNotificationFailure(exception, timeoutNotification)); + } + } + + return httpCall; + } + + private Void handleTimeoutNotificationSuccess(HttpClientResponse response, HttpRequest timeoutNotification) { + final boolean isSuccessful = response.getStatusCode() >= 200 && response.getStatusCode() < 300; + + metrics.updateTimeoutNotificationMetric(isSuccessful); + + if (logTimeoutNotificationResult && !(logTimeoutNotificationFailureOnly && isSuccessful)) { + conditionalLogger.warn( + String.format( + "Notified bidder about timeout. Status code: %s. Request body: %s", + response.getStatusCode(), + timeoutNotification.getBody()), + logTimeoutNotificationSamplingRate); + } + + return null; + } + + private Void handleTimeoutNotificationFailure(Throwable exception, HttpRequest timeoutNotification) { + metrics.updateTimeoutNotificationMetric(false); + + if (logTimeoutNotificationResult) { + conditionalLogger.warn( + String.format( + "Error occurred while notifying bidder about timeout. Error message: %s. Request body: %s", + exception.getMessage(), + timeoutNotification.getBody()), + logTimeoutNotificationSamplingRate); + } + + return null; + } +} diff --git a/src/main/java/org/prebid/server/bidder/BidderInstanceDeps.java b/src/main/java/org/prebid/server/bidder/BidderInstanceDeps.java new file mode 100644 index 00000000000..840d8485c6f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/BidderInstanceDeps.java @@ -0,0 +1,47 @@ +package org.prebid.server.bidder; + +import lombok.Builder; +import lombok.Value; +import org.prebid.server.proto.response.BidderInfo; + +import java.util.List; + +/** + * Gathers all dependencies for single bidder instance that may represent core bidder or its alias. + */ +@Builder +@Value +public class BidderInstanceDeps { + + /** + * Bidder's name that will be used to determine if a bidder. + *

+ * For example, for OpenRTB 2.5 it should participate in auction by + * inspecting bidrequest.imp[i].ext.{bidder} fields. + */ + String name; + + /** + * Bidder's deprecated names. + *

+ * Appropriate error message will be added to response.ext.errors.{bidder} + * in case of bidder determined as deprecated. + */ + List deprecatedNames; + + /** + * Bidder's meta information is used in {@link org.prebid.server.handler.info.BidderDetailsHandler} handler + */ + BidderInfo bidderInfo; + + /** + * Bidder's user syncer is used in {@link org.prebid.server.handler.CookieSyncHandler} handler and holds cookie + * family name. + */ + Usersyncer usersyncer; + + /** + * Bidder implementation is used in auction handling. + */ + Bidder bidder; +} diff --git a/src/main/java/org/prebid/server/bidder/DealsBidderRequestCompletionTrackerFactory.java b/src/main/java/org/prebid/server/bidder/DealsBidderRequestCompletionTrackerFactory.java new file mode 100644 index 00000000000..8be85f88415 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/DealsBidderRequestCompletionTrackerFactory.java @@ -0,0 +1,96 @@ +package org.prebid.server.bidder; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Pmp; +import com.iab.openrtb.response.Bid; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.model.BidderBid; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class DealsBidderRequestCompletionTrackerFactory implements BidderRequestCompletionTrackerFactory { + + public BidderRequestCompletionTracker create(BidRequest bidRequest) { + final Map impToTopDealMap = new HashMap<>(); + for (final Imp imp : bidRequest.getImp()) { + final Pmp pmp = imp.getPmp(); + final List deals = pmp != null ? pmp.getDeals() : null; + final Deal topDeal = CollectionUtils.isNotEmpty(deals) ? deals.get(0) : null; + + impToTopDealMap.put(imp.getId(), topDeal != null ? topDeal.getId() : null); + } + + return !impToTopDealMap.containsValue(null) + ? new TopDealsReceivedTracker(impToTopDealMap) + : new NeverCompletedTracker(); + } + + private static class NeverCompletedTracker implements BidderRequestCompletionTracker { + + @Override + public Future future() { + return Future.failedFuture("No deals to wait for"); + } + + @Override + public void processBids(List bids) { + // no need to process bid when no deals to wait for + } + } + + private static class TopDealsReceivedTracker implements BidderRequestCompletionTracker { + + private final Map impToTopDealMap; + + private final Promise completionPromise; + + private TopDealsReceivedTracker(Map impToTopDealMap) { + this.impToTopDealMap = new HashMap<>(impToTopDealMap); + this.completionPromise = Promise.promise(); + } + + @Override + public Future future() { + return completionPromise.future(); + } + + @Override + public void processBids(List bids) { + if (completionPromise.future().isComplete()) { + return; + } + + bids.stream() + .map(BidderBid::getBid) + .filter(Objects::nonNull) + .map(this::toImpIdIfTopDeal) + .filter(Objects::nonNull) + .forEach(impToTopDealMap::remove); + + if (impToTopDealMap.isEmpty()) { + completionPromise.tryComplete(); + } + } + + private String toImpIdIfTopDeal(Bid bid) { + final String impId = bid.getImpid(); + final String dealId = bid.getDealid(); + if (StringUtils.isNoneBlank(impId, dealId)) { + final String topDealForImp = impToTopDealMap.get(impId); + if (topDealForImp != null && Objects.equals(dealId, topDealForImp)) { + return impId; + } + } + + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/DisabledAdapter.java b/src/main/java/org/prebid/server/bidder/DisabledAdapter.java deleted file mode 100644 index 387286cc8c7..00000000000 --- a/src/main/java/org/prebid/server/bidder/DisabledAdapter.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.prebid.server.bidder; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.response.BidResponse; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.bidder.model.AdapterHttpRequest; -import org.prebid.server.bidder.model.ExchangeCall; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.proto.response.Bid; - -import java.util.List; -import java.util.Objects; - -/** - * Used to indicate disabled adapter. First method call to this adapter should throw exception. - * Other methods should never be called. - */ -public class DisabledAdapter implements Adapter { - - private String errorMessage; - - public DisabledAdapter(String errorMessage) { - this.errorMessage = Objects.requireNonNull(errorMessage); - } - - @Override - public List> makeHttpRequests( - AdapterRequest adapterRequest, PreBidRequestContext preBidRequestContext) throws PreBidException { - throw new PreBidException(errorMessage); - } - - @Override - public List extractBids( - AdapterRequest adapterRequest, ExchangeCall exchangeCall) throws PreBidException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean tolerateErrors() { - throw new UnsupportedOperationException(); - } - - @Override - public TypeReference responseTypeReference() { - throw new UnsupportedOperationException(); - } -} diff --git a/src/main/java/org/prebid/server/bidder/DisabledBidder.java b/src/main/java/org/prebid/server/bidder/DisabledBidder.java index 1719bcb1669..dda95ee859e 100644 --- a/src/main/java/org/prebid/server/bidder/DisabledBidder.java +++ b/src/main/java/org/prebid/server/bidder/DisabledBidder.java @@ -8,7 +8,6 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -26,7 +25,7 @@ public DisabledBidder(String errorMessage) { @Override public Result>> makeHttpRequests(BidRequest request) { - return Result.of(Collections.emptyList(), Collections.singletonList(BidderError.badInput(errorMessage))); + return Result.withError(BidderError.badInput(errorMessage)); } @Override diff --git a/src/main/java/org/prebid/server/bidder/HttpAdapterConnector.java b/src/main/java/org/prebid/server/bidder/HttpAdapterConnector.java deleted file mode 100644 index ca97442d900..00000000000 --- a/src/main/java/org/prebid/server/bidder/HttpAdapterConnector.java +++ /dev/null @@ -1,321 +0,0 @@ -package org.prebid.server.bidder; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.response.BidResponse; -import io.netty.channel.ConnectTimeoutException; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.CompositeFuture; -import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.auction.model.AdUnitBid; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.AdapterResponse; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.bidder.model.AdapterHttpRequest; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.ExchangeCall; -import org.prebid.server.bidder.model.Result; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.privacy.PrivacyExtractor; -import org.prebid.server.privacy.model.Privacy; -import org.prebid.server.proto.request.PreBidRequest; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.BidderDebug; -import org.prebid.server.proto.response.BidderStatus; -import org.prebid.server.proto.response.MediaType; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; - -import java.time.Clock; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.ListIterator; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; - -/** - * Implements HTTP communication functionality common for {@link Adapter}'s. - *

- * This class exists to help segregate core auction logic and minimize code duplication across the {@link Adapter} - * implementations. - */ -public class HttpAdapterConnector { - - private static final Logger logger = LoggerFactory.getLogger(HttpAdapterConnector.class); - - private final HttpClient httpClient; - private final PrivacyExtractor privacyExtractor; - private final Clock clock; - private final JacksonMapper mapper; - - public HttpAdapterConnector(HttpClient httpClient, - PrivacyExtractor privacyExtractor, - Clock clock, - JacksonMapper mapper) { - - this.httpClient = Objects.requireNonNull(httpClient); - this.privacyExtractor = Objects.requireNonNull(privacyExtractor); - this.clock = Objects.requireNonNull(clock); - this.mapper = Objects.requireNonNull(mapper); - } - - /** - * Executes HTTP requests for particular {@link Adapter} and returns {@link AdapterResponse} - */ - public Future call(Adapter adapter, Usersyncer usersyncer, - AdapterRequest adapterRequest, - PreBidRequestContext preBidRequestContext) { - final long bidderStarted = clock.millis(); - - final List> httpRequests; - try { - httpRequests = adapter.makeHttpRequests(adapterRequest, preBidRequestContext); - } catch (PreBidException e) { - final String message = e.getMessage(); - return Future.succeededFuture(AdapterResponse.of(BidderStatus.builder() - .bidder(adapterRequest.getBidderCode()) - .error(message) - .responseTimeMs(responseTime(bidderStarted)) - .build(), Collections.emptyList(), BidderError.badInput(message))); - } - - return CompositeFuture.join(httpRequests.stream() - .map(httpRequest -> doRequest(httpRequest, preBidRequestContext.getTimeout(), - adapter.responseTypeReference())) - .collect(Collectors.toList())) - .map(compositeFuture -> toBidderResult(adapter, usersyncer, adapterRequest, preBidRequestContext, - bidderStarted, compositeFuture.list())); - } - - /** - * Makes an HTTP request and returns {@link Future} that will be eventually completed with success or error result. - */ - private Future> doRequest(AdapterHttpRequest httpRequest, Timeout timeout, - TypeReference typeReference) { - final String uri = httpRequest.getUri(); - final T requestBody = httpRequest.getPayload(); - final String body = requestBody != null ? mapper.encode(requestBody) : null; - final BidderDebug.BidderDebugBuilder bidderDebugBuilder = beginBidderDebug(uri, body); - - final long remainingTimeout = timeout.remaining(); - if (remainingTimeout <= 0) { - return failResponse(new TimeoutException("Timeout has been exceeded"), bidderDebugBuilder); - } - - return httpClient.request(httpRequest.getMethod(), uri, httpRequest.getHeaders(), body, remainingTimeout) - .compose(response -> processResponse(response, typeReference, requestBody, bidderDebugBuilder)) - .recover(exception -> failResponse(exception, bidderDebugBuilder)); - } - - private static BidderDebug.BidderDebugBuilder beginBidderDebug(String url, String bidRequestBody) { - return BidderDebug.builder() - .requestUri(url) - .requestBody(bidRequestBody); - } - - private static BidderDebug completeBidderDebug(BidderDebug.BidderDebugBuilder bidderDebugBuilder, int statusCode, - String body) { - return bidderDebugBuilder - .responseBody(body) - .statusCode(statusCode) - .build(); - } - - /** - * Handles request (e.g. read timeout) and response (e.g. connection reset) errors producing - * {@link ExchangeCall} containing {@link BidderDebug} and error description. - */ - private static Future> failResponse(Throwable exception, - BidderDebug.BidderDebugBuilder bidderDebugBuilder) { - logger.warn("Error occurred while sending bid request to an exchange: {0}", exception.getMessage()); - logger.debug("Error occurred while sending bid request to an exchange", exception); - - final BidderError error = exception instanceof TimeoutException || exception instanceof ConnectTimeoutException - ? BidderError.timeout("Timed out") - : BidderError.generic(exception.getMessage()); - - return Future.succeededFuture(ExchangeCall.error(bidderDebugBuilder.build(), error)); - } - - /** - * Handles {@link HttpClientResponse}, analyzes response status - * and creates {@link Future} with {@link ExchangeCall} from body content. - */ - private Future> processResponse( - HttpClientResponse response, TypeReference responseTypeReference, T request, - BidderDebug.BidderDebugBuilder bidderDebugBuilder) { - - return Future.succeededFuture(toExchangeCall( - request, response.getStatusCode(), response.getBody(), responseTypeReference, bidderDebugBuilder)); - } - - /** - * Transforms HTTP call results into {@link ExchangeCall} filled with debug information, - * {@link BidResponse} and errors happened along the way. - */ - private ExchangeCall toExchangeCall(T request, int statusCode, String body, - TypeReference responseTypeReference, - BidderDebug.BidderDebugBuilder bidderDebugBuilder) { - final BidderDebug bidderDebug = completeBidderDebug(bidderDebugBuilder, statusCode, body); - - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return ExchangeCall.empty(bidderDebug); - } - - if (statusCode != HttpResponseStatus.OK.code()) { - return ExchangeCall.error(bidderDebug, - BidderError.create(String.format("HTTP status %d; body: %s", statusCode, body), - statusCode == HttpResponseStatus.BAD_REQUEST.code() - ? BidderError.Type.bad_input - : BidderError.Type.bad_server_response)); - } - - final R bidResponse; - try { - bidResponse = mapper.decodeValue(body, responseTypeReference); - } catch (DecodeException e) { - return ExchangeCall.error(bidderDebug, BidderError.badServerResponse(e.getMessage())); - } - - return ExchangeCall.success(request, bidResponse, bidderDebug); - } - - /** - * Transforms {@link ExchangeCall} into single {@link AdapterResponse} filled with debug information, - * list of {@link Bid}, {@link BidderStatus}, etc. - */ - private AdapterResponse toBidderResult( - Adapter adapter, Usersyncer usersyncer, AdapterRequest adapterRequest, - PreBidRequestContext preBidRequestContext, long bidderStarted, List> exchangeCalls) { - - final Integer responseTime = responseTime(bidderStarted); - - final BidderStatus.BidderStatusBuilder bidderStatusBuilder = BidderStatus.builder() - .bidder(adapterRequest.getBidderCode()) - .responseTimeMs(responseTime); - - if (preBidRequestContext.isDebug()) { - bidderStatusBuilder - .debug(exchangeCalls.stream().map(ExchangeCall::getBidderDebug).collect(Collectors.toList())); - } - - final PreBidRequest preBidRequest = preBidRequestContext.getPreBidRequest(); - if (preBidRequest.getApp() == null - && preBidRequestContext.getUidsCookie().uidFrom(usersyncer.getCookieFamilyName()) == null) { - - final Privacy privacy = privacyExtractor.validPrivacyFrom(preBidRequest); - bidderStatusBuilder - .noCookie(true) - .usersync(UsersyncInfoAssembler.from(usersyncer).withPrivacy(privacy).assemble()); - } - - final List>> bidsWithErrors = exchangeCalls.stream() - .map(exchangeCall -> bidsWithError(adapter, adapterRequest, exchangeCall, responseTime)) - .collect(Collectors.toList()); - - final Result> failedBidsWithError = failedBidsWithError(bidsWithErrors); - final List bids = bidsWithErrors.stream() - .flatMap(bidsWithError -> bidsWithError.getValue().stream()) - .collect(Collectors.toList()); - - BidderError errorToReturn = null; - List bidsToReturn = Collections.emptyList(); - - if (failedBidsWithError != null - && (!adapter.tolerateErrors() || (adapter.tolerateErrors() && bids.isEmpty()))) { - errorToReturn = failedBidsWithError.getErrors().get(0); - bidderStatusBuilder.error(errorToReturn.getMessage()); - } else if (bids.isEmpty()) { - bidderStatusBuilder.noBid(true); - } else { - bidsToReturn = dropBidsWithNotValidSize(bids, adapterRequest.getAdUnitBids()); - bidderStatusBuilder.numBids(bidsToReturn.size()); - } - - return AdapterResponse.of(bidderStatusBuilder.build(), bidsToReturn, errorToReturn); - } - - private int responseTime(long bidderStarted) { - return Math.toIntExact(clock.millis() - bidderStarted); - } - - /** - * Transforms {@link ExchangeCall} into {@link Result}<{@link List}<{@link Bid}>> object with list of - * bids and error. - */ - private static Result> bidsWithError(Adapter adapter, AdapterRequest adapterRequest, - ExchangeCall exchangeCall, Integer responseTime) { - final BidderError error = exchangeCall.getError(); - if (error != null) { - return Result.emptyWithError(error); - } - try { - final List bids = adapter.extractBids(adapterRequest, exchangeCall).stream() - .map(bidBuilder -> bidBuilder.responseTimeMs(responseTime)) - .map(Bid.BidBuilder::build) - .collect(Collectors.toList()); - return Result.of(bids, Collections.emptyList()); - } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); - } - } - - /** - * Searches for last error in list of {@link Result}<{@link List}<{@link Bid}>> - */ - private static >> U failedBidsWithError(List bidsWithErrors) { - final ListIterator iterator = bidsWithErrors.listIterator(bidsWithErrors.size()); - while (iterator.hasPrevious()) { - final U current = iterator.previous(); - if (CollectionUtils.isNotEmpty(current.getErrors())) { - return current; - } - } - return null; - } - - /** - * Removes bids with zero width or height if it is not possible to find these values in correspond AdUnitBid - */ - private static List dropBidsWithNotValidSize(List bids, List adUnitBids) { - final List notValidBids = bids.stream() - .filter(bid -> bid.getMediaType() == MediaType.banner - && (bid.getHeight() == null || bid.getHeight() == 0 - || bid.getWidth() == null || bid.getWidth() == 0)) - .collect(Collectors.toList()); - - if (notValidBids.isEmpty()) { - return bids; - } - - // bids which are not in invalid list are valid - final List validBids = new ArrayList<>(bids); - validBids.removeAll(notValidBids); - - for (final Bid bid : notValidBids) { - final Optional matchingAdUnit = adUnitBids.stream() - .filter(adUnitBid -> adUnitBid.getAdUnitCode().equals(bid.getCode()) - && adUnitBid.getBidId().equals(bid.getBidId()) && adUnitBid.getSizes().size() == 1) - .findAny(); - if (matchingAdUnit.isPresent()) { - final Format format = matchingAdUnit.get().getSizes().get(0); - // IMPORTANT: see javadoc in Bid class - bid.setWidth(format.getW()).setHeight(format.getH()); - validBids.add(bid); - } else { - logger.warn("Bid was rejected for bidder {0} because no size was defined", bid.getBidder()); - } - } - - return validBids; - } -} diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequestEnricher.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequestEnricher.java new file mode 100644 index 00000000000..f58d0ae4aff --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequestEnricher.java @@ -0,0 +1,92 @@ +package org.prebid.server.bidder; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.MultiMap; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; +import org.prebid.server.util.HttpUtil; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class HttpBidderRequestEnricher { + + private static final Set HEADERS_TO_COPY = Collections.singleton(HttpUtil.SEC_GPC_HEADER.toString()); + + private final String pbsRecord; + + public HttpBidderRequestEnricher(String pbsVersion) { + pbsRecord = createNameVersionRecord("pbs-java", Objects.requireNonNull(pbsVersion)); + } + + MultiMap enrichHeaders( + MultiMap bidderRequestHeaders, + CaseInsensitiveMultiMap originalRequestHeaders, + BidRequest bidRequest) { + + // some bidders has headers on class level, so we create copy to not affect them + final MultiMap bidderRequestHeadersCopy = copyMultiMap(bidderRequestHeaders); + + addOriginalRequestHeaders(originalRequestHeaders, bidderRequestHeadersCopy); + addXPrebidHeader(bidderRequestHeadersCopy, bidRequest); + + return bidderRequestHeadersCopy; + } + + private static MultiMap copyMultiMap(MultiMap source) { + final MultiMap copiedMultiMap = MultiMap.caseInsensitiveMultiMap(); + if (source != null && !source.isEmpty()) { + source.forEach(entry -> copiedMultiMap.add(entry.getKey(), entry.getValue())); + } + return copiedMultiMap; + } + + private static void addOriginalRequestHeaders(CaseInsensitiveMultiMap originalHeaders, MultiMap bidderHeaders) { + originalHeaders.entries().stream() + .filter(entry -> HEADERS_TO_COPY.contains(entry.getKey()) + && !bidderHeaders.contains(entry.getKey())) + .forEach(entry -> bidderHeaders.add(entry.getKey(), entry.getValue())); + } + + private void addXPrebidHeader(MultiMap headers, BidRequest bidRequest) { + final String channelRecord = resolveChannelVersionRecord(bidRequest.getExt()); + final String sdkRecord = resolveSdkVersionRecord(bidRequest.getApp()); + final String value = Stream.of(channelRecord, sdkRecord, pbsRecord) + .filter(Objects::nonNull) + .collect(Collectors.joining(",")); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_PREBID_HEADER, value); + } + + private static String resolveChannelVersionRecord(ExtRequest extRequest) { + final ExtRequestPrebid extPrebid = extRequest != null ? extRequest.getPrebid() : null; + final ExtRequestPrebidChannel channel = extPrebid != null ? extPrebid.getChannel() : null; + final String channelName = channel != null ? channel.getName() : null; + final String channelVersion = channel != null ? channel.getVersion() : null; + + return createNameVersionRecord(channelName, channelVersion); + } + + private static String resolveSdkVersionRecord(App app) { + final ExtApp extApp = app != null ? app.getExt() : null; + final ExtAppPrebid extPrebid = extApp != null ? extApp.getPrebid() : null; + final String sdkSource = extPrebid != null ? extPrebid.getSource() : null; + final String sdkVersion = extPrebid != null ? extPrebid.getVersion() : null; + + return createNameVersionRecord(sdkSource, sdkVersion); + } + + private static String createNameVersionRecord(String name, String version) { + return StringUtils.isNotEmpty(name) && StringUtils.isNotEmpty(version) + ? String.format("%s/%s", name, version) + : null; + } +} diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java index 095f9a23a82..11011e40066 100644 --- a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java @@ -8,7 +8,9 @@ import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; @@ -17,7 +19,9 @@ import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.execution.Timeout; +import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall; +import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; @@ -44,40 +48,56 @@ public class HttpBidderRequester { private static final Logger logger = LoggerFactory.getLogger(HttpBidderRequester.class); - private static final int NOTIFICATION_TIMEOUT_MS = 200; - private final HttpClient httpClient; private final BidderRequestCompletionTrackerFactory completionTrackerFactory; + private final BidderErrorNotifier bidderErrorNotifier; + private final HttpBidderRequestEnricher requestEnricher; public HttpBidderRequester(HttpClient httpClient, - BidderRequestCompletionTrackerFactory completionTrackerFactory) { + BidderRequestCompletionTrackerFactory completionTrackerFactory, + BidderErrorNotifier bidderErrorNotifier, + HttpBidderRequestEnricher requestEnricher) { this.httpClient = Objects.requireNonNull(httpClient); this.completionTrackerFactory = completionTrackerFactoryOrFallback(completionTrackerFactory); + this.bidderErrorNotifier = Objects.requireNonNull(bidderErrorNotifier); + this.requestEnricher = Objects.requireNonNull(requestEnricher); } /** * Executes given request to a given bidder. */ - public Future requestBids( - Bidder bidder, BidRequest bidRequest, Timeout timeout, boolean debugEnabled) { + public Future requestBids(Bidder bidder, + BidderRequest bidderRequest, + Timeout timeout, + CaseInsensitiveMultiMap requestHeaders, + boolean debugEnabled) { - final Result>> httpRequestsWithErrors = bidder.makeHttpRequests(bidRequest); + final BidRequest bidRequest = bidderRequest.getBidRequest(); + final Result>> httpRequestsWithErrors = bidder.makeHttpRequests(bidRequest); final List bidderErrors = httpRequestsWithErrors.getErrors(); - final List> httpRequests = httpRequestsWithErrors.getValue(); + final List> httpRequests = + enrichRequests(httpRequestsWithErrors.getValue(), requestHeaders, bidRequest); if (CollectionUtils.isEmpty(httpRequests)) { return emptyBidderSeatBidWithErrors(bidderErrors); } - final BidderRequestCompletionTracker completionTracker = completionTrackerFactory.create(bidRequest); + final String storedResponse = bidderRequest.getStoredResponse(); + final String bidderName = bidderRequest.getBidder(); + + // stored response available only for single request interaction for the moment. + final Stream>> httpCalls = isStoredResponse(httpRequests, storedResponse, bidderName) + ? Stream.of(makeStoredHttpCall(httpRequests.get(0), storedResponse)) + : httpRequests.stream().map(httpRequest -> doRequest(httpRequest, timeout)); + final BidderRequestCompletionTracker completionTracker = completionTrackerFactory.create(bidRequest); final ResultBuilder resultBuilder = new ResultBuilder<>(httpRequests, bidderErrors, completionTracker); - final List> httpRequestFutures = httpRequests.stream() - .map(httpRequest -> doRequest(httpRequest, timeout, bidder)) + final List> httpRequestFutures = httpCalls .map(httpCallFuture -> httpCallFuture + .map(httpCall -> bidderErrorNotifier.processTimeout(httpCall, bidder)) .map(httpCall -> processHttpCall(bidder, bidRequest, resultBuilder, httpCall))) .collect(Collectors.toList()); @@ -89,6 +109,39 @@ public Future requestBids( .map(ignored -> resultBuilder.toBidderSeatBid(debugEnabled)); } + private List> enrichRequests(List> httpRequests, + CaseInsensitiveMultiMap requestHeaders, + BidRequest bidRequest) { + + return httpRequests.stream().map(httpRequest -> httpRequest.toBuilder() + .headers(requestEnricher.enrichHeaders( + httpRequest.getHeaders(), requestHeaders, bidRequest)) + .build()) + .collect(Collectors.toList()); + } + + private boolean isStoredResponse(List> httpRequests, String storedResponse, String bidder) { + if (StringUtils.isBlank(storedResponse)) { + return false; + } + + if (httpRequests.size() > 1) { + logger.warn("More than one request was created for stored response, when only single stored response " + + "per bidder is supported for the moment. Request to real {0} bidder " + + "will be performed .", bidder); + return false; + } + + return true; + } + + private Future> makeStoredHttpCall(HttpRequest httpRequest, String storedResponse) { + return Future.succeededFuture( + HttpCall.success(httpRequest, + HttpResponse.of(HttpResponseStatus.OK.code(), null, storedResponse), + null)); + } + /** * Creates {@link Future} with empty list of {@link BidderBid}s * and list of {@link ExtHttpCall}s with list of {@link BidderError}s. @@ -105,7 +158,7 @@ private Future emptyBidderSeatBidWithErrors(List bid /** * Makes an HTTP request and returns {@link Future} that will be eventually completed with success or error result. */ - private Future> doRequest(HttpRequest httpRequest, Timeout timeout, Bidder bidder) { + private Future> doRequest(HttpRequest httpRequest, Timeout timeout) { final long remainingTimeout = timeout.remaining(); if (remainingTimeout <= 0) { return failResponse(new TimeoutException("Timeout has been exceeded"), httpRequest); @@ -114,8 +167,7 @@ private Future> doRequest(HttpRequest httpRequest, Timeout ti return httpClient.request(httpRequest.getMethod(), httpRequest.getUri(), httpRequest.getHeaders(), httpRequest.getBody(), remainingTimeout) .compose(response -> processResponse(response, httpRequest)) - .recover(exception -> failResponse(exception, httpRequest)) - .map(httpCall -> notifyTimeoutBidder(bidder, httpCall)); + .recover(exception -> failResponse(exception, httpRequest)); } /** @@ -135,30 +187,6 @@ private static Future> failResponse(Throwable exception, HttpReq HttpCall.failure(httpRequest, BidderError.create(exception.getMessage(), errorType))); } - /** - * Calls when bidder's exchange responds with timeout and sends notification if bidder supports it. - */ - private HttpCall notifyTimeoutBidder(Bidder bidder, HttpCall httpCall) { - final BidderError bidderError = httpCall.getError(); - final BidderError.Type errorType = bidderError != null ? bidderError.getType() : null; - - if (errorType == BidderError.Type.timeout && bidder instanceof TimeoutBidder) { - final TimeoutBidder timeoutBidder = (TimeoutBidder) bidder; - final HttpRequest timeoutNotification = timeoutBidder.makeTimeoutNotification(httpCall.getRequest()); - - if (timeoutNotification != null) { - httpClient.request( - timeoutNotification.getMethod(), - timeoutNotification.getUri(), - timeoutNotification.getHeaders(), - timeoutNotification.getBody(), - NOTIFICATION_TIMEOUT_MS); - } - } - - return httpCall; - } - /** * Produces {@link Future} with {@link HttpCall} containing request, response and possible error description * (if status code indicates an error). @@ -193,18 +221,24 @@ private Void processHttpCall(Bidder bidder, } private static Result> makeBids(Bidder bidder, HttpCall httpCall, BidRequest bidRequest) { - if (httpCall.getError() != null || !isOkOrNoContent(httpCall)) { - return null; - } - return bidder.makeBids(toHttpCallWithSafeResponseBody(httpCall), bidRequest); + return httpCall.getError() != null + ? null + : makeResult(bidder, httpCall, bidRequest); } /** - * Returns true if response HTTP status code is equal to 200 or 204, otherwise false. + * Returns result based on response status code */ - private static boolean isOkOrNoContent(HttpCall httpCall) { + private static Result> makeResult(Bidder bidder, HttpCall httpCall, + BidRequest bidRequest) { final int statusCode = httpCall.getResponse().getStatusCode(); - return statusCode == HttpResponseStatus.OK.code() || statusCode == HttpResponseStatus.NO_CONTENT.code(); + if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { + return Result.empty(); + } + if (statusCode != HttpResponseStatus.OK.code()) { + return null; + } + return bidder.makeBids(toHttpCallWithSafeResponseBody(httpCall), bidRequest); } /** @@ -280,7 +314,8 @@ private static ExtHttpCall toExt(HttpCall httpCall) { final HttpRequest request = httpCall.getRequest(); final ExtHttpCall.ExtHttpCallBuilder builder = ExtHttpCall.builder() .uri(request.getUri()) - .requestbody(request.getBody()); + .requestbody(request.getBody()) + .requestheaders(HttpUtil.toDebugHeaders(request.getHeaders())); final HttpResponse response = httpCall.getResponse(); if (response != null) { diff --git a/src/main/java/org/prebid/server/bidder/OpenrtbAdapter.java b/src/main/java/org/prebid/server/bidder/OpenrtbAdapter.java deleted file mode 100644 index 35b77d13c70..00000000000 --- a/src/main/java/org/prebid/server/bidder/OpenrtbAdapter.java +++ /dev/null @@ -1,180 +0,0 @@ -package org.prebid.server.bidder; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.iab.openrtb.request.Banner; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.Source; -import com.iab.openrtb.request.User; -import com.iab.openrtb.request.Video; -import com.iab.openrtb.response.BidResponse; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.vertx.core.MultiMap; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.auction.model.AdUnitBid; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.request.PreBidRequest; -import org.prebid.server.proto.response.MediaType; -import org.prebid.server.util.HttpUtil; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Stream; - -/** - * Basic {@link Adapter} implementation containing common logic functionality and helper methods. - */ -public abstract class OpenrtbAdapter implements Adapter { - - protected final String cookieFamilyName; - - protected OpenrtbAdapter(String cookieFamilyName) { - this.cookieFamilyName = Objects.requireNonNull(cookieFamilyName); - } - - @Override - public boolean tolerateErrors() { - return false; // by default all adapters throw up errors - } - - @Override - public TypeReference responseTypeReference() { - return new TypeReference() { - }; - } - - protected static Banner.BannerBuilder bannerBuilder(AdUnitBid adUnitBid) { - final List sizes = adUnitBid.getSizes(); - return Banner.builder() - .w(sizes.get(0).getW()) - .h(sizes.get(0).getH()) - .format(sizes) - .topframe(adUnitBid.getTopframe()); - } - - protected static Video.VideoBuilder videoBuilder(AdUnitBid adUnitBid) { - final org.prebid.server.proto.request.Video video = adUnitBid.getVideo(); - final Format format = adUnitBid.getSizes().get(0); - return Video.builder() - .mimes(video.getMimes()) - .minduration(video.getMinduration()) - .maxduration(video.getMaxduration()) - .w(format.getW()) - .h(format.getH()) - .startdelay(video.getStartdelay()) - .playbackmethod(Collections.singletonList(video.getPlaybackMethod())) - .protocols(video.getProtocols()); - } - - protected static Site.SiteBuilder siteBuilder(PreBidRequestContext preBidRequestContext) { - return preBidRequestContext.getPreBidRequest().getApp() != null ? null : Site.builder() - .domain(preBidRequestContext.getDomain()) - .page(preBidRequestContext.getReferer()); - } - - protected static Site makeSite(PreBidRequestContext preBidRequestContext) { - final Site.SiteBuilder siteBuilder = siteBuilder(preBidRequestContext); - return siteBuilder == null ? null : siteBuilder.build(); - } - - protected static Device.DeviceBuilder deviceBuilder(PreBidRequestContext preBidRequestContext) { - final PreBidRequest preBidRequest = preBidRequestContext.getPreBidRequest(); - final Device device = preBidRequest.getDevice(); - - // create a copy since device might be shared with other adapters - final Device.DeviceBuilder deviceBuilder = device != null ? device.toBuilder() : Device.builder(); - - if (preBidRequest.getApp() == null) { - deviceBuilder.ua(preBidRequestContext.getUa()); - } - - return deviceBuilder.ip(preBidRequestContext.getIp()); - } - - protected User.UserBuilder userBuilder(PreBidRequestContext preBidRequestContext) { - final UidsCookie uidsCookie = preBidRequestContext.getUidsCookie(); - final User user = preBidRequestContext.getPreBidRequest().getUser(); - final ExtUser userExt = user != null ? user.getExt() : null; - return preBidRequestContext.getPreBidRequest().getApp() != null ? null : User.builder() - .buyeruid(uidsCookie.uidFrom(cookieFamilyName)) - // id is a UID for "adnxs" (see logic in open-source implementation) - .id(uidsCookie.uidFrom("adnxs")) - .ext(userExt); - } - - protected User makeUser(PreBidRequestContext preBidRequestContext) { - final User.UserBuilder userBuilder = userBuilder(preBidRequestContext); - return userBuilder == null ? preBidRequestContext.getPreBidRequest().getUser() : userBuilder.build(); - } - - protected static Source makeSource(PreBidRequestContext preBidRequestContext) { - final PreBidRequest preBidRequest = preBidRequestContext.getPreBidRequest(); - return Source.builder() - .tid(preBidRequest.getTid()) - .fd(preBidRequest.getApp() == null ? 1 : null) // upstream, aka header - .build(); - } - - protected static void validateAdUnitBidsMediaTypes(List adUnitBids, Set allowedMediaTypes) { - if (!adUnitBids.stream() - .allMatch(adUnitBid -> adUnitBid.getMediaTypes().stream() - .filter(allowedMediaTypes::contains) - .allMatch(mediaType -> isValidAdUnitBidVideoMediaType(mediaType, adUnitBid)))) { - throw new PreBidException("Invalid AdUnit: VIDEO media type with no video data"); - } - } - - private static boolean isValidAdUnitBidVideoMediaType(MediaType mediaType, AdUnitBid adUnitBid) { - final org.prebid.server.proto.request.Video video = adUnitBid.getVideo(); - return !(MediaType.video.equals(mediaType) - && (video == null || CollectionUtils.isEmpty(video.getMimes()))); - } - - protected static Set allowedMediaTypes(AdUnitBid adUnitBid, Set adapterAllowedMediaTypes) { - final Set allowedMediaTypes = new HashSet<>(adapterAllowedMediaTypes); - allowedMediaTypes.retainAll(adUnitBid.getMediaTypes()); - return allowedMediaTypes; - } - - protected MultiMap headers() { - return MultiMap.caseInsensitiveMultiMap() - .add(HttpUtil.CONTENT_TYPE_HEADER, HttpUtil.APPLICATION_JSON_CONTENT_TYPE) - .add(HttpUtil.ACCEPT_HEADER, HttpHeaderValues.APPLICATION_JSON); - } - - protected static void validateImps(List imps) { - if (CollectionUtils.isEmpty(imps)) { - throw new PreBidException("openRTB bids need at least one Imp"); - } - } - - protected static AdUnitBid lookupBid(List adUnitBids, String adUnitCode) { - for (AdUnitBid adUnitBid : adUnitBids) { - if (Objects.equals(adUnitBid.getAdUnitCode(), adUnitCode)) { - return adUnitBid; - } - } - throw new PreBidException(String.format("Unknown ad unit code '%s'", adUnitCode)); - } - - /** - * Extracts bids from response, returns empty stream in case of missing bid response or seat bids - */ - protected static Stream responseBidStream(BidResponse bidResponse) { - return bidResponse == null || bidResponse.getSeatbid() == null ? Stream.empty() - : bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .filter(seatBid -> seatBid.getBid() != null) - .flatMap(seatBid -> seatBid.getBid().stream()) - .filter(Objects::nonNull); - } -} diff --git a/src/main/java/org/prebid/server/bidder/OpenrtbBidder.java b/src/main/java/org/prebid/server/bidder/OpenrtbBidder.java index f0795ff9768..72998feb6f7 100644 --- a/src/main/java/org/prebid/server/bidder/OpenrtbBidder.java +++ b/src/main/java/org/prebid/server/bidder/OpenrtbBidder.java @@ -1,10 +1,11 @@ package org.prebid.server.bidder; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.model.BidderBid; @@ -23,14 +24,11 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; public abstract class OpenrtbBidder implements Bidder { - private static final String DEFAULT_BID_CURRENCY = "USD"; - private final String endpointUrl; private final RequestCreationStrategy requestCreationStrategy; private final Class extType; @@ -52,7 +50,7 @@ public final Result>> makeHttpRequests(BidRequest b try { validateRequest(bidRequest); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badInput(e.getMessage())); + return Result.withError(BidderError.badInput(e.getMessage())); } final List errors = new ArrayList<>(); @@ -67,7 +65,7 @@ public final Result>> makeHttpRequests(BidRequest b } } if (modifiedImpsWithExts.isEmpty()) { - return Result.of(Collections.emptyList(), errors); + return Result.withErrors(errors); } return Result.of(createHttpRequests(bidRequest, modifiedImpsWithExts), errors); @@ -169,11 +167,18 @@ private HttpRequest makeRequest(BidRequest bidRequest, List @@ -211,14 +216,14 @@ protected void modifyRequest(BidRequest bidRequest, BidRequest.BidRequestBuilder public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - if (bidResponse == null || bidResponse.getSeatbid() == null) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } return bidsFromResponse(bidRequest, bidResponse); @@ -230,11 +235,18 @@ private List bidsFromResponse(BidRequest bidRequest, BidResponse bidR .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), - getBidCurrency(bidRequest.getCur(), bidResponse.getCur()))) + .map(bid -> makeBidderBid(bidRequest, bidResponse, bid)) + .filter(Objects::nonNull) .collect(Collectors.toList()); } + private BidderBid makeBidderBid(BidRequest bidRequest, BidResponse bidResponse, Bid bid) { + final BidType bidType = getBidType(bid, bidRequest.getImp()); + return bidType != null + ? BidderBid.of(bid, bidType, bidResponse.getCur()) + : null; + } + /** * A hook for resolving bidder-specific bid type. *

@@ -242,11 +254,12 @@ private List bidsFromResponse(BidRequest bidRequest, BidResponse bidR * bid type based on impression details with the following priority: * 1.Banner, 2.Video, 3.Native, 4.Audio. * - * @param impId - impression id taken from bid.getImpid() - * @param imps - list of impressions from outgoing request + * @param bid - bid type of which we are looking for + * @param imps - list of impressions from outgoing request * @return - bid type depending on impression's content */ - protected BidType getBidType(String impId, List imps) { + protected BidType getBidType(Bid bid, List imps) { + final String impId = bid.getImpid(); BidType bidType = BidType.banner; for (Imp imp : imps) { if (imp.getId().equals(impId)) { @@ -264,32 +277,6 @@ protected BidType getBidType(String impId, List imps) { return bidType; } - /** - * A hook for defining a bid currency. - *

- * By default - USD. - * - * @return - bid currency - */ - protected String getBidCurrency(List requestCurrencies, String responseCurrency) { - final String result; - - if (responseCurrency != null) { - result = responseCurrency; - } else if (CollectionUtils.isNotEmpty(requestCurrencies)) { - result = requestCurrencies.get(0); - } else { - result = DEFAULT_BID_CURRENCY; - } - - return result; - } - - @Override - public final Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } - public enum RequestCreationStrategy { SINGLE_REQUEST, REQUEST_PER_IMP diff --git a/src/main/java/org/prebid/server/bidder/README.md b/src/main/java/org/prebid/server/bidder/README.md deleted file mode 100644 index cf64cb872d3..00000000000 --- a/src/main/java/org/prebid/server/bidder/README.md +++ /dev/null @@ -1,23 +0,0 @@ -Please note that some of the bidder adapters require separate authentication parameters -and contracts in order to integrate, as server-to-server integrations pose a greater risk of -accidentally or purposefully misrepresenting the inventory and audience being sold. Please consult the -bidders with whom you would like to integrate on this matter to ensure that the appropriate agreements -are in place to proceed forward. - -The following adapters are fully supported in the Java implementation by the owning exchange: - -- rubicon - -All other adapters have been ported from Go and basic testing has been done. The project team will reach out to the following entities -for review and approval of the ported adapter: - -- adform -- appnexus -- audienceNetwork -- conversant -- ix -- lifestreet -- openx -- pubmatic -- pulsepoint -- sovrn \ No newline at end of file diff --git a/src/main/java/org/prebid/server/bidder/TimeoutBidder.java b/src/main/java/org/prebid/server/bidder/TimeoutBidder.java deleted file mode 100644 index 1e4a7701944..00000000000 --- a/src/main/java/org/prebid/server/bidder/TimeoutBidder.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.bidder; - -import org.prebid.server.bidder.model.HttpRequest; - -/** - * TimeoutBidder is used to identify bidders that support timeout notifications. - */ -public interface TimeoutBidder extends Bidder { - - /** - * makeTimeoutNotification method much the same as makeRequests, except it is fed the bidder request that timed out, - * and expects that only one notification "request" will be generated. A use case for multiple timeout notifications - * has not been anticipated. - *

- * Do note that if makeRequests returns multiple requests, and more than one of these times out, - * makeTimeoutNotification will be called once for each timed out request. - */ - HttpRequest makeTimeoutNotification(HttpRequest httpRequest); -} diff --git a/src/main/java/org/prebid/server/bidder/UsersyncInfoAssembler.java b/src/main/java/org/prebid/server/bidder/UsersyncInfoAssembler.java index 5108e3a2328..8d14a7ba773 100644 --- a/src/main/java/org/prebid/server/bidder/UsersyncInfoAssembler.java +++ b/src/main/java/org/prebid/server/bidder/UsersyncInfoAssembler.java @@ -13,12 +13,13 @@ public class UsersyncInfoAssembler { private String type; private Boolean supportCORS; - public static UsersyncInfoAssembler from(Usersyncer usersyncer) { + public static UsersyncInfoAssembler from(Usersyncer.UsersyncMethod usersyncMethod) { final UsersyncInfoAssembler usersyncInfoAssembler = new UsersyncInfoAssembler(); - usersyncInfoAssembler.usersyncUrl = usersyncer.getUsersyncUrl(); - usersyncInfoAssembler.redirectUrl = ObjectUtils.defaultIfNull(usersyncer.getRedirectUrl(), ""); - usersyncInfoAssembler.type = usersyncer.getType(); - usersyncInfoAssembler.supportCORS = usersyncer.isSupportCORS(); + usersyncInfoAssembler.usersyncUrl = usersyncMethod.getUsersyncUrl(); + usersyncInfoAssembler.redirectUrl = UsersyncUtil.enrichUsersyncUrlWithFormat( + StringUtils.stripToEmpty(usersyncMethod.getRedirectUrl()), usersyncMethod.getType()); + usersyncInfoAssembler.type = usersyncMethod.getType(); + usersyncInfoAssembler.supportCORS = usersyncMethod.isSupportCORS(); return usersyncInfoAssembler; } diff --git a/src/main/java/org/prebid/server/bidder/UsersyncMethodChooser.java b/src/main/java/org/prebid/server/bidder/UsersyncMethodChooser.java new file mode 100644 index 00000000000..cbece8b6186 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/UsersyncMethodChooser.java @@ -0,0 +1,94 @@ +package org.prebid.server.bidder; + +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.proto.request.CookieSyncRequest; +import org.prebid.server.util.StreamUtil; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +public class UsersyncMethodChooser { + + private static final String CATCH_ALL_BIDDERS = "*"; + + private final Map filters; + + public UsersyncMethodChooser(CookieSyncRequest.FilterSettings filterSettings) { + filters = initializeFilters(filterSettings); + } + + public static UsersyncMethodChooser from(CookieSyncRequest.FilterSettings filterSettings) { + return new UsersyncMethodChooser(filterSettings); + } + + public Usersyncer.UsersyncMethod choose(Usersyncer usersyncer, String bidder) { + return Stream.of(usersyncer.getPrimaryMethod(), usersyncer.getSecondaryMethod()) + .filter(method -> methodValidAndAllowed(method, bidder)) + .findFirst() + .orElse(null); + } + + private static Map initializeFilters( + CookieSyncRequest.FilterSettings filterSettings) { + + if (filterSettings == null) { + return Collections.emptyMap(); + } + + final Map filterMap = new HashMap<>(); + + filterMap.computeIfAbsent(Usersyncer.UsersyncMethod.IFRAME_TYPE, key -> filterSettings.getIframe()); + filterMap.computeIfAbsent(Usersyncer.UsersyncMethod.REDIRECT_TYPE, key -> filterSettings.getImage()); + + return filterMap; + } + + private boolean methodValidAndAllowed(Usersyncer.UsersyncMethod usersyncMethod, String bidder) { + return methodValid(usersyncMethod) && methodAllowed(usersyncMethod, bidder); + } + + private boolean methodValid(Usersyncer.UsersyncMethod usersyncMethod) { + return usersyncMethod != null && StringUtils.isNotBlank(usersyncMethod.getUsersyncUrl()); + } + + private boolean methodAllowed(Usersyncer.UsersyncMethod usersyncMethod, String bidder) { + final CookieSyncRequest.MethodFilter filter = filters.get(usersyncMethod.getType()); + + return filter == null || filter.getFilter() == null + || bidderNotExcluded(bidder, filter) || bidderIncluded(bidder, filter); + } + + private boolean bidderNotExcluded(String bidder, CookieSyncRequest.MethodFilter filter) { + return filter.getFilter() == CookieSyncRequest.FilterType.exclude && !bidderInList(bidder, filter.getBidders()); + } + + private boolean bidderIncluded(String bidder, CookieSyncRequest.MethodFilter filter) { + return filter.getFilter() == CookieSyncRequest.FilterType.include && bidderInList(bidder, filter.getBidders()); + } + + private boolean bidderInList(String bidder, JsonNode bidders) { + return listMatchesAllBidders(bidders) || listContainsBidder(bidders, bidder); + } + + private boolean listMatchesAllBidders(JsonNode bidders) { + return bidders == null + || bidders.isNull() + || (bidders.isTextual() && Objects.equals(bidders.textValue(), CATCH_ALL_BIDDERS)); + } + + private boolean listContainsBidder(JsonNode bidders, String bidder) { + return bidders.isArray() && arrayContainsString(bidders, bidder); + } + + private static boolean arrayContainsString(JsonNode bidders, String value) { + return StreamUtil.asStream(bidders.spliterator()).anyMatch(element -> elementEqualsString(element, value)); + } + + private static boolean elementEqualsString(JsonNode element, String value) { + return element != null && element.isTextual() && Objects.equals(element.textValue(), value); + } +} diff --git a/src/main/java/org/prebid/server/bidder/UsersyncUtil.java b/src/main/java/org/prebid/server/bidder/UsersyncUtil.java new file mode 100644 index 00000000000..a769471df5b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/UsersyncUtil.java @@ -0,0 +1,61 @@ +package org.prebid.server.bidder; + +import org.apache.commons.lang3.StringUtils; + +public class UsersyncUtil { + + public static final String FORMAT_PARAMETER = "f"; + public static final String BLANK_FORMAT = "b"; + public static final String IMG_FORMAT = "i"; + + private UsersyncUtil() { + } + + /** + * Places format query string param into the given url. + *

+ * Caution: it doesn't care if it already exists in url, just adds new one. + *

+ * Note: format is inserted before the last param for safety reason because of + * usersync url can be appended with UID on the exchange side without parsing query string. + */ + public static String enrichUsersyncUrlWithFormat(String url, String type) { + if (StringUtils.isAnyEmpty(url, type)) { + return url; + } + + final String formatValue = resolveFormatValueByType(type); + + return hasTwoOrMoreParameters(url) + ? insertFormatParameter(url, formatValue) + : appendFormatParameter(url, formatValue); + } + + private static String resolveFormatValueByType(String type) { + switch (type) { + case Usersyncer.UsersyncMethod.REDIRECT_TYPE: + return IMG_FORMAT; + case Usersyncer.UsersyncMethod.IFRAME_TYPE: + return BLANK_FORMAT; + default: + return StringUtils.EMPTY; // never should happen + } + } + + private static boolean hasTwoOrMoreParameters(String url) { + return url.indexOf('&') != -1; + } + + private static String insertFormatParameter(String url, String formatValue) { + final int lastParamIndex = url.lastIndexOf('&'); + + return url.substring(0, lastParamIndex) + + "&" + FORMAT_PARAMETER + "=" + formatValue + + url.substring(lastParamIndex); + } + + private static String appendFormatParameter(String url, String formatValue) { + final String separator = url.indexOf('?') != -1 ? "&" : "?"; + return url + separator + FORMAT_PARAMETER + "=" + formatValue; + } +} diff --git a/src/main/java/org/prebid/server/bidder/Usersyncer.java b/src/main/java/org/prebid/server/bidder/Usersyncer.java index 9cd81b421e5..2aaf8121f86 100644 --- a/src/main/java/org/prebid/server/bidder/Usersyncer.java +++ b/src/main/java/org/prebid/server/bidder/Usersyncer.java @@ -1,39 +1,28 @@ package org.prebid.server.bidder; -import lombok.AllArgsConstructor; -import lombok.Data; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.util.HttpUtil; +import lombok.Value; -import java.util.Objects; - -@Data -@AllArgsConstructor +@Value(staticConstructor = "of") public class Usersyncer { - private String cookieFamilyName; + String cookieFamilyName; + + UsersyncMethod primaryMethod; + + UsersyncMethod secondaryMethod; - private String usersyncUrl; + @Value(staticConstructor = "of") + public static class UsersyncMethod { - private String redirectUrl; + public static final String IFRAME_TYPE = "iframe"; + public static final String REDIRECT_TYPE = "redirect"; - private String type; + String type; - private boolean supportCORS; + String usersyncUrl; - public Usersyncer(String cookieFamilyName, - String usersyncUrl, - String redirectUrl, - String externalUri, - String type, - boolean supportCORS) { + String redirectUrl; - this.cookieFamilyName = cookieFamilyName; - this.usersyncUrl = Objects.requireNonNull(usersyncUrl); - this.redirectUrl = StringUtils.isNotBlank(redirectUrl) - ? HttpUtil.validateUrl(externalUri) + redirectUrl - : StringUtils.EMPTY; - this.type = type; - this.supportCORS = supportCORS; + boolean supportCORS; } } diff --git a/src/main/java/org/prebid/server/bidder/acuityads/AcuityadsBidder.java b/src/main/java/org/prebid/server/bidder/acuityads/AcuityadsBidder.java new file mode 100644 index 00000000000..64ee46778d3 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/acuityads/AcuityadsBidder.java @@ -0,0 +1,173 @@ +package org.prebid.server.bidder.acuityads; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.acuity.ExtImpAcuityads; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Acuityads {@link Bidder} implementation. + */ +public class AcuityadsBidder implements Bidder { + + private static final TypeReference> ACUITYADS_EXT_TYPE_REFERENCE = + new TypeReference>() { + }; + private static final String OPENRTB_VERSION = "2.5"; + private static final String URL_HOST_MACRO = "{{Host}}"; + private static final String URL_ACCOUNT_ID_MACRO = "{{AccountID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AcuityadsBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final ExtImpAcuityads extImpAcuityads; + final String url; + + try { + extImpAcuityads = parseImpExt(request.getImp().get(0)); + url = resolveEndpoint(extImpAcuityads.getHost(), extImpAcuityads.getAccountId()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + final BidRequest outgoingRequest = request.toBuilder() + .imp(removeFirstImpExt(request.getImp())) + .build(); + + return Result.of(Collections.singletonList( + HttpRequest.builder() + .method(HttpMethod.POST) + .uri(url) + .headers(resolveHeaders(request.getDevice())) + .payload(outgoingRequest) + .body(mapper.encode(outgoingRequest)) + .build()), + Collections.emptyList()); + } + + private ExtImpAcuityads parseImpExt(Imp imp) { + final ExtImpAcuityads extImpAcuityads; + try { + extImpAcuityads = mapper.mapper().convertValue(imp.getExt(), ACUITYADS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("ext.bidder not provided"); + } + if (extImpAcuityads == null) { + throw new PreBidException("ext.bidder not provided"); + } + if (StringUtils.isBlank(extImpAcuityads.getHost())) { + throw new PreBidException("Missed host param"); + } + if (StringUtils.isBlank(extImpAcuityads.getAccountId())) { + throw new PreBidException("Missed accountId param"); + } + return extImpAcuityads; + } + + private String resolveEndpoint(String host, String accountId) { + return endpointUrl + .replace(URL_HOST_MACRO, StringUtils.stripToEmpty(host)) + .replace(URL_ACCOUNT_ID_MACRO, StringUtils.stripToEmpty(accountId)); + } + + private static List removeFirstImpExt(List imps) { + return IntStream.range(0, imps.size()) + .mapToObj(impIndex -> impIndex == 0 + ? imps.get(impIndex).toBuilder().ext(null).build() + : imps.get(impIndex)) + .collect(Collectors.toList()); + } + + private static MultiMap resolveHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_OPENRTB_VERSION_HEADER, OPENRTB_VERSION); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + } + return headers; + } + + @Override + public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null) { + throw new PreBidException("Bad Server Response"); + } + if (CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("Empty SeatBid array"); + } + return bidsFromResponse(bidRequest, bidResponse); + } + + private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + final List bids = firstSeatBid.getBid(); + + if (CollectionUtils.isEmpty(bids)) { + throw new PreBidException("Empty bids array"); + } + + return bids.stream() + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getVideo() != null) { + return BidType.video; + } + if (imp.getXNative() != null) { + return BidType.xNative; + } + } + } + return BidType.banner; + } +} diff --git a/src/main/java/org/prebid/server/bidder/adf/AdfBidder.java b/src/main/java/org/prebid/server/bidder/adf/AdfBidder.java new file mode 100644 index 00000000000..3eb87cb794d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adf/AdfBidder.java @@ -0,0 +1,46 @@ +package org.prebid.server.bidder.adf; + +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.OpenrtbBidder; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.adf.ExtImpAdf; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.List; + +/** + * Adf {@link Bidder} implementation. + */ +public class AdfBidder extends OpenrtbBidder { + + public AdfBidder(String endpointUrl, JacksonMapper mapper) { + super(endpointUrl, RequestCreationStrategy.SINGLE_REQUEST, ExtImpAdf.class, mapper); + } + + @Override + protected Imp modifyImp(Imp imp, ExtImpAdf impExt) { + return imp.toBuilder() + .tagid(impExt.getMid()) + .build(); + } + + @Override + protected BidType getBidType(Bid bid, List imps) { + final String impId = bid.getImpid(); + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } + } + } + throw new PreBidException(String.format("Failed to find impression %s", impId)); + } +} diff --git a/src/main/java/org/prebid/server/bidder/adform/AdformAdapter.java b/src/main/java/org/prebid/server/bidder/adform/AdformAdapter.java deleted file mode 100644 index 18b6ca38d8d..00000000000 --- a/src/main/java/org/prebid/server/bidder/adform/AdformAdapter.java +++ /dev/null @@ -1,238 +0,0 @@ -package org.prebid.server.bidder.adform; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.User; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AdUnitBid; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.bidder.Adapter; -import org.prebid.server.bidder.adform.model.AdformBid; -import org.prebid.server.bidder.adform.model.AdformDigitrust; -import org.prebid.server.bidder.adform.model.AdformParams; -import org.prebid.server.bidder.adform.model.UrlParameters; -import org.prebid.server.bidder.model.AdapterHttpRequest; -import org.prebid.server.bidder.model.ExchangeCall; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.MediaType; -import org.prebid.server.util.HttpUtil; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -/** - * Adform {@link Adapter} implementation. - */ -public class AdformAdapter implements Adapter> { - - private static final String VERSION = "0.1.3"; - private static final String DEFAULT_CURRENCY = "USD"; - - private final String cookieFamilyName; - private final String endpointUrl; - private final JacksonMapper mapper; - - private final AdformRequestUtil requestUtil; - private final AdformHttpUtil httpUtil; - - public AdformAdapter(String cookieFamilyName, String endpointUrl, JacksonMapper mapper) { - this.cookieFamilyName = Objects.requireNonNull(cookieFamilyName); - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - - this.requestUtil = new AdformRequestUtil(); - this.httpUtil = new AdformHttpUtil(mapper); - } - - /** - * Creates {@link AdapterHttpRequest} with GET http method and all parameters in url and headers, with empty body. - */ - @Override - public List> makeHttpRequests(AdapterRequest adapterRequest, - PreBidRequestContext preBidRequestContext) { - final List adformParams = adapterRequest.getAdUnitBids().stream() - .map(this::toAdformParams) - .collect(Collectors.toList()); - - final User user = preBidRequestContext.getPreBidRequest().getUser(); - final ExtUser extUser = user != null ? user.getExt() : null; - - return Collections.singletonList(AdapterHttpRequest.of( - HttpMethod.GET, - getUrl(preBidRequestContext, adformParams, extUser), - null, - headers(preBidRequestContext, requestUtil.getAdformDigitrust(extUser)))); - } - - @Override - public List extractBids(AdapterRequest adapterRequest, - ExchangeCall> exchangeCall) throws PreBidException { - return toBidBuilder(exchangeCall.getResponse(), adapterRequest); - } - - @Override - public boolean tolerateErrors() { - return false; - } - - @Override - public TypeReference> responseTypeReference() { - return new TypeReference>() { - }; - } - - /** - * Converts {@link AdUnitBid} to masterTagId. In case of any problem to retrieve or validate masterTagId, throws - * {@link PreBidException} - */ - private AdformParams toAdformParams(AdUnitBid adUnitBid) { - final ObjectNode params = adUnitBid.getParams(); - if (params == null) { - throw new PreBidException("Adform params section is missing"); - } - final AdformParams adformParams; - try { - adformParams = mapper.mapper().treeToValue(params, AdformParams.class); - } catch (JsonProcessingException e) { - throw new PreBidException(e.getMessage(), e.getCause()); - } - final Long masterTagId = adformParams.getMid(); - if (masterTagId != null && masterTagId > 0) { - return adformParams; - } else { - throw new PreBidException(String.format("master tag(placement) id is invalid=%s", masterTagId)); - } - } - - /** - * Creates adform url with parameters - */ - private String getUrl(PreBidRequestContext preBidRequestContext, List adformParams, ExtUser extUser) { - final Integer secure = preBidRequestContext.getSecure(); - final Device device = preBidRequestContext.getPreBidRequest().getDevice(); - return httpUtil.buildAdformUrl( - UrlParameters.builder() - .masterTagIds(getMasterIds(adformParams)) - .keyValues(getKeyValues(adformParams)) - .keyWords(getKeyWords(adformParams)) - .priceTypes(getPriceTypes(adformParams)) - .cdims(getCdims(adformParams)) - .minPrices(getMinPrices(adformParams)) - .endpointUrl(endpointUrl) - .tid(ObjectUtils.defaultIfNull(preBidRequestContext.getPreBidRequest().getTid(), "")) - .ip(ObjectUtils.defaultIfNull(preBidRequestContext.getIp(), "")) - .advertisingId(device != null ? device.getIfa() : "") - .secure(secure != null && secure == 1) - .gdprApplies(requestUtil.getGdprApplies(preBidRequestContext.getPreBidRequest().getRegs())) - .consent(requestUtil.getConsent(extUser)) - .currency(DEFAULT_CURRENCY) - .url(getUrlFromParams(adformParams)) - .build()); - } - - /** - * Converts {@link AdformParams} {@link List} to master ids {@link List} - */ - private List getMasterIds(List adformParams) { - return adformParams.stream().map(AdformParams::getMid).collect(Collectors.toList()); - } - - /** - * Converts {@link AdformParams} {@link List} to key values {@link List} - */ - private List getKeyValues(List adformParams) { - return adformParams.stream().map(AdformParams::getKeyValues).collect(Collectors.toList()); - } - - /** - * Converts {@link AdformParams} {@link List} to key words {@link List} - */ - private List getKeyWords(List adformParams) { - return adformParams.stream().map(AdformParams::getKeyWords).collect(Collectors.toList()); - } - - /** - * Converts {@link AdformParams} {@link List} to price types {@link List} - */ - private List getPriceTypes(List adformParams) { - return adformParams.stream().map(AdformParams::getPriceType).collect(Collectors.toList()); - } - - /** - * Converts {@link AdformParams} {@link List} to cdims {@link List}. - */ - private List getCdims(List adformParams) { - return adformParams.stream().map(AdformParams::getCdims).collect(Collectors.toList()); - } - - /** - * Converts {@link AdformParams} {@link List} to minPrices {@link List}. - */ - private List getMinPrices(List adformParams) { - return adformParams.stream().map(AdformParams::getMinPrice).collect(Collectors.toList()); - } - - /** - * Finds not blank url from {@link AdformParams}. - */ - private String getUrlFromParams(List adformParams) { - return adformParams.stream() - .map(AdformParams::getUrl) - .filter(StringUtils::isNotBlank) - .findFirst() - .orElse(""); - } - - /** - * Creates adform headers, which stores adform request parameters - */ - private MultiMap headers(PreBidRequestContext preBidRequestContext, AdformDigitrust adformDigitrust) { - return httpUtil.buildAdformHeaders( - VERSION, - ObjectUtils.defaultIfNull(preBidRequestContext.getUa(), ""), - ObjectUtils.defaultIfNull(preBidRequestContext.getIp(), ""), - preBidRequestContext.getReferer(), - preBidRequestContext.getUidsCookie().uidFrom(cookieFamilyName), - adformDigitrust); - } - - /** - * Coverts response {@link AdformBid} to {@link Bid} by matching them with {@link AdUnitBid} - */ - private static List toBidBuilder(List adformBids, AdapterRequest adapterRequest) { - final List adUnitBids = adapterRequest.getAdUnitBids(); - final List bidBuilders = new ArrayList<>(); - for (int i = 0; i < adformBids.size(); i++) { - final AdformBid adformBid = adformBids.get(i); - final String banner = adformBid.getBanner(); - if (StringUtils.isNotEmpty(banner) && Objects.equals(adformBid.getResponse(), "banner")) { - final AdUnitBid adUnitBid = adUnitBids.get(i); - bidBuilders.add(Bid.builder() - .bidId(adUnitBid.getBidId()) - .code(adUnitBid.getAdUnitCode()) - .bidder(adapterRequest.getBidderCode()) - .price(adformBid.getWinBid()) - .adm(banner) - .width(adformBid.getWidth()) - .height(adformBid.getHeight()) - .dealId(adformBid.getDealId()) - .creativeId(adformBid.getWinCrid()) - .mediaType(MediaType.banner) - ); - } - } - return bidBuilders; - } -} diff --git a/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java b/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java index e55f66b607d..576bb3754fc 100644 --- a/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java +++ b/src/main/java/org/prebid/server/bidder/adform/AdformBidder.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; @@ -34,7 +33,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -62,14 +60,9 @@ public AdformBidder(String endpointUrl, JacksonMapper mapper) { this.mapper = Objects.requireNonNull(mapper); this.requestUtil = new AdformRequestUtil(); - this.httpUtil = new AdformHttpUtil(mapper); + this.httpUtil = new AdformHttpUtil(); } - /** - * Makes the HTTP requests which should be made to fetch bids. - *

- * Creates GET http request with all parameters in url and headers with empty body. - */ @Override public Result>> makeHttpRequests(BidRequest request) { final List imps = request.getImp(); @@ -78,7 +71,7 @@ public Result>> makeHttpRequests(BidRequest request) { final List errors = extImpAdformsResult.getErrors(); if (extImpAdforms.isEmpty()) { - return Result.of(Collections.emptyList(), errors); + return Result.withErrors(errors); } final String currency = resolveRequestCurrency(request.getCur()); @@ -100,8 +93,8 @@ public Result>> makeHttpRequests(BidRequest request) { .secure(getSecure(imps)) .gdprApplies(requestUtil.getGdprApplies(request.getRegs())) .consent(requestUtil.getConsent(extUser)) - .currency(currency) .eids(requestUtil.getEids(extUser, mapper)) + .currency(currency) .url(getUrl(extImpAdforms)) .build()); @@ -110,8 +103,7 @@ public Result>> makeHttpRequests(BidRequest request) { getUserAgent(device), getIp(device), getReferer(request.getSite()), - getUserId(user), - requestUtil.getAdformDigitrust(extUser)); + getUserId(user)); return Result.of(Collections.singletonList( HttpRequest.builder() @@ -147,14 +139,9 @@ public Result> makeBids(HttpCall httpCall, BidRequest bidR httpResponse.getBody(), mapper.mapper().getTypeFactory().constructCollectionType(List.class, AdformBid.class)); } catch (JsonProcessingException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } - return Result.of(toBidderBid(adformBids, bidRequest.getImp()), Collections.emptyList()); - } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); + return Result.withValues(toBidderBid(adformBids, bidRequest.getImp())); } /** @@ -294,28 +281,45 @@ private String getUrl(List extImpAdforms) { private List toBidderBid(List adformBids, List imps) { final List bidderBids = new ArrayList<>(); - final String currency = CollectionUtils.isNotEmpty(adformBids) ? adformBids.get(0).getWinCur() : null; - for (int i = 0; i < adformBids.size(); i++) { final AdformBid adformBid = adformBids.get(i); - if (StringUtils.isEmpty(adformBid.getBanner()) || !Objects.equals(adformBid.getResponse(), BANNER)) { + final String adm = resolveAdm(adformBid); + if (StringUtils.isBlank(adm)) { continue; } + final BidType bidType = resolveBidType(adformBid.getResponse()); final Imp imp = imps.get(i); bidderBids.add(BidderBid.of(Bid.builder() .id(imp.getId()) .impid(imp.getId()) .price(adformBid.getWinBid()) - .adm(adformBid.getBanner()) + .adm(adm) .w(adformBid.getWidth()) .h(adformBid.getHeight()) .dealid(adformBid.getDealId()) .crid(adformBid.getWinCrid()) .build(), - BidType.banner, - currency)); + bidType, + adformBid.getWinCur())); } return bidderBids; } + + private String resolveAdm(AdformBid adformBid) { + if (Objects.equals(adformBid.getResponse(), "banner")) { + return adformBid.getBanner(); + } + + if (Objects.equals(adformBid.getResponse(), "vast_content")) { + return adformBid.getVastContent(); + } + + return ""; + } + + private BidType resolveBidType(String response) { + return Objects.equals(response, BANNER) + ? BidType.banner : BidType.video; + } } diff --git a/src/main/java/org/prebid/server/bidder/adform/AdformHttpUtil.java b/src/main/java/org/prebid/server/bidder/adform/AdformHttpUtil.java index e57f0b28dbd..6b53abc1d65 100644 --- a/src/main/java/org/prebid/server/bidder/adform/AdformHttpUtil.java +++ b/src/main/java/org/prebid/server/bidder/adform/AdformHttpUtil.java @@ -4,22 +4,17 @@ import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.bidder.adform.model.AdformDigitrust; import org.prebid.server.bidder.adform.model.UrlParameters; -import org.prebid.server.json.EncodeException; -import org.prebid.server.json.JacksonMapper; import org.prebid.server.util.HttpUtil; import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.stream.Collectors; /** - * Util class to help {@link org.prebid.server.bidder.adform.AdformBidder} and - * {@link org.prebid.server.bidder.adform.AdformAdapter} to create adform url and headers + * Util class to help {@link org.prebid.server.bidder.adform.AdformBidder} to create adform url and headers */ class AdformHttpUtil { @@ -30,10 +25,7 @@ class AdformHttpUtil { private static final Locale LOCALE = Locale.US; - private final JacksonMapper mapper; - - AdformHttpUtil(JacksonMapper mapper) { - this.mapper = Objects.requireNonNull(mapper); + AdformHttpUtil() { } /** @@ -43,8 +35,7 @@ MultiMap buildAdformHeaders(String version, String userAgent, String ip, String referer, - String userId, - AdformDigitrust adformDigitrust) { + String userId) { final MultiMap headers = MultiMap.caseInsensitiveMultiMap() .add(HttpUtil.CONTENT_TYPE_HEADER, HttpUtil.APPLICATION_JSON_CONTENT_TYPE) @@ -61,18 +52,6 @@ MultiMap buildAdformHeaders(String version, cookieValues.add(String.format("uid=%s", userId)); } - if (adformDigitrust != null) { - try { - final String adformDigitrustEncoded = Base64.getUrlEncoder().withoutPadding() - .encodeToString(mapper.encode(adformDigitrust).getBytes()); - // Cookie name and structure are described here: - // https://github.com/digi-trust/dt-cdn/wiki/Cookies-for-Platforms - cookieValues.add(String.format("DigiTrust.v1.identity=%s", adformDigitrustEncoded)); - } catch (EncodeException e) { - // do not add digitrust to cookie header and just ignore this exception - } - } - if (CollectionUtils.isNotEmpty(cookieValues)) { headers.add(HttpUtil.COOKIE_HEADER, String.join(";", cookieValues)); } diff --git a/src/main/java/org/prebid/server/bidder/adform/AdformRequestUtil.java b/src/main/java/org/prebid/server/bidder/adform/AdformRequestUtil.java index 3a5dec5e5e0..9cc2260fff0 100644 --- a/src/main/java/org/prebid/server/bidder/adform/AdformRequestUtil.java +++ b/src/main/java/org/prebid/server/bidder/adform/AdformRequestUtil.java @@ -3,12 +3,9 @@ import com.iab.openrtb.request.Regs; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.bidder.adform.model.AdformDigitrust; -import org.prebid.server.bidder.adform.model.AdformDigitrustPrivacy; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid; @@ -19,13 +16,10 @@ import java.util.Map; /** - * Util class to help {@link org.prebid.server.bidder.adform.AdformBidder} and - * {@link org.prebid.server.bidder.adform.AdformAdapter} to retrieve data from request. + * Util class to help {@link org.prebid.server.bidder.adform.AdformBidder} to retrieve data from request. */ class AdformRequestUtil { - private static final int DIGITRUST_VERSION = 1; - /** * Retrieves gdpr from regs.ext.gdpr and in case of any exception or invalid values returns empty string. */ @@ -44,20 +38,6 @@ String getConsent(ExtUser extUser) { return ObjectUtils.defaultIfNull(gdprConsent, ""); } - /** - * Creates {@link AdformDigitrust} from user.extUser.digitrust, if something wrong, returns null. - */ - AdformDigitrust getAdformDigitrust(ExtUser extUser) { - final ExtUserDigiTrust extUserDigiTrust = extUser != null ? extUser.getDigitrust() : null; - return extUserDigiTrust != null - ? AdformDigitrust.of( - extUserDigiTrust.getId(), - DIGITRUST_VERSION, - extUserDigiTrust.getKeyv(), - AdformDigitrustPrivacy.of(extUserDigiTrust.getPref() != 0)) - : null; - } - /** * Retrieves eids from user.ext.eids and in case of any exception or invalid values return empty collection. */ @@ -79,6 +59,6 @@ String getEids(ExtUser extUser, JacksonMapper mapper) { return ObjectUtils .defaultIfNull(Base64.getUrlEncoder().withoutPadding().encodeToString(encodedEids.getBytes()), - ""); + ""); } } diff --git a/src/main/java/org/prebid/server/bidder/adform/model/AdformBid.java b/src/main/java/org/prebid/server/bidder/adform/model/AdformBid.java index f6fc6789f9f..64729679896 100644 --- a/src/main/java/org/prebid/server/bidder/adform/model/AdformBid.java +++ b/src/main/java/org/prebid/server/bidder/adform/model/AdformBid.java @@ -24,4 +24,6 @@ public class AdformBid { String dealId; String winCrid; + + String vastContent; } diff --git a/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrust.java b/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrust.java deleted file mode 100644 index d6dbe48a2a8..00000000000 --- a/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrust.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.bidder.adform.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class AdformDigitrust { - - String id; - - Integer version; - - Integer keyv; - - AdformDigitrustPrivacy privacy; -} diff --git a/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrustPrivacy.java b/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrustPrivacy.java deleted file mode 100644 index e654da1e065..00000000000 --- a/src/main/java/org/prebid/server/bidder/adform/model/AdformDigitrustPrivacy.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.adform.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class AdformDigitrustPrivacy { - - Boolean optout; -} diff --git a/src/main/java/org/prebid/server/bidder/adgeneration/AdgenerationBidder.java b/src/main/java/org/prebid/server/bidder/adgeneration/AdgenerationBidder.java index 7ab45c530fb..08fa6540283 100644 --- a/src/main/java/org/prebid/server/bidder/adgeneration/AdgenerationBidder.java +++ b/src/main/java/org/prebid/server/bidder/adgeneration/AdgenerationBidder.java @@ -1,7 +1,7 @@ package org.prebid.server.bidder.adgeneration; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Format; @@ -9,7 +9,6 @@ import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Source; import com.iab.openrtb.response.Bid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; @@ -31,10 +30,9 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; +import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -64,27 +62,24 @@ public AdgenerationBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - if (CollectionUtils.isEmpty(request.getImp())) { - return Result.emptyWithError(BidderError.badInput("No impression in the bid request")); - } - + final List> requests = new ArrayList<>(); final List errors = new ArrayList<>(); - final List> result = new ArrayList<>(); for (Imp imp : request.getImp()) { try { final ExtImpAdgeneration extImpAdgeneration = parseAndValidateImpExt(imp); - final String extImpAdgenerationId = extImpAdgeneration.getId(); + final String adgenerationId = extImpAdgeneration.getId(); final String adSizes = getAdSize(imp); final String currency = getCurrency(request); - final String uri = getUri(endpointUrl, adSizes, extImpAdgenerationId, currency, + final String uri = getUri(adSizes, adgenerationId, currency, request.getSite(), request.getSource()); - result.add(createSingleRequest(uri, request.getDevice())); + + requests.add(createSingleRequest(uri, request.getDevice())); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } } - return Result.of(result, errors); + return Result.of(requests, errors); } private ExtImpAdgeneration parseAndValidateImpExt(Imp imp) { @@ -102,9 +97,15 @@ private ExtImpAdgeneration parseAndValidateImpExt(Imp imp) { return extImpAdgeneration; } - private String getUri(String endpointUrl, String adSize, String id, String currency, Site site, Source source) { - final URIBuilder uriBuilder = new URIBuilder() - .setPath(endpointUrl) + private String getUri(String adSize, String id, String currency, Site site, Source source) { + final URIBuilder uriBuilder; + try { + uriBuilder = new URIBuilder(endpointUrl); + } catch (URISyntaxException e) { + throw new PreBidException(String.format("Invalid url: %s, error: %s", endpointUrl, e.getMessage())); + } + + uriBuilder .addParameter("posall", "SSPLOC") .addParameter("id", id) .addParameter("sdktype", "0") @@ -123,31 +124,30 @@ private String getUri(String endpointUrl, String adSize, String id, String curre uriBuilder.addParameter("tp", page); } - final String transactionid = source != null ? source.getTid() : null; - if (StringUtils.isNotBlank(transactionid)) { - uriBuilder.addParameter("transactionid", transactionid); + final String transactionId = source != null ? source.getTid() : null; + if (StringUtils.isNotBlank(transactionId)) { + uriBuilder.addParameter("transactionid", transactionId); } return uriBuilder.toString(); } - private String getAdSize(Imp imp) { - final List formats = imp.getBanner() == null ? null : imp.getBanner().getFormat(); - return CollectionUtils.isEmpty(formats) - ? null - : formats.stream() + private static String getAdSize(Imp imp) { + final Banner banner = imp.getBanner(); + final List formats = banner != null ? banner.getFormat() : null; + return CollectionUtils.emptyIfNull(formats).stream() .map(format -> String.format("%sx%s", format.getW(), format.getH())) .collect(Collectors.joining(",")); } - private String getCurrency(BidRequest bidRequest) { + private static String getCurrency(BidRequest bidRequest) { final List currencies = bidRequest.getCur(); return CollectionUtils.isEmpty(currencies) ? DEFAULT_REQUEST_CURRENCY : currencies.contains(DEFAULT_REQUEST_CURRENCY) ? DEFAULT_REQUEST_CURRENCY : currencies.get(0); } - private HttpRequest createSingleRequest(String uri, Device device) { + private static HttpRequest createSingleRequest(String uri, Device device) { return HttpRequest.builder() .method(HttpMethod.GET) .uri(uri) @@ -155,7 +155,7 @@ private HttpRequest createSingleRequest(String uri, Device device) { .build(); } - private MultiMap resolveHeaders(Device device) { + private static MultiMap resolveHeaders(Device device) { final MultiMap headers = HttpUtil.headers(); final String userAgent = device != null ? device.getUa() : null; @@ -167,20 +167,15 @@ private MultiMap resolveHeaders(Device device) { @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - try { final AdgenerationResponse adgenerationResponse = decodeBodyToBidResponse(httpCall.getResponse()); if (CollectionUtils.isEmpty(adgenerationResponse.getResults())) { - return Result.emptyWithError(BidderError.badServerResponse("Results object in BidResponse is empty")); + return Result.withError(BidderError.badServerResponse("Results object in BidResponse is empty")); } return resultWithBidderBids(bidRequest, adgenerationResponse); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } @@ -211,7 +206,7 @@ private Result> resultWithBidderBids(BidRequest bidRequest, .dealid(adgenerationResponse.getDealid()) .build(); final BidderBid bidderBid = BidderBid.of(updatedBid, BidType.banner, getCurrency(bidRequest)); - return Result.of(Collections.singletonList(bidderBid), Collections.emptyList()); + return Result.withValue(bidderBid); } } return null; @@ -247,9 +242,4 @@ private String removeWrapper(String ad) { ? "" : ad.replace("", "").replaceFirst("", "").trim(); } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java index 9c558a998b3..df9545801f4 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java +++ b/src/main/java/org/prebid/server/bidder/adhese/AdheseBidder.java @@ -3,25 +3,32 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.adhese.model.AdheseBid; import org.prebid.server.bidder.adhese.model.AdheseOriginData; +import org.prebid.server.bidder.adhese.model.AdheseRequestBody; import org.prebid.server.bidder.adhese.model.AdheseResponseExt; +import org.prebid.server.bidder.adhese.model.Cpm; +import org.prebid.server.bidder.adhese.model.CpmValues; +import org.prebid.server.bidder.adhese.model.Prebid; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; @@ -33,24 +40,25 @@ import org.prebid.server.util.HttpUtil; import java.math.BigDecimal; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.TreeMap; -import java.util.stream.Collectors; +/** + * Adhese {@link Bidder} implementation. + */ public class AdheseBidder implements Bidder { private static final TypeReference> ADHESE_EXT_TYPE_REFERENCE = new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; - private static final String ORIGIN = "JERLICIA"; - private static final String QUERY_PARAMETER_GDPR = "/xt"; - private static final String QUERY_PARAMETER_REFERER = "/xf"; + private static final String ORIGIN_BID = "JERLICIA"; + private static final String GDPR_QUERY_PARAMETER = "xt"; + private static final String REFERER_QUERY_PARAMETER = "xf"; + private static final String IFA_QUERY_PARAMETER = "xz"; private final String endpointUrl; private final JacksonMapper mapper; @@ -63,29 +71,37 @@ public AdheseBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { if (CollectionUtils.isEmpty(request.getImp())) { - return Result.emptyWithError(BidderError.badInput("No impression in the bid request")); + return Result.withError(BidderError.badInput("No impression in the bid request")); } - ExtImpAdhese extImpAdhese; + final ExtImpAdhese extImpAdhese; try { extImpAdhese = parseImpExt(request.getImp().get(0)); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badInput(e.getMessage())); + return Result.withError(BidderError.badInput(e.getMessage())); } - final String uri = buildUrl(request, endpointUrl, extImpAdhese); + final String uri = getUrl(extImpAdhese); return Result.of(Collections.singletonList( HttpRequest.builder() - .method(HttpMethod.GET) + .method(HttpMethod.POST) .uri(uri) - .body(null) - .headers(HttpUtil.headers()) - .payload(null) + .body(mapper.encode(buildBody(request, extImpAdhese))) + .headers(replaceHeaders(request.getDevice())) .build()), Collections.emptyList()); } + private MultiMap replaceHeaders(Device device) { + MultiMap headers = HttpUtil.headers(); + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpHeaders.createOptimized("X-Real-IP"), device.getIp()); + } + return headers; + } + private ExtImpAdhese parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), ADHESE_EXT_TYPE_REFERENCE).getBidder(); @@ -94,83 +110,122 @@ private ExtImpAdhese parseImpExt(Imp imp) { } } - private String buildUrl(BidRequest request, String endpointUrl, ExtImpAdhese extImpAdhese) { - final String uri = endpointUrl.replace("{{AccountId}}", extImpAdhese.getAccount()); - final String slotParameter = String.format("/sl%s-%s", HttpUtil.encodeUrl(extImpAdhese.getLocation()), - HttpUtil.encodeUrl(extImpAdhese.getFormat())); - - return String.format("%s%s%s%s%s", uri, slotParameter, getTargetParameters(extImpAdhese), - getGdprParameter(request.getUser()), getRefererParameter(request.getSite())); + private AdheseRequestBody buildBody(BidRequest request, ExtImpAdhese extImpAdhese) { + final Map> parameterMap = new TreeMap<>(); + parameterMap.putAll(getTargetParameters(extImpAdhese)); + parameterMap.putAll(getGdprParameter(request.getUser())); + parameterMap.putAll(getRefererParameter(request.getSite())); + parameterMap.putAll(getIfaParameter(request.getDevice())); + + return AdheseRequestBody.builder() + .slots(Collections.singletonList( + AdheseRequestBody.Slot.builder() + .slotname(getSlotParameter(extImpAdhese)) + .build())) + .parameters(parameterMap) + .build(); } - private String getTargetParameters(ExtImpAdhese extImpAdhese) { - final JsonNode keywords = extImpAdhese.getKeywords(); - if (keywords == null || keywords.isNull()) { - return ""; - } + private String getUrl(ExtImpAdhese extImpAdhese) { + return endpointUrl.replace("{{AccountId}}", extImpAdhese.getAccount()); + } - final Map> targetParameters = parseTargetParametersAndSort(extImpAdhese.getKeywords()); - return targetParameters.entrySet().stream() - .map(stringListEntry -> createPartOrUrl(stringListEntry.getKey(), stringListEntry.getValue())) - .collect(Collectors.joining()); + private static String getSlotParameter(ExtImpAdhese extImpAdhese) { + return String.format("%s-%s", + HttpUtil.encodeUrl(extImpAdhese.getLocation()), + HttpUtil.encodeUrl(extImpAdhese.getFormat())); } - private Map> parseTargetParametersAndSort(JsonNode keywords) { - return keywords != null ? new TreeMap<>( - mapper.mapper().convertValue(keywords, new TypeReference>>() { - })) : null; + private Map> getTargetParameters(ExtImpAdhese extImpAdhese) { + final JsonNode targets = extImpAdhese.getTargets(); + return targets == null || targets.isEmpty() ? Collections.emptyMap() : parseTargetParametersAndSort(targets); } - private String createPartOrUrl(String key, List values) { - final String formattedValues = String.join(";", values); - return String.format("/%s%s", HttpUtil.encodeUrl(key), formattedValues); + private Map> parseTargetParametersAndSort(JsonNode targets) { + return new TreeMap<>( + mapper.mapper().convertValue(targets, new TypeReference>>() { + })); } - private String getGdprParameter(User user) { + private static Map> getGdprParameter(User user) { final ExtUser extUser = user != null ? user.getExt() : null; final String consent = extUser != null ? extUser.getConsent() : null; - return StringUtils.isNotBlank(consent) ? String.format("%s%s", QUERY_PARAMETER_GDPR, consent) : ""; + return StringUtils.isNotBlank(consent) + ? Collections.singletonMap(GDPR_QUERY_PARAMETER, Collections.singletonList(consent)) + : Collections.emptyMap(); } - private String getRefererParameter(Site site) { + private static Map> getRefererParameter(Site site) { final String page = site != null ? site.getPage() : null; return StringUtils.isNotBlank(page) - ? String.format("%s%s", QUERY_PARAMETER_REFERER, HttpUtil.encodeUrl(page)) - : ""; + ? Collections.singletonMap(REFERER_QUERY_PARAMETER, Collections.singletonList(page)) + : Collections.emptyMap(); + } + + private static Map> getIfaParameter(Device device) { + final String ifa = device != null ? device.getIfa() : null; + return StringUtils.isNotBlank(ifa) + ? Collections.singletonMap(IFA_QUERY_PARAMETER, Collections.singletonList(ifa)) + : Collections.emptyMap(); } @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { + final HttpResponse httpResponse = httpCall.getResponse(); + + final JsonNode bodyNode; + try { + bodyNode = mapper.decodeValue(httpResponse.getBody(), JsonNode.class); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + if (!bodyNode.isArray()) { + return Result.withError(BidderError.badServerResponse("Unexpected response body")); + } + if (bodyNode.size() == 0) { return Result.empty(); } - final List adheseBid; - final List adheseResponseExt; - final List adheseOriginData; - final AdheseBid firstAdheseBid; - Bid bid; + final JsonNode bidNode = bodyNode.get(0); + final AdheseBid adheseBid; try { - adheseBid = decodeBodyToBidList(httpCall, AdheseBid.class); - firstAdheseBid = adheseBid.get(0); - if (Objects.equals(firstAdheseBid.getOrigin(), ORIGIN)) { - adheseResponseExt = decodeBodyToBidList(httpCall, AdheseResponseExt.class); - adheseOriginData = decodeBodyToBidList(httpCall, AdheseOriginData.class); - bid = convertAdheseBid(firstAdheseBid, adheseResponseExt.get(0), adheseOriginData.get(0)); - } else { - bid = convertAdheseOpenRtbBid(firstAdheseBid); - } + adheseBid = toObjectOfType(bidNode, AdheseBid.class); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + + final Bid bid; + if (Objects.equals(adheseBid.getOrigin(), ORIGIN_BID)) { + final AdheseResponseExt responseExt; + final AdheseOriginData originData; + try { + responseExt = toObjectOfType(bidNode, AdheseResponseExt.class); + originData = toObjectOfType(bidNode, AdheseOriginData.class); + } catch (PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + bid = convertAdheseBid(adheseBid, responseExt, originData); + } else { + bid = convertAdheseOpenRtbBid(adheseBid); + } + if (bid == null) { + return Result.withError(BidderError.badServerResponse("Response resulted in an empty seatBid array")); } - final BigDecimal price = new BigDecimal(firstAdheseBid.getExtension().getPrebid().getCpm().getAmount()); - final Integer width = Integer.valueOf(firstAdheseBid.getWidth()); - final Integer height = Integer.valueOf(firstAdheseBid.getHeight()); + final BigDecimal price; + final Integer width; + final Integer height; + try { + price = getPrice(adheseBid); + width = Integer.valueOf(adheseBid.getWidth()); + height = Integer.valueOf(adheseBid.getHeight()); + } catch (NumberFormatException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } - final Bid updateBid = bid != null - ? Bid.builder() + final Bid updatedBid = Bid.builder() + .id("1") // hardcoded because it is not provided and should be not empty value + .impid(bidRequest.getImp().get(0).getId()) .price(price) .w(width) .h(height) @@ -178,42 +233,27 @@ public Result> makeBids(HttpCall httpCall, BidRequest bidR .crid(bid.getCrid()) .adm(bid.getAdm()) .ext(bid.getExt()) - .build() - : null; - - if (updateBid == null) { - return Result.emptyWithError(BidderError - .badServerResponse("Response resulted in an empty seatBid array. %s.")); - } + .build(); - /** - * Used ImpId from Imp of bidRequest, because it is not provided and should be not empty value - */ - final List errors = new ArrayList<>(); - return Result.of(Collections.singletonList(makeBid(updateBid, bidRequest.getImp().get(0).getId(), errors)), - errors); + final BidderBid bidderBid = BidderBid.of(updatedBid, getBidType(bid.getAdm()), getCurrency(adheseBid)); + return Result.of(Collections.singletonList(bidderBid), Collections.emptyList()); } - private List decodeBodyToBidList(HttpCall httpCall, Class bidClassName) { + private T toObjectOfType(JsonNode jsonNode, Class clazz) { try { - return mapper.mapper().readValue( - httpCall.getResponse().getBody(), - mapper.mapper().getTypeFactory().constructCollectionType(List.class, bidClassName)); - } catch (DecodeException | JsonProcessingException e) { + return mapper.mapper().treeToValue(jsonNode, clazz); + } catch (JsonProcessingException e) { throw new PreBidException(e.getMessage(), e); } } private Bid convertAdheseBid(AdheseBid adheseBid, AdheseResponseExt adheseResponseExt, AdheseOriginData adheseOriginData) { - final ObjectNode adheseExtJson = mapper.mapper().valueToTree(adheseOriginData); - return Bid.builder() - .id("1") .dealid(adheseResponseExt.getOrderId()) .crid(adheseResponseExt.getId()) .adm(getAdMarkup(adheseBid, adheseResponseExt)) - .ext(adheseExtJson) + .ext(mapper.mapper().valueToTree(adheseOriginData)) .build(); } @@ -235,35 +275,37 @@ private String getAdMarkup(AdheseBid adheseBid, AdheseResponseExt adheseResponse } private Bid convertAdheseOpenRtbBid(AdheseBid adheseBid) { - final List seatbid = adheseBid.getOriginData().getSeatbid(); - return CollectionUtils.isNotEmpty(seatbid) - && CollectionUtils.isNotEmpty(seatbid.get(0).getBid()) - ? Bid.builder().adm(adheseBid.getBody()).build() - : null; + final BidResponse originData = adheseBid.getOriginData(); + final List seatBids = originData != null ? originData.getSeatbid() : null; + + return CollectionUtils.emptyIfNull(seatBids).stream() + .filter(Objects::nonNull) + .flatMap(seatBid -> seatBid.getBid().stream()) + .filter(Objects::nonNull) + .findFirst() + .map(bid -> bid.toBuilder().adm(adheseBid.getBody()).build()) + .orElse(null); } - private static BidderBid makeBid(Bid bid, String impId, List errors) { - /** - * Hardcoded bidId =1, because it is not provided and should be not empty value - */ - try { - final BidType bidType = getBidType(bid.getAdm()); - final Bid updateBid = bid.toBuilder().id("1").impid(impId).build(); - return BidderBid.of(updateBid, bidType, DEFAULT_BID_CURRENCY); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - return null; - } + private static BigDecimal getPrice(AdheseBid adheseBid) { + final CpmValues cpmValues = getCpmValues(adheseBid.getExtension()); + final String amount = cpmValues != null ? cpmValues.getAmount() : null; + return new BigDecimal(StringUtils.stripToEmpty(amount)); + } + + private static String getCurrency(AdheseBid adheseBid) { + final CpmValues cpmValues = getCpmValues(adheseBid.getExtension()); + return cpmValues != null ? StringUtils.stripToNull(cpmValues.getCurrency()) : null; + } + + private static CpmValues getCpmValues(Prebid prebid) { + final Cpm cpm = prebid != null ? prebid.getPrebid() : null; + return cpm != null ? cpm.getCpm() : null; } private static BidType getBidType(String bidAdm) { - return (StringUtils.isNotBlank(bidAdm) && StringUtils.containsAny(bidAdm, " extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java new file mode 100644 index 00000000000..608f79d905b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseRequestBody.java @@ -0,0 +1,22 @@ +package org.prebid.server.bidder.adhese.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Builder +@Value +public class AdheseRequestBody { + + List slots; + Map> parameters; + + @Builder + @Value + public static class Slot { + + String slotname; + } +} diff --git a/src/main/java/org/prebid/server/bidder/adkernel/AdkernelBidder.java b/src/main/java/org/prebid/server/bidder/adkernel/AdkernelBidder.java index 8c283cd9778..7558256f61d 100644 --- a/src/main/java/org/prebid/server/bidder/adkernel/AdkernelBidder.java +++ b/src/main/java/org/prebid/server/bidder/adkernel/AdkernelBidder.java @@ -1,7 +1,6 @@ package org.prebid.server.bidder.adkernel; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -10,6 +9,7 @@ import com.iab.openrtb.response.SeatBid; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -43,8 +43,6 @@ public class AdkernelBidder implements Bidder { new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; - private final String endpointTemplate; private final JacksonMapper mapper; @@ -59,16 +57,14 @@ public Result>> makeHttpRequests(BidRequest request final Map> pubToImps = new HashMap<>(); for (Imp imp : request.getImp()) { try { - validateImp(imp); - final ExtImpAdkernel extImpAdkernel = parseAndValidateImpExt(imp); - dispatchImpression(imp, extImpAdkernel, pubToImps); + processImp(imp, pubToImps); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } } if (hasNoImpressions(pubToImps)) { - return Result.of(null, errors); + return Result.withErrors(errors); } final BidRequest.BidRequestBuilder requestBuilder = request.toBuilder(); @@ -79,6 +75,12 @@ public Result>> makeHttpRequests(BidRequest request return Result.of(httpRequests, errors); } + private void processImp(Imp imp, Map> pubToImps) { + validateImp(imp); + final ExtImpAdkernel extImpAdkernel = parseAndValidateImpExt(imp); + dispatchImpression(imp, extImpAdkernel, pubToImps); + } + private static void validateImp(Imp imp) { if (imp.getBanner() == null && imp.getVideo() == null) { throw new PreBidException(String.format( @@ -106,16 +108,14 @@ private ExtImpAdkernel parseAndValidateImpExt(Imp imp) { return extImpAdkernel; } - //Group impressions by AdKernel-specific parameters `zoneId` & `host` private static void dispatchImpression(Imp imp, ExtImpAdkernel extImpAdkernel, Map> pubToImp) { pubToImp.putIfAbsent(extImpAdkernel, new ArrayList<>()); pubToImp.get(extImpAdkernel).add(compatImpression(imp)); } - //Alter impression info to comply with adkernel platform requirements private static Imp compatImpression(Imp imp) { - final Imp.ImpBuilder impBuilder = imp.toBuilder().ext(null) //do not forward ext to adkernel platform + final Imp.ImpBuilder impBuilder = imp.toBuilder().ext(null) .audio(null) .xNative(null); return imp.getBanner() != null ? impBuilder.video(null).build() : impBuilder.build(); @@ -123,8 +123,7 @@ private static Imp compatImpression(Imp imp) { private static boolean hasNoImpressions(Map> pubToImps) { return pubToImps.values().stream() - .mapToLong(Collection::size) - .sum() == 0; + .allMatch(CollectionUtils::isEmpty); } private HttpRequest createHttpRequest(Map.Entry> extAndImp, @@ -133,7 +132,7 @@ private HttpRequest createHttpRequest(Map.Entry createHttpRequest(Map.Entry imps, BidRequest.BidRequestBuilder requestBuilder, Site site, App app) { + private static BidRequest createBidRequest(List imps, + BidRequest.BidRequestBuilder requestBuilder, + Site site, + App app) { requestBuilder.imp(imps); @@ -164,9 +165,9 @@ private static BidRequest createBidRequest( public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } @@ -184,13 +185,10 @@ private static List bidsFromResponse(BidRequest bidRequest, BidRespon return bidResponse.getSeatbid().stream() .map(SeatBid::getBid) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getType(bid.getImpid(), bidRequest.getImp()), DEFAULT_BID_CURRENCY)) + .map(bid -> BidderBid.of(bid, getType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); } - /** - * Figures out which media type this bid is for. - */ private static BidType getType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId) && imp.getBanner() != null) { @@ -199,9 +197,4 @@ private static BidType getType(String impId, List imps) { } return BidType.video; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java b/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java index eca5e25bb55..91d95e67ae2 100644 --- a/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java +++ b/src/main/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidder.java @@ -1,7 +1,6 @@ package org.prebid.server.bidder.adkerneladn; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; @@ -13,6 +12,8 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -27,8 +28,6 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; -import java.net.MalformedURLException; -import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -46,8 +45,9 @@ public class AdkernelAdnBidder implements Bidder { private static final TypeReference> ADKERNELADN_EXT_TYPE_REFERENCE = new TypeReference>() { }; - - private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final String DEFAULT_DOMAIN = "tag.adkernel.com"; + private static final String URL_HOST_MACRO = "{{Host}}"; + private static final String URL_PUBLISHER_ID_MACRO = "{{PublisherID}}"; private final String endpointUrl; private final JacksonMapper mapper; @@ -59,39 +59,36 @@ public AdkernelAdnBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest bidRequest) { - final List imps = bidRequest.getImp(); - + final List validImps = bidRequest.getImp(); final List errors = new ArrayList<>(); - final List> httpRequests = new ArrayList<>(); - try { - final List impExts = getAndValidateImpExt(imps); - final Map> pubToImps = dispatchImpressions(imps, impExts); - httpRequests.addAll(buildAdapterRequests(bidRequest, pubToImps, endpointUrl)); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - } - return Result.of(httpRequests, errors); - } + final Map impWithExts = getAndValidateImpExt(validImps, errors); + final Map> pubToImps = dispatchImpressions(impWithExts, errors); + if (MapUtils.isEmpty(pubToImps)) { + return Result.withErrors(errors); + } - private static MultiMap headers() { - return HttpUtil.headers() - .add("x-openrtb-version", "2.5"); + return Result.of(buildAdapterRequests(bidRequest, pubToImps), errors); } - private List getAndValidateImpExt(List imps) { - return imps.stream() - .map(AdkernelAdnBidder::validateImp) - .map(this::parseAndValidateAdkernelAdnExt) - .collect(Collectors.toList()); + private Map getAndValidateImpExt(List imps, List errors) { + final Map validImpsWithExts = new HashMap<>(); + for (Imp imp : imps) { + try { + validateImp(imp); + validImpsWithExts.put(imp, parseAndValidateAdkernelAdnExt(imp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + return validImpsWithExts; } - private static Imp validateImp(Imp imp) { + private static void validateImp(Imp imp) { if (imp.getBanner() == null && imp.getVideo() == null) { throw new PreBidException(String.format("Invalid imp with id=%s. Expected imp.banner or imp.video", imp.getId())); } - return imp; } private ExtImpAdkernelAdn parseAndValidateAdkernelAdnExt(Imp imp) { @@ -108,41 +105,42 @@ private ExtImpAdkernelAdn parseAndValidateAdkernelAdnExt(Imp imp) { return adkernelAdnExt; } - /** - * Group impressions by AdKernel-specific parameters `pubId` & `host`. - */ - private static Map> dispatchImpressions(List imps, - List impExts) { + private static Map> dispatchImpressions(Map impsWithExts, + List errors) { final Map> result = new HashMap<>(); - for (int i = 0; i < imps.size(); i++) { - final Imp imp = compatImpression(imps.get(i)); - final ExtImpAdkernelAdn impExt = impExts.get(i); + for (Imp key : impsWithExts.keySet()) { + final Imp imp; + try { + imp = compatImpression(key); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + continue; + } + final ExtImpAdkernelAdn impExt = impsWithExts.get(key); result.putIfAbsent(impExt, new ArrayList<>()); result.get(impExt).add(imp); } + return result; } - /** - * Alter impression info to comply with adkernel platform requirements. - */ private static Imp compatImpression(Imp imp) { final Imp.ImpBuilder impBuilder = imp.toBuilder(); - impBuilder.ext(null); // do not forward ext to adkernel platform + impBuilder.ext(null); final Banner banner = imp.getBanner(); if (banner != null) { - return compatBannerImpression(impBuilder, banner); + compatBannerImpression(impBuilder, banner); } - return impBuilder.audio(null) + return impBuilder + .audio(null) .xNative(null) .build(); } - private static Imp compatBannerImpression(Imp.ImpBuilder impBuilder, Banner compatBanner) { + private static void compatBannerImpression(Imp.ImpBuilder impBuilder, Banner compatBanner) { if (compatBanner.getW() == null && compatBanner.getH() == null) { - // As banner.w/h are required fields for adkernel adn platform - take the first format entry final List compatBannerFormat = compatBanner.getFormat(); if (CollectionUtils.isEmpty(compatBannerFormat)) { @@ -164,32 +162,32 @@ private static Imp compatBannerImpression(Imp.ImpBuilder impBuilder, Banner comp impBuilder.banner(bannerBuilder.build()); } - return impBuilder.video(null) - .audio(null) - .xNative(null) - .build(); + impBuilder.video(null); } private List> buildAdapterRequests(BidRequest preBidRequest, - Map> pubToImps, - String endpointUrl) { + Map> pubToImps) { final List> result = new ArrayList<>(); for (Map.Entry> entry : pubToImps.entrySet()) { - final BidRequest outgoingRequest = createBidRequest(preBidRequest, entry.getValue()); - final String body = mapper.encode(outgoingRequest); - result.add(HttpRequest.builder() - .method(HttpMethod.POST) - .uri(buildEndpoint(entry.getKey(), endpointUrl)) - .body(body) - .headers(headers()) - .payload(outgoingRequest) - .build()); + result.add(createRequest(entry.getKey(), entry.getValue(), preBidRequest)); } return result; } + private HttpRequest createRequest(ExtImpAdkernelAdn extImp, List imps, BidRequest preBidRequest) { + final BidRequest outgoingRequest = createBidRequest(preBidRequest, imps); + final String body = mapper.encode(outgoingRequest); + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(buildEndpoint(extImp)) + .body(body) + .headers(headers()) + .payload(outgoingRequest) + .build(); + } + private static BidRequest createBidRequest(BidRequest preBidRequest, List imps) { final BidRequest.BidRequestBuilder bidRequestBuilder = preBidRequest.toBuilder() .imp(imps); @@ -206,36 +204,27 @@ private static BidRequest createBidRequest(BidRequest preBidRequest, List i return bidRequestBuilder.build(); } - /** - * Builds endpoint url based on adapter-specific pub settings from imp.ext. - */ - private static String buildEndpoint(ExtImpAdkernelAdn impExt, String endpointUrl) { - final String updatedEndpointUrl; + private String buildEndpoint(ExtImpAdkernelAdn impExt) { + final String impHost = impExt.getHost(); + final String host = StringUtils.isNotBlank(impHost) ? impHost : DEFAULT_DOMAIN; - if (impExt.getHost() != null) { - final URL url; - try { - url = new URL(endpointUrl); - } catch (MalformedURLException e) { - throw new PreBidException( - String.format("Error occurred while parsing AdkernelAdn endpoint url: %s", endpointUrl), e); - } - final String currentHostAndPort = url.getHost() + (url.getPort() == -1 ? "" : ":" + url.getPort()); - updatedEndpointUrl = endpointUrl.replace(currentHostAndPort, impExt.getHost()); - } else { - updatedEndpointUrl = endpointUrl; - } + return endpointUrl + .replace(URL_HOST_MACRO, host) + .replace(URL_PUBLISHER_ID_MACRO, impExt.getPubId().toString()); + } - return String.format("%s%s", updatedEndpointUrl, impExt.getPubId()); + private static MultiMap headers() { + return HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); } @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } @@ -252,14 +241,13 @@ private static List extractBids(BidRequest bidRequest, BidResponse bi private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { return bidResponse.getSeatbid().stream() .map(SeatBid::getBid) + .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getType(bid.getImpid(), bidRequest.getImp()), DEFAULT_BID_CURRENCY)) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); } - /** - * Figures out which media type this bid is for. - */ private static BidType getType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId) && imp.getBanner() != null) { @@ -268,9 +256,4 @@ private static BidType getType(String impId, List imps) { } return BidType.video; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/adman/AdmanBidder.java b/src/main/java/org/prebid/server/bidder/adman/AdmanBidder.java index 45634bf5a4d..1760b406b61 100644 --- a/src/main/java/org/prebid/server/bidder/adman/AdmanBidder.java +++ b/src/main/java/org/prebid/server/bidder/adman/AdmanBidder.java @@ -1,10 +1,15 @@ package org.prebid.server.bidder.adman; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.OpenrtbBidder; +import org.prebid.server.exception.PreBidException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.request.adman.ExtImpAdman; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.List; /** * Adman {@link Bidder} implementation. @@ -21,4 +26,15 @@ protected Imp modifyImp(Imp imp, ExtImpAdman impExt) { .tagid(impExt.getTagId()) .build(); } + + @Override + protected BidType getBidType(Bid bid, List imps) { + final String impId = bid.getImpid(); + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + return imp.getBanner() == null && imp.getVideo() != null ? BidType.video : BidType.banner; + } + } + throw new PreBidException(String.format("Failed to find impression %s", impId)); + } } diff --git a/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java b/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java index df76f0f9504..0ab9489dc9c 100644 --- a/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java +++ b/src/main/java/org/prebid/server/bidder/admixer/AdmixerBidder.java @@ -7,9 +7,9 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -42,8 +42,6 @@ public class AdmixerBidder implements Bidder { new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; - private final String endpointUrl; private final JacksonMapper mapper; @@ -57,11 +55,6 @@ public Result>> makeHttpRequests(BidRequest request final List errors = new ArrayList<>(); final List validImps = new ArrayList<>(); - if (CollectionUtils.isEmpty(request.getImp())) { - errors.add(BidderError.badInput("No valid impressions in the bid request")); - return Result.of(Collections.emptyList(), errors); - } - for (Imp imp : request.getImp()) { try { final ExtImpAdmixer extImp = parseImpExt(imp); @@ -86,21 +79,25 @@ public Result>> makeHttpRequests(BidRequest request } private ExtImpAdmixer parseImpExt(Imp imp) { + final ExtImpAdmixer extImpAdmixer; try { - return mapper.mapper().convertValue(imp.getExt(), ADMIXER_EXT_TYPE_REFERENCE).getBidder(); + extImpAdmixer = mapper.mapper().convertValue(imp.getExt(), ADMIXER_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException(String.format("Wrong Admixer bidder ext in imp with id : %s", imp.getId())); } - } - - private Imp processImp(Imp imp, ExtImpAdmixer extImpAdmixer) { - if (extImpAdmixer.getZone().length() != 36) { + if (StringUtils.length(extImpAdmixer.getZone()) != 36) { throw new PreBidException("ZoneId must be UUID/GUID"); } + return extImpAdmixer; + } + + private Imp processImp(Imp imp, ExtImpAdmixer extImpAdmixer) { + final Double extImpFloor = extImpAdmixer.getCustomFloor(); + final BigDecimal customFloor = extImpFloor != null ? BigDecimal.valueOf(extImpFloor) : BigDecimal.ZERO; return imp.toBuilder() .tagid(extImpAdmixer.getZone()) - .bidfloor(BigDecimal.valueOf(extImpAdmixer.getCustomFloor())) + .bidfloor(customFloor) .ext(makeImpExt(extImpAdmixer.getCustomParams())) .build(); } @@ -113,29 +110,26 @@ private ObjectNode makeImpExt(Map customParams) { @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - final BidResponse bidResponse; try { bidResponse = decodeBodyToBidResponse(httpCall); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } - if (CollectionUtils.isEmpty(bidResponse.getSeatbid()) + if (bidResponse == null + || CollectionUtils.isEmpty(bidResponse.getSeatbid()) || CollectionUtils.isEmpty(bidResponse.getSeatbid().get(0).getBid())) { - return Result.of(Collections.emptyList(), Collections.emptyList()); + return Result.empty(); } final List bidderBids = bidResponse.getSeatbid().stream() .map(SeatBid::getBid) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), DEFAULT_BID_CURRENCY)) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); - return Result.of(bidderBids, Collections.emptyList()); + + return Result.withValues(bidderBids); } private BidResponse decodeBodyToBidResponse(HttpCall httpCall) { @@ -162,9 +156,4 @@ private static BidType getBidType(String impId, List imps) { } return BidType.banner; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/adocean/AdoceanBidder.java b/src/main/java/org/prebid/server/bidder/adocean/AdoceanBidder.java index f8c6b1ace92..d398d7fcb1e 100644 --- a/src/main/java/org/prebid/server/bidder/adocean/AdoceanBidder.java +++ b/src/main/java/org/prebid/server/bidder/adocean/AdoceanBidder.java @@ -1,16 +1,21 @@ package org.prebid.server.bidder.adocean; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URIBuilder; import org.apache.http.client.utils.URLEncodedUtils; @@ -36,20 +41,24 @@ import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; +/** + * Adocean {@link Bidder} implementation. + */ public class AdoceanBidder implements Bidder { private static final TypeReference> ADOCEAN_EXT_TYPE_REFERENCE = new TypeReference>() { }; - private static final String VERSION = "1.0.0"; + private static final String VERSION = "1.2.0"; private static final int MAX_URI_LENGTH = 8000; - private static final String DEFAULT_BID_CURRENCY = "USD"; private static final String MEASUREMENT_CODE_TEMPLATE = " ", eventTracker.getUrl()); + } + } + + return mapper.encode(response.toBuilder() + .eventtrackers(null) + .jstracker(jstracker) + .imptrackers(imptrackers) + .build()); + } + + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getXNative() != null) { + return BidType.xNative; + } else if (imp.getBanner() != null) { + return BidType.banner; + } + } + } + throw new PreBidException(String.format("Failed to find native/banner impression \"%s\"", impId)); + } +} diff --git a/src/main/java/org/prebid/server/bidder/pangle/PangleBidder.java b/src/main/java/org/prebid/server/bidder/pangle/PangleBidder.java new file mode 100644 index 00000000000..2ae2fc47348 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pangle/PangleBidder.java @@ -0,0 +1,228 @@ +package org.prebid.server.bidder.pangle; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.pangle.model.BidExt; +import org.prebid.server.bidder.pangle.model.NetworkIds; +import org.prebid.server.bidder.pangle.model.PangleBidExt; +import org.prebid.server.bidder.pangle.model.WrappedImpExtBidder; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.pangle.ExtImpPangle; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Pangle {@link Bidder} implementation. + */ +public class PangleBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public PangleBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final WrappedImpExtBidder extBidder = parseImpExt(imp); + final ExtImpPangle extImpPangle = extBidder.getBidder(); + final Integer adType = resolveAdType(imp, extBidder); + final Imp modifiedImp = modifyImp(imp, adType, extBidder, extImpPangle); + + requests.add(createRequest(request, modifiedImp, extBidder.getBidder().getToken())); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private WrappedImpExtBidder parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), WrappedImpExtBidder.class); + } catch (IllegalArgumentException e) { + throw new PreBidException(String.format("failed unmarshalling imp ext (err)%s", e.getMessage())); + } + } + + private static int resolveAdType(Imp imp, WrappedImpExtBidder extBidder) { + if (imp.getVideo() != null) { + final ExtImpPrebid extPrebid = extBidder != null ? extBidder.getPrebid() : null; + final Integer isRewardedInventory = extPrebid != null ? extPrebid.getIsRewardedInventory() : null; + if (Objects.equals(isRewardedInventory, 1)) { + return 7; + } + + if (Objects.equals(imp.getInstl(), 1)) { + return 8; + } + } + + if (imp.getBanner() != null) { + if (Objects.equals(imp.getInstl(), 1)) { + return 2; + } else { + return 1; + } + } + if (imp.getXNative() != null && StringUtils.isNotBlank(imp.getXNative().getRequest())) { + return 5; + } + + throw new PreBidException("not a supported adtype"); + } + + private Imp modifyImp(Imp imp, Integer adType, WrappedImpExtBidder extBidder, ExtImpPangle bidderImpExt) { + final NetworkIds modifiedNetworkIds = getNetworkIds(bidderImpExt); + + final WrappedImpExtBidder updatedImpExt = extBidder.toBuilder() + .adType(adType) + .isPrebid(true) + .networkids(modifiedNetworkIds == null ? extBidder.getNetworkids() : modifiedNetworkIds) + .build(); + + return imp.toBuilder() + .ext(mapper.mapper().convertValue(updatedImpExt, ObjectNode.class)) + .build(); + } + + private static NetworkIds getNetworkIds(ExtImpPangle bidderImpExt) { + if (bidderImpExt != null) { + final String appid = bidderImpExt.getAppid(); + final String placementid = bidderImpExt.getPlacementid(); + + if (StringUtils.isNotEmpty(appid) && StringUtils.isNotEmpty(placementid)) { + return NetworkIds.of(appid, placementid); + } else if (StringUtils.isNotEmpty(appid) || StringUtils.isNotEmpty(placementid)) { + throw new PreBidException("only one of appid or placementid is provided"); + } + } + + return null; + } + + private HttpRequest createRequest(BidRequest request, Imp imp, String token) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(makeHeaders(token)) + .payload(outgoingRequest) + .body(mapper.encode(outgoingRequest)) + .build(); + } + + private static MultiMap makeHeaders(String token) { + return HttpUtil.headers() + .add("TOKEN", token); + } + + @Override + public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .flatMap(Collection::stream) + .map(bid -> createBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private BidderBid createBid(Bid bid, String currency, List errors) { + final Integer adType; + try { + adType = getAdTypeFromBidExt(bid); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + final BidType bidType; + switch (adType) { + case 1: + case 2: + bidType = BidType.banner; + break; + case 5: + bidType = BidType.xNative; + break; + case 7: + case 8: + bidType = BidType.video; + break; + default: + errors.add(BidderError.badServerResponse("unrecognized adtype in response")); + return null; + } + + return BidderBid.of(bid, bidType, currency); + } + + private Integer getAdTypeFromBidExt(Bid bid) { + if (bid == null) { + throw new PreBidException("the bid request object is not present"); + } + final PangleBidExt bidExt; + try { + bidExt = mapper.mapper().convertValue(bid.getExt(), PangleBidExt.class); + } catch (IllegalArgumentException e) { + throw new PreBidException("invalid bid ext"); + } + + final BidExt pangleBidExt = bidExt != null ? bidExt.getPangle() : null; + final Integer adType = pangleBidExt != null ? pangleBidExt.getAdType() : null; + if (adType == null) { + throw new PreBidException("missing pangleExt/adtype in bid ext"); + } + return adType; + } +} diff --git a/src/main/java/org/prebid/server/bidder/pangle/model/BidExt.java b/src/main/java/org/prebid/server/bidder/pangle/model/BidExt.java new file mode 100644 index 00000000000..ded899b2091 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pangle/model/BidExt.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidder.pangle.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Builder(toBuilder = true) +@Value +public class BidExt { + + @JsonProperty("adtype") + Integer adType; +} diff --git a/src/main/java/org/prebid/server/bidder/pangle/model/NetworkIds.java b/src/main/java/org/prebid/server/bidder/pangle/model/NetworkIds.java new file mode 100644 index 00000000000..ae59d88b9f4 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pangle/model/NetworkIds.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.pangle.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class NetworkIds { + + String appid; + + String placementid; +} diff --git a/src/main/java/org/prebid/server/bidder/pangle/model/PangleBidExt.java b/src/main/java/org/prebid/server/bidder/pangle/model/PangleBidExt.java new file mode 100644 index 00000000000..c85dd19b087 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pangle/model/PangleBidExt.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.pangle.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Builder(toBuilder = true) +@Value +public class PangleBidExt { + + BidExt pangle; +} diff --git a/src/main/java/org/prebid/server/bidder/pangle/model/WrappedImpExtBidder.java b/src/main/java/org/prebid/server/bidder/pangle/model/WrappedImpExtBidder.java new file mode 100644 index 00000000000..10db06f4819 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pangle/model/WrappedImpExtBidder.java @@ -0,0 +1,25 @@ +package org.prebid.server.bidder.pangle.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.pangle.ExtImpPangle; + +@AllArgsConstructor(staticName = "of") +@Builder(toBuilder = true) +@Value +public class WrappedImpExtBidder { + + ExtImpPrebid prebid; + + ExtImpPangle bidder; + + @JsonProperty("adtype") + Integer adType; + + Boolean isPrebid; + + NetworkIds networkids; +} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticAdapter.java b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticAdapter.java deleted file mode 100644 index 38884ec643b..00000000000 --- a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticAdapter.java +++ /dev/null @@ -1,411 +0,0 @@ -package org.prebid.server.bidder.pubmatic; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Banner; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Publisher; -import com.iab.openrtb.request.Site; -import com.iab.openrtb.response.BidResponse; -import io.vertx.core.MultiMap; -import io.vertx.core.http.Cookie; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AdUnitBid; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.bidder.Adapter; -import org.prebid.server.bidder.OpenrtbAdapter; -import org.prebid.server.bidder.model.AdUnitBidWithParams; -import org.prebid.server.bidder.model.AdapterHttpRequest; -import org.prebid.server.bidder.model.ExchangeCall; -import org.prebid.server.bidder.pubmatic.model.NormalizedPubmaticParams; -import org.prebid.server.bidder.pubmatic.proto.PubmaticParams; -import org.prebid.server.bidder.pubmatic.proto.PubmaticRequestExt; -import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.request.PreBidRequest; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.MediaType; -import org.prebid.server.util.HttpUtil; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Pubmatic {@link Adapter} implementation. - */ -public class PubmaticAdapter extends OpenrtbAdapter { - - private static final Logger logger = LoggerFactory.getLogger(PubmaticAdapter.class); - - private static final Set ALLOWED_MEDIA_TYPES = - Collections.unmodifiableSet(EnumSet.of(MediaType.banner, MediaType.video)); - - private final String endpointUrl; - private final JacksonMapper mapper; - - public PubmaticAdapter(String cookieFamilyName, String endpointUrl, JacksonMapper mapper) { - super(cookieFamilyName); - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public List> makeHttpRequests(AdapterRequest adapterRequest, - PreBidRequestContext preBidRequestContext) { - final MultiMap headers = headers() - .add(HttpUtil.SET_COOKIE_HEADER, makeUserCookie(preBidRequestContext)); - - final BidRequest bidRequest = createBidRequest(adapterRequest, preBidRequestContext); - final AdapterHttpRequest httpRequest = AdapterHttpRequest.of(HttpMethod.POST, endpointUrl, - bidRequest, headers); - return Collections.singletonList(httpRequest); - } - - private BidRequest createBidRequest(AdapterRequest adapterRequest, PreBidRequestContext preBidRequestContext) { - final List adUnitBids = adapterRequest.getAdUnitBids(); - - validateAdUnitBidsMediaTypes(adUnitBids, ALLOWED_MEDIA_TYPES); - - final PreBidRequest preBidRequest = preBidRequestContext.getPreBidRequest(); - final List> adUnitBidsWithParams = - createAdUnitBidsWithParams(adUnitBids, preBidRequest.getTid()); - final List imps = makeImps(adUnitBidsWithParams, preBidRequestContext); - validateImps(imps); - - final Publisher publisher = makePublisher(preBidRequestContext, adUnitBidsWithParams); - - return BidRequest.builder() - .id(preBidRequest.getTid()) - .at(1) - .tmax(preBidRequest.getTimeoutMillis()) - .imp(imps) - .app(makeApp(preBidRequestContext, publisher)) - .site(makeSite(preBidRequestContext, publisher)) - .device(deviceBuilder(preBidRequestContext).build()) - .user(makeUser(preBidRequestContext)) - .source(makeSource(preBidRequestContext)) - .regs(preBidRequest.getRegs()) - .ext(makeRequestExt(adUnitBidsWithParams)) - .build(); - } - - // Parse Wrapper Extension i.e. ProfileID and VersionID only once per request - private ExtRequest makeRequestExt(List> adUnitBidsWithParams) { - final ObjectNode wrapExt = adUnitBidsWithParams.stream() - .map(AdUnitBidWithParams::getParams) - .filter(Objects::nonNull) - .map(NormalizedPubmaticParams::getWrapExt) - .filter(Objects::nonNull) - .findFirst().orElse(null); - - if (wrapExt != null) { - return mapper.fillExtension(ExtRequest.empty(), PubmaticRequestExt.of(wrapExt)); - } - return null; - } - - private List> createAdUnitBidsWithParams( - List adUnitBids, String requestId) { - final List errors = new ArrayList<>(); - final List> adUnitBidWithParams = adUnitBids.stream() - .map(adUnitBid -> AdUnitBidWithParams.of(adUnitBid, - parseAndValidateParams(adUnitBid, requestId, errors))) - .collect(Collectors.toList()); - - // at least one adUnitBid of banner type must be with valid params - if (adUnitBidWithParams.stream().noneMatch(PubmaticAdapter::isValidParams)) { - throw new PreBidException("Incorrect adSlot / Publisher param, " - + "Error list: [" + String.join(",", errors) + "]"); - } - - return adUnitBidWithParams; - } - - private static boolean isValidParams(AdUnitBidWithParams adUnitBidWithParams) { - final NormalizedPubmaticParams params = adUnitBidWithParams.getParams(); - if (params == null) { - return false; - } - - // if adUnitBid has banner type, params should contains tagId, width and height fields - return !adUnitBidWithParams.getAdUnitBid().getMediaTypes().contains(MediaType.banner) - || ObjectUtils.allNotNull(params.getTagId(), params.getWidth(), params.getHeight()); - } - - private NormalizedPubmaticParams parseAndValidateParams(AdUnitBid adUnitBid, - String requestId, List errors) { - final ObjectNode params = adUnitBid.getParams(); - final List sizes = adUnitBid.getSizes(); - final String bidId = adUnitBid.getBidId(); - if (params == null) { - errors.add(paramError(bidId, "Params section is missing", null)); - logWrongParams(requestId, null, adUnitBid, "Ignored bid: invalid JSON [%s] err [%s]", null, - "params section is missing"); - return null; - } - - final PubmaticParams pubmaticParams; - try { - pubmaticParams = mapper.mapper().convertValue(params, PubmaticParams.class); - } catch (IllegalArgumentException e) { - errors.add(paramError(bidId, "Invalid BidParam", params)); - logWrongParams(requestId, null, adUnitBid, "Ignored bid: invalid JSON [%s] err [%s]", params, e); - return null; - } - - final String publisherId = pubmaticParams.getPublisherId(); - if (StringUtils.isEmpty(publisherId)) { - errors.add(paramError(bidId, "Missing PubID", params)); - logWrongParams(requestId, publisherId, adUnitBid, "Ignored bid: Publisher Id missing"); - return null; - } - - final String adSlot = StringUtils.trimToNull(pubmaticParams.getAdSlot()); - Integer width = null; - Integer height = null; - String tagId = null; - boolean slotWithoutSize = true; - if (!StringUtils.isEmpty(adSlot)) { - if (!adSlot.contains("@")) { - tagId = adSlot; - } else { - final String[] adSlots = adSlot.split("@"); - if (adSlots.length != 2 || StringUtils.isEmpty(adSlots[0].trim()) - || StringUtils.isEmpty(adSlots[1].trim())) { - errors.add(paramError(bidId, "Invalid AdSlot", params)); - logWrongParams(requestId, publisherId, adUnitBid, "Ignored bid: invalid adSlot [%s]", adSlot); - return null; - } - tagId = adSlots[0].trim(); - final String[] adSizes = adSlots[1].toLowerCase().split("x"); - if (adSizes.length != 2) { - errors.add(paramError(bidId, "Invalid AdSize", params)); - logWrongParams(requestId, publisherId, adUnitBid, "Ignored bid: invalid adSize [%s]", - adSlots[1]); - return null; - } - - try { - width = Integer.parseInt(adSizes[0].trim()); - } catch (NumberFormatException e) { - errors.add(paramError(bidId, "Invalid Width", params)); - logWrongParams(requestId, publisherId, adUnitBid, "Ignored bid: invalid adSlot width [%s]", - adSizes[0]); - return null; - } - - final String[] adSizeHeights = adSizes[1].split(":"); - try { - height = Integer.parseInt(adSizeHeights[0].trim()); - } catch (NumberFormatException e) { - errors.add(paramError(bidId, "Invalid Height", params)); - logWrongParams(requestId, publisherId, adUnitBid, "Ignored bid: invalid adSlot height [%s]", - adSizes[0]); - return null; - } - slotWithoutSize = false; - } - } - if (slotWithoutSize) { - if (CollectionUtils.isEmpty(sizes)) { - errors.add(paramError(bidId, "Invalid AdSize", params)); - logWrongParams(requestId, publisherId, adUnitBid, "Ignored bid: Size missing"); - return null; - } - height = sizes.get(0).getH(); - width = sizes.get(0).getW(); - } - - final ObjectNode wrapExt; - if (pubmaticParams.getWrapper() != null) { - try { - mapper.mapper().convertValue(pubmaticParams.getWrapper(), new TypeReference>() { - }); - } catch (IllegalArgumentException e) { - errors.add(paramError(bidId, "Invalid WrapperExt", params)); - logWrongParams(requestId, pubmaticParams.getPublisherId(), adUnitBid, - "Ignored bid: Wrapper Extension Invalid"); - return null; - } - wrapExt = pubmaticParams.getWrapper(); - } else { - wrapExt = null; - } - - final ObjectNode keyValue; - final List keywords = makeKeywords(pubmaticParams.getKeywords()); - if (!keywords.isEmpty()) { - try { - keyValue = mapper.mapper().readValue("{" + String.join(",", keywords) + "}", ObjectNode.class); - } catch (IOException e) { - errors.add(String.format("Failed to create keywords with error: %s", e.getMessage())); - return null; - } - } else { - keyValue = null; - } - - return NormalizedPubmaticParams.of(publisherId, adSlot, tagId, width, height, wrapExt, keyValue); - } - - private static String paramError(String bidId, String message, ObjectNode params) { - return String.format("BidID:%s;Error:%s;param:%s", bidId, message, params); - } - - private static List makeKeywords(Map keywords) { - if (keywords == null) { - return Collections.emptyList(); - } - final List keywordPair = new ArrayList<>(); - for (Map.Entry entry : keywords.entrySet()) { - final String key = entry.getKey(); - if (StringUtils.isBlank(entry.getValue())) { - logger.warn(String.format("No values present for key = %s", key)); - } else { - keywordPair.add(String.format("\"%s\":\"%s\"", key, entry.getValue())); - } - } - return keywordPair; - } - - private static void logWrongParams(String requestId, String publisherId, AdUnitBid adUnitBid, String errorMessage, - Object... args) { - logger.warn("[PUBMATIC] ReqID [{0}] PubID [{1}] AdUnit [{2}] BidID [{3}] {4}", requestId, publisherId, - adUnitBid.getAdUnitCode(), adUnitBid.getBidId(), String.format(errorMessage, args)); - } - - private static List makeImps(List> adUnitBidsWithParams, - PreBidRequestContext preBidRequestContext) { - return adUnitBidsWithParams.stream() - .filter(PubmaticAdapter::containsAnyAllowedMediaType) - .map(adUnitBidWithParams -> makeImp(adUnitBidWithParams, preBidRequestContext)) - .collect(Collectors.toList()); - } - - private static boolean containsAnyAllowedMediaType( - AdUnitBidWithParams adUnitBidWithParams) { - return CollectionUtils.containsAny(adUnitBidWithParams.getAdUnitBid().getMediaTypes(), ALLOWED_MEDIA_TYPES); - } - - private static Imp makeImp(AdUnitBidWithParams adUnitBidWithParams, - PreBidRequestContext preBidRequestContext) { - final AdUnitBid adUnitBid = adUnitBidWithParams.getAdUnitBid(); - final NormalizedPubmaticParams params = adUnitBidWithParams.getParams(); - - final Set mediaTypes = allowedMediaTypes(adUnitBid, ALLOWED_MEDIA_TYPES); - final Imp.ImpBuilder impBuilder = Imp.builder() - .id(adUnitBid.getAdUnitCode()) - .instl(adUnitBid.getInstl()) - .secure(preBidRequestContext.getSecure()) - .tagid(mediaTypes.contains(MediaType.banner) && params != null ? params.getTagId() : null) - .ext(params != null ? params.getKeywords() : null); - if (mediaTypes.contains(MediaType.banner)) { - impBuilder.banner(makeBanner(adUnitBid, params)); - } - if (mediaTypes.contains(MediaType.video)) { - impBuilder.video(videoBuilder(adUnitBid).build()); - } - return impBuilder.build(); - } - - private static Banner makeBanner(AdUnitBid adUnitBid, NormalizedPubmaticParams params) { - final List sizes = adUnitBid.getSizes(); - final Format format = sizes.get(0); - return Banner.builder() - .w(params != null ? params.getWidth() : format.getW()) - .h(params != null ? params.getHeight() : format.getH()) - .format(CollectionUtils.isNotEmpty(sizes) ? sizes : null) // pubmatic now supports format object - .topframe(adUnitBid.getTopframe()) - .build(); - } - - private static Publisher makePublisher(PreBidRequestContext preBidRequestContext, - List> adUnitBidsWithParams) { - return adUnitBidsWithParams.stream() - .map(AdUnitBidWithParams::getParams) - .filter(params -> params != null && params.getPublisherId() != null && params.getAdSlot() != null) - .map(NormalizedPubmaticParams::getPublisherId) - .reduce((first, second) -> second) - .map(publisherId -> Publisher.builder() - .id(publisherId) - .domain(preBidRequestContext.getDomain()) - .build()) - .orElse(null); - } - - private static App makeApp(PreBidRequestContext preBidRequestContext, Publisher publisher) { - final App app = preBidRequestContext.getPreBidRequest().getApp(); - return app == null ? null : app.toBuilder() - .publisher(publisher) - .build(); - } - - private static Site makeSite(PreBidRequestContext preBidRequestContext, Publisher publisher) { - final Site.SiteBuilder siteBuilder = siteBuilder(preBidRequestContext); - return siteBuilder == null ? null : siteBuilder - .publisher(publisher) - .build(); - } - - private String makeUserCookie(PreBidRequestContext preBidRequestContext) { - final UidsCookie uidsCookie = preBidRequestContext.getUidsCookie(); - final String cookieValue = uidsCookie != null ? uidsCookie.uidFrom(cookieFamilyName) : null; - - return Cookie.cookie("KADUSERCOOKIE", ObjectUtils.defaultIfNull(cookieValue, "")).encode(); - } - - @Override - public List extractBids(AdapterRequest adapterRequest, - ExchangeCall exchangeCall) { - return responseBidStream(exchangeCall.getResponse()) - .map(bid -> toBidBuilder(bid, adapterRequest, exchangeCall.getRequest().getImp())) - .collect(Collectors.toList()); - } - - private static Bid.BidBuilder toBidBuilder(com.iab.openrtb.response.Bid bid, AdapterRequest adapterRequest, - List imps) { - final String impId = bid.getImpid(); - final AdUnitBid adUnitBid = lookupBid(adapterRequest.getAdUnitBids(), impId); - return Bid.builder() - .bidder(adUnitBid.getBidderCode()) - .bidId(adUnitBid.getBidId()) - .code(bid.getImpid()) - .price(bid.getPrice()) - .adm(bid.getAdm()) - .creativeId(bid.getCrid()) - .mediaType(mediaTypeFor(imps, impId)) - .width(bid.getW()) - .height(bid.getH()) - .dealId(bid.getDealid()); - } - - private static MediaType mediaTypeFor(List imps, String impId) { - return imps.stream() - .filter(imp -> Objects.equals(imp.getId(), impId)) - .findAny() - .map(PubmaticAdapter::mediaTypeFromImp) - .orElse(MediaType.banner); - } - - private static MediaType mediaTypeFromImp(Imp imp) { - return imp.getVideo() != null ? MediaType.video : MediaType.banner; - } -} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java index 3f062228695..955c334007b 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java @@ -2,10 +2,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Site; @@ -13,9 +15,8 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -23,22 +24,21 @@ import org.prebid.server.bidder.model.HttpCall; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; -import org.prebid.server.bidder.pubmatic.proto.PubmaticBidExt; -import org.prebid.server.bidder.pubmatic.proto.PubmaticRequestExt; -import org.prebid.server.bidder.pubmatic.proto.VideoCreativeInfo; +import org.prebid.server.bidder.pubmatic.model.request.PubmaticBidderImpExt; +import org.prebid.server.bidder.pubmatic.model.request.PubmaticExtData; +import org.prebid.server.bidder.pubmatic.model.request.PubmaticExtDataAdServer; +import org.prebid.server.bidder.pubmatic.model.response.PubmaticBidExt; +import org.prebid.server.bidder.pubmatic.model.response.VideoCreativeInfo; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic; -import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmaticKeyVal; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.util.HttpUtil; -import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -47,16 +47,19 @@ import java.util.Objects; import java.util.stream.Collectors; +/** + * Pubmatic {@link Bidder} implementation. + */ public class PubmaticBidder implements Bidder { - private static final Logger logger = LoggerFactory.getLogger(PubmaticBidder.class); - - private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final String DCTR_KEY_NAME = "key_val"; + private static final String PM_ZONE_ID_KEY_NAME = "pmZoneId"; + private static final String PM_ZONE_ID_OLD_KEY_NAME = "pmZoneID"; + private static final String IMP_EXT_AD_UNIT_KEY = "dfp_ad_unit_code"; + private static final String AD_SERVER_GAM = "gam"; private static final String PREBID = "prebid"; - private static final TypeReference> PUBMATIC_EXT_TYPE_REFERENCE = - new TypeReference>() { - }; - private static final TypeReference> WRAPPER_VALIDATION = + + private static final TypeReference> WRAPPER_TYPE = new TypeReference>() { }; @@ -64,152 +67,202 @@ public class PubmaticBidder implements Bidder { private final JacksonMapper mapper; public PubmaticBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(endpointUrl); + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @Override - public final Result>> makeHttpRequests(BidRequest bidRequest) { + public Result>> makeHttpRequests(BidRequest request) { final List errors = new ArrayList<>(); - final List modifiedImps = new ArrayList<>(); - final List extImpPubmatics = new ArrayList<>(); - for (Imp imp : bidRequest.getImp()) { + final List validImps = new ArrayList<>(); + + ObjectNode wrapper = null; + String pubId = null; + + for (Imp imp : request.getImp()) { try { - final ExtImpPubmatic extImpPubmatic = parseImpExt(imp); - final Imp modifiedImp = modifyImp(imp, extImpPubmatic); - modifiedImps.add(modifiedImp); - extImpPubmatics.add(extImpPubmatic); + if (imp.getBanner() == null && imp.getVideo() == null) { + throw new PreBidException(String.format("Invalid MediaType. PubMatic only supports " + + "Banner and Video. Ignoring ImpID=%s", imp.getId())); + } + final PubmaticBidderImpExt impExt = parseImpExt(imp); + final ExtImpPubmatic extBidder = impExt.getBidder(); + if (pubId == null) { + pubId = StringUtils.trimToNull(extBidder.getPublisherId()); + } + final ObjectNode extWrapper = impExt.getBidder().getWrapper(); + if (wrapper == null && extWrapper != null && !extWrapper.isEmpty()) { + validateExtWrapper(extWrapper, imp.getId()); + wrapper = extWrapper; + } + + validImps.add(modifyImp(imp, impExt)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } } - if (modifiedImps.isEmpty()) { - return Result.of(Collections.emptyList(), errors); + if (validImps.isEmpty()) { + return Result.withErrors(errors); } - return Result.of(Collections.singletonList(makeRequest(bidRequest, modifiedImps, extImpPubmatics)), errors); + return Result.of(Collections.singletonList(createRequest(request, validImps, pubId, wrapper)), errors); } - private ExtImpPubmatic parseImpExt(Imp imp) { + private PubmaticBidderImpExt parseImpExt(Imp imp) { try { - return mapper.mapper().convertValue(imp.getExt(), PUBMATIC_EXT_TYPE_REFERENCE) - .getBidder(); + return mapper.mapper().convertValue(imp.getExt(), PubmaticBidderImpExt.class); } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException(e.getMessage()); } } - private Imp modifyImp(Imp imp, ExtImpPubmatic extImpPubmatic) throws PreBidException { - // validate Impression - if (imp.getBanner() == null && imp.getVideo() == null) { - throw new PreBidException(String.format("Invalid MediaType. PubMatic only supports Banner and Video. " - + "Ignoring ImpID=%s", imp.getId())); + private Imp modifyImp(Imp imp, PubmaticBidderImpExt impExt) { + final Imp.ImpBuilder impBuilder = imp.toBuilder().audio(null); + + final Banner banner = imp.getBanner(); + + if (banner != null) { + impBuilder.banner(assignSizesIfMissing(banner)); } - // impression extension validation - final ObjectNode wrapExt = extImpPubmatic.getWrapper(); - if (wrapExt != null) { - try { - mapper.mapper().convertValue(wrapExt, WRAPPER_VALIDATION); - } catch (IllegalArgumentException e) { - throw new PreBidException( - String.format("Error in Wrapper Parameters = %s for ImpID = %s WrapperExt = %s", - e.getMessage(), imp.getId(), wrapExt.toString())); - } + enrichImpBuilderWithAdSlotParameters(impBuilder, impExt.getBidder().getAdSlot(), banner); + + final ObjectNode keywordsNode = makeKeywords(impExt); + if (!keywordsNode.isEmpty()) { + impBuilder.ext(keywordsNode); + } else { + impBuilder.ext(null); } - // impression changes and additional validation - final Imp.ImpBuilder modifiedImp = imp.toBuilder(); - if (imp.getAudio() != null) { - modifiedImp.audio(null); + return impBuilder.build(); + } + + private void validateExtWrapper(ObjectNode wrapExt, String impId) { + try { + mapper.mapper().convertValue(wrapExt, WRAPPER_TYPE); + } catch (IllegalArgumentException e) { + throw new PreBidException( + String.format("Error in Wrapper Parameters = %s for ImpID = %s WrapperExt = %s", + e.getMessage(), impId, wrapExt.toString())); } - final Banner banner = imp.getBanner(); - if (banner != null) { - final String adSlotString = StringUtils.trimToNull(extImpPubmatic.getAdSlot()); - Integer width = null; - Integer height = null; - if (!StringUtils.isEmpty(adSlotString)) { - if (!adSlotString.contains("@")) { - modifiedImp.tagid(adSlotString); - } else { - final String[] adSlot = adSlotString.split("@"); - if (adSlot.length != 2 || StringUtils.isEmpty(adSlot[0].trim()) - || StringUtils.isEmpty(adSlot[1].trim())) { - throw new PreBidException("Invalid adSlot provided"); - } - modifiedImp.tagid(adSlot[0].trim()); - final String[] adSize = adSlot[1].toLowerCase().split("x"); - if (adSize.length != 2) { - throw new PreBidException("Invalid size provided in adSlot"); - } - final String[] heightStr = adSize[1].split(":"); - try { - width = Integer.valueOf(adSize[0].trim()); - height = Integer.valueOf(heightStr[0].trim()); - } catch (NumberFormatException e) { - throw new PreBidException("Invalid size provided in adSlot"); - } - } - } - if (width == null && height == null) { - final boolean isFormatsPresent = CollectionUtils.isNotEmpty(banner.getFormat()); - width = isFormatsPresent && banner.getW() == null && banner.getH() == null - ? banner.getFormat().get(0).getW() : banner.getW(); + } - height = isFormatsPresent && banner.getH() == null && banner.getW() == null - ? banner.getFormat().get(0).getH() : banner.getH(); - } - final Banner updatedBanner = banner.toBuilder().w(width).h(height).build(); - modifiedImp.banner(updatedBanner); + private void enrichImpBuilderWithAdSlotParameters(Imp.ImpBuilder impBuilder, String adSlot, Banner banner) { + final String trimmedAdSlot = StringUtils.trimToNull(adSlot); + + if (StringUtils.isEmpty(trimmedAdSlot)) { + return; } - if (CollectionUtils.isNotEmpty(extImpPubmatic.getKeywords())) { - modifiedImp.ext(makeKeywords(extImpPubmatic.getKeywords())); - } else { - modifiedImp.ext(null); + if (!trimmedAdSlot.contains("@")) { + impBuilder.tagid(trimmedAdSlot); + return; + } + + final String[] adSlotParams = trimmedAdSlot.split("@"); + if (adSlotParams.length != 2 + || StringUtils.isEmpty(adSlotParams[0].trim()) + || StringUtils.isEmpty(adSlotParams[1].trim())) { + throw new PreBidException(String.format("Invalid adSlot '%s'", trimmedAdSlot)); + } + impBuilder.tagid(adSlotParams[0]); + final String[] adSize = adSlotParams[1].toLowerCase().split("x"); + if (adSize.length != 2) { + throw new PreBidException(String.format("Invalid size provided in adSlot '%s'", trimmedAdSlot)); + } + + final Integer width = parseAdSizeParam(adSize[0], "width", adSlot); + final String[] heightParams = adSize[1].split(":"); + final Integer height = parseAdSizeParam(heightParams[0], "height", adSlot); + + impBuilder.banner(modifyWithSizeParams(banner, width, height)); + } + + private static Integer parseAdSizeParam(String number, String paramName, String adSlot) { + try { + return Integer.parseInt(number.trim()); + } catch (NumberFormatException e) { + throw new PreBidException(String.format("Invalid %s provided in adSlot '%s'", paramName, adSlot)); } - return modifiedImp.build(); } - private ObjectNode makeKeywords(List keywords) { - final List eachKv = new ArrayList<>(); - for (ExtImpPubmaticKeyVal keyVal : keywords) { - if (CollectionUtils.isEmpty(keyVal.getValue())) { - logger.error(String.format("No values present for key = %s", keyVal.getKey())); - } else { - eachKv.add(String.format("\"%s\":\"%s\"", keyVal.getKey(), - String.join(",", keyVal.getValue()))); + private static Banner modifyWithSizeParams(Banner banner, Integer width, Integer height) { + return banner != null + ? banner.toBuilder().w(width).h(height).build() + : null; + } + + private static Banner assignSizesIfMissing(Banner banner) { + final List format = banner.getFormat(); + if ((banner.getW() != null && banner.getH() != null) || CollectionUtils.isEmpty(format)) { + return banner; + } + + final Format firstFormat = format.get(0); + + return modifyWithSizeParams(banner, firstFormat.getW(), firstFormat.getH()); + } + + private ObjectNode makeKeywords(PubmaticBidderImpExt impExt) { + final ObjectNode keywordsNode = mapper.mapper().createObjectNode(); + putExtBidderKeywords(keywordsNode, impExt.getBidder()); + + final PubmaticExtData pubmaticExtData = impExt.getData(); + if (pubmaticExtData != null) { + putExtDataKeywords(keywordsNode, pubmaticExtData); + } + + return keywordsNode; + } + + private static void putExtBidderKeywords(ObjectNode keywords, ExtImpPubmatic extBidder) { + CollectionUtils.emptyIfNull(extBidder.getKeywords()).forEach(keyword -> { + if (CollectionUtils.isEmpty(keyword.getValue())) { + return; } + keywords.put(keyword.getKey(), String.join(",", keyword.getValue())); + }); + final JsonNode pmZoneIdKeyWords = keywords.remove(PM_ZONE_ID_OLD_KEY_NAME); + final String pmZomeId = extBidder.getPmZoneId(); + if (StringUtils.isNotEmpty(pmZomeId)) { + keywords.put(PM_ZONE_ID_KEY_NAME, extBidder.getPmZoneId()); + } else if (pmZoneIdKeyWords != null) { + keywords.set(PM_ZONE_ID_KEY_NAME, pmZoneIdKeyWords); } - final String keywordsString = "{" + String.join(",", eachKv) + "}"; - try { - return mapper.mapper().readValue(keywordsString, ObjectNode.class); - } catch (IOException e) { - throw new PreBidException(String.format("Failed to create keywords with error: %s", e.getMessage()), e); + + final String dctr = extBidder.getDctr(); + if (StringUtils.isNotEmpty(dctr)) { + keywords.put(DCTR_KEY_NAME, dctr); } } - private HttpRequest makeRequest(BidRequest bidRequest, List imps, - List extImpPubmatics) { - final BidRequest.BidRequestBuilder requestBuilder = bidRequest.toBuilder().imp(imps); + private static void putExtDataKeywords(ObjectNode keywords, PubmaticExtData extData) { + final String pbaAdSlot = extData.getPbAdSlot(); + final PubmaticExtDataAdServer extAdServer = extData.getAdServer(); + final String adSeverName = extAdServer != null ? extAdServer.getName() : null; + final String adSeverAdSlot = extAdServer != null ? extAdServer.getAdSlot() : null; + if (AD_SERVER_GAM.equals(adSeverName) && StringUtils.isNotEmpty(adSeverAdSlot)) { + keywords.put(IMP_EXT_AD_UNIT_KEY, adSeverAdSlot); + } else if (StringUtils.isNotEmpty(pbaAdSlot)) { + keywords.put(IMP_EXT_AD_UNIT_KEY, pbaAdSlot); + } + } - extImpPubmatics.stream() - .map(ExtImpPubmatic::getWrapper) - .filter(Objects::nonNull) - .findFirst() - .ifPresent(wrapExt -> requestBuilder.ext( - mapper.fillExtension(ExtRequest.empty(), PubmaticRequestExt.of(wrapExt)))); + private HttpRequest createRequest(BidRequest request, + List imps, + String pubId, + ObjectNode wrapper) { + final BidRequest.BidRequestBuilder requestBuilder = request.toBuilder().imp(imps); - final String pubId = extImpPubmatics.stream() - .map(ExtImpPubmatic::getPublisherId) - .filter(Objects::nonNull) - .findFirst().orElse(null); + if (wrapper != null) { + requestBuilder.ext( + mapper.fillExtension(ExtRequest.empty(), + mapper.mapper().createObjectNode().set("wrapper", wrapper))); + } - if (bidRequest.getSite() != null) { - modifySite(pubId, bidRequest, requestBuilder); - } else if (bidRequest.getApp() != null) { - modifyApp(pubId, bidRequest, requestBuilder); + if (pubId != null) { + updateRequestWithPubIdParam(requestBuilder, request.getSite(), request.getApp(), pubId); } final BidRequest modifiedRequest = requestBuilder.build(); @@ -224,29 +277,32 @@ private HttpRequest makeRequest(BidRequest bidRequest, List imp .build(); } - private static void modifySite(String pubId, BidRequest bidRequest, - BidRequest.BidRequestBuilder bidRequestBuilder) { - final Site site = bidRequest.getSite(); + private static void updateRequestWithPubIdParam(BidRequest.BidRequestBuilder requestBuilder, + Site site, + App app, + String pubId) { + if (site != null) { + modifySite(pubId, site, requestBuilder); + } else if (app != null) { + modifyApp(pubId, app, requestBuilder); + } + } + + private static void modifySite(String pubId, Site site, BidRequest.BidRequestBuilder bidRequestBuilder) { if (site.getPublisher() != null) { final Publisher modifiedPublisher = site.getPublisher().toBuilder().id(pubId).build(); bidRequestBuilder.site(site.toBuilder().publisher(modifiedPublisher).build()); } else { - bidRequestBuilder.site(site.toBuilder() - .publisher(Publisher.builder().id(pubId).build()) - .build()); + bidRequestBuilder.site(site.toBuilder().publisher(Publisher.builder().id(pubId).build()).build()); } } - private static void modifyApp(String pubId, BidRequest bidRequest, - BidRequest.BidRequestBuilder bidRequestBuilder) { - final App app = bidRequest.getApp(); + private static void modifyApp(String pubId, App app, BidRequest.BidRequestBuilder bidRequestBuilder) { if (app.getPublisher() != null) { final Publisher modifiedPublisher = app.getPublisher().toBuilder().id(pubId).build(); bidRequestBuilder.app(app.toBuilder().publisher(modifiedPublisher).build()); } else { - bidRequestBuilder.app(app.toBuilder() - .publisher(Publisher.builder().id(pubId).build()) - .build()); + bidRequestBuilder.app(app.toBuilder().publisher(Publisher.builder().id(pubId).build()).build()); } } @@ -256,12 +312,12 @@ public final Result> makeBids(HttpCall httpCall, Bid final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); return Result.of(extractBids(bidResponse), Collections.emptyList()); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private List extractBids(BidResponse bidResponse) { - return bidResponse == null || bidResponse.getSeatbid() == null + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() : bidsFromResponse(bidResponse); } @@ -272,65 +328,58 @@ private List bidsFromResponse(BidResponse bidResponse) { .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(this::bidderBid) + .map(bid -> resolveBidderBid(bid, bidResponse.getCur())) .collect(Collectors.toList()); } - private BidderBid bidderBid(Bid bid) { - final List bidCat = bid.getCat(); - final boolean updateBidCat = bidCat != null && bidCat.size() > 1; - final List singleElementCat = updateBidCat - ? Collections.singletonList(bidCat.get(0)) - : bidCat; + private BidderBid resolveBidderBid(Bid bid, String currency) { + final List singleElementBidCat = CollectionUtils.emptyIfNull(bid.getCat()).stream() + .limit(1) + .collect(Collectors.collectingAndThen(Collectors.toList(), + bidCat -> !bidCat.isEmpty() ? bidCat : null)); + final PubmaticBidExt pubmaticBidExt = extractBidExt(bid.getExt()); final Integer duration = getDuration(pubmaticBidExt); - final Bid updatedBid = updateBidCat || duration != null + final Bid updatedBid = singleElementBidCat != null || duration != null ? bid.toBuilder() - .cat(singleElementCat) + .cat(singleElementBidCat) .ext(duration != null ? updateBidExtWithExtPrebid(duration, bid.getExt()) : bid.getExt()) .build() : bid; - return BidderBid.of(updatedBid, getBidType(pubmaticBidExt), DEFAULT_BID_CURRENCY); + return BidderBid.of(updatedBid, getBidType(pubmaticBidExt), currency); } private PubmaticBidExt extractBidExt(ObjectNode bidExt) { try { return bidExt != null ? mapper.mapper().treeToValue(bidExt, PubmaticBidExt.class) : null; } catch (JsonProcessingException e) { - throw new PreBidException(String.format("Error parsing pubmatic bid.ext %s", e.getMessage())); + return null; } } - private static BidType getBidType(PubmaticBidExt pubmaticBidExt) { - if (pubmaticBidExt != null) { - final Integer bidTypeVal = pubmaticBidExt.getBidType(); - if (bidTypeVal != null) { - switch (bidTypeVal) { - case 1: - return BidType.video; - case 2: - return BidType.xNative; - default: - return BidType.banner; - } - } + private static BidType getBidType(PubmaticBidExt bidExt) { + final Integer bidType = bidExt != null + ? ObjectUtils.defaultIfNull(bidExt.getBidType(), 0) + : 0; + + switch (bidType) { + case 1: + return BidType.video; + case 2: + return BidType.xNative; + default: + return BidType.banner; } - return BidType.banner; } - private static Integer getDuration(PubmaticBidExt pubmaticBidExt) { - final VideoCreativeInfo video = pubmaticBidExt != null ? pubmaticBidExt.getVideo() : null; - return video != null ? video.getDuration() : null; + private static Integer getDuration(PubmaticBidExt bidExt) { + final VideoCreativeInfo creativeInfo = bidExt != null ? bidExt.getVideo() : null; + return creativeInfo != null ? creativeInfo.getDuration() : null; } private ObjectNode updateBidExtWithExtPrebid(Integer duration, ObjectNode extBid) { final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().video(ExtBidPrebidVideo.of(duration, null)).build(); return extBid.set(PREBID, mapper.mapper().valueToTree(extBidPrebid)); } - - @Override - public final Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/NormalizedPubmaticParams.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/NormalizedPubmaticParams.java deleted file mode 100644 index 36f212c2c73..00000000000 --- a/src/main/java/org/prebid/server/bidder/pubmatic/model/NormalizedPubmaticParams.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.bidder.pubmatic.model; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class NormalizedPubmaticParams { - - String publisherId; - - String adSlot; - - String tagId; - - Integer width; - - Integer height; - - ObjectNode wrapExt; - - ObjectNode keywords; -} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java new file mode 100644 index 00000000000..ffb67603170 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticBidderImpExt.java @@ -0,0 +1,14 @@ +package org.prebid.server.bidder.pubmatic.model.request; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic; + +@AllArgsConstructor(staticName = "of") +@Value +public class PubmaticBidderImpExt { + + ExtImpPubmatic bidder; + + PubmaticExtData data; +} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticExtData.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticExtData.java new file mode 100644 index 00000000000..3e20baa13e4 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticExtData.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.pubmatic.model.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class PubmaticExtData { + + @JsonProperty("pbadslot") + String pbAdSlot; + + @JsonProperty("adserver") + PubmaticExtDataAdServer adServer; +} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticExtDataAdServer.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticExtDataAdServer.java new file mode 100644 index 00000000000..b112cbe9e95 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticExtDataAdServer.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidder.pubmatic.model.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class PubmaticExtDataAdServer { + + String name; + + @JsonProperty("adslot") + String adSlot; +} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java new file mode 100644 index 00000000000..ba21327958b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java @@ -0,0 +1,17 @@ +package org.prebid.server.bidder.pubmatic.model.response; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class PubmaticBidExt { + + @JsonProperty("BidType") + @JsonAlias({"bidtype", "bidType"}) + Integer bidType; + + VideoCreativeInfo video; +} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/response/VideoCreativeInfo.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/response/VideoCreativeInfo.java new file mode 100644 index 00000000000..7b2254200e9 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/response/VideoCreativeInfo.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.pubmatic.model.response; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class VideoCreativeInfo { + + Integer duration; +} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/proto/PubmaticBidExt.java b/src/main/java/org/prebid/server/bidder/pubmatic/proto/PubmaticBidExt.java deleted file mode 100644 index 0f1783e3038..00000000000 --- a/src/main/java/org/prebid/server/bidder/pubmatic/proto/PubmaticBidExt.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.bidder.pubmatic.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -@Value -@AllArgsConstructor(staticName = "of") -public class PubmaticBidExt { - - @JsonProperty("BidType") - Integer bidType; - - VideoCreativeInfo video; -} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/proto/PubmaticParams.java b/src/main/java/org/prebid/server/bidder/pubmatic/proto/PubmaticParams.java deleted file mode 100644 index e2e5d570bc9..00000000000 --- a/src/main/java/org/prebid/server/bidder/pubmatic/proto/PubmaticParams.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.bidder.pubmatic.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.Map; - -@AllArgsConstructor(staticName = "of") -@Value -public class PubmaticParams { - - @JsonProperty("publisherId") - String publisherId; - - @JsonProperty("adSlot") - String adSlot; - - ObjectNode wrapper; - - Map keywords; -} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/proto/PubmaticRequestExt.java b/src/main/java/org/prebid/server/bidder/pubmatic/proto/PubmaticRequestExt.java deleted file mode 100644 index c787e2d1052..00000000000 --- a/src/main/java/org/prebid/server/bidder/pubmatic/proto/PubmaticRequestExt.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.prebid.server.bidder.pubmatic.proto; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class PubmaticRequestExt { - - ObjectNode wrapper; -} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/proto/VideoCreativeInfo.java b/src/main/java/org/prebid/server/bidder/pubmatic/proto/VideoCreativeInfo.java deleted file mode 100644 index e7c8ef93b43..00000000000 --- a/src/main/java/org/prebid/server/bidder/pubmatic/proto/VideoCreativeInfo.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.pubmatic.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@Value -@AllArgsConstructor(staticName = "of") -public class VideoCreativeInfo { - - Integer duration; -} diff --git a/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java b/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java index ca5a72cd927..b1c2420120c 100644 --- a/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java +++ b/src/main/java/org/prebid/server/bidder/pubnative/PubnativeBidder.java @@ -7,19 +7,20 @@ import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -28,11 +29,11 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -41,21 +42,25 @@ public class PubnativeBidder implements Bidder { private static final TypeReference> PUBNATIVE_EXT_TYPE_REFERENCE = new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final String PUBNATIVE_CURRENCY = "USD"; private final String endpointUrl; private final JacksonMapper mapper; + private final CurrencyConversionService currencyConversionService; - public PubnativeBidder(String endpointUrl, JacksonMapper mapper) { + public PubnativeBidder(String endpointUrl, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); } @Override public Result>> makeHttpRequests(BidRequest bidRequest) { final Device device = bidRequest.getDevice(); if (device == null || StringUtils.isBlank(device.getOs())) { - return Result.emptyWithError(BidderError.badInput("Impression is missing device OS information")); + return Result.withError(BidderError.badInput("Impression is missing device OS information")); } final List> httpRequests = new ArrayList<>(); @@ -64,7 +69,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ try { validateImp(imp); final ExtImpPubnative extImpPubnative = parseImpExt(imp.getExt()); - final BidRequest outgoingRequest = modifyRequest(bidRequest, imp); + final BidRequest outgoingRequest = createRequest(bidRequest, imp); httpRequests.add(createHttpRequest(outgoingRequest, extImpPubnative)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); @@ -88,30 +93,63 @@ private ExtImpPubnative parseImpExt(ObjectNode impExt) { } } - private static BidRequest modifyRequest(BidRequest bidRequest, Imp imp) { - Imp outgoingImp = imp; - final Banner banner = imp.getBanner(); + private BidRequest createRequest(BidRequest bidRequest, Imp imp) { + return bidRequest.toBuilder() + .cur(Collections.singletonList(PUBNATIVE_CURRENCY)) + .imp(Collections.singletonList(resolveImp(bidRequest, imp))) + .build(); + } + + private Imp resolveImp(BidRequest bidRequest, Imp imp) { + final Banner resolvedBanner = resolveBanner(imp.getBanner()); + final BigDecimal resolvedBidFloor = resolveBidFloor(bidRequest, imp); + return resolvedBanner == null && resolvedBidFloor == null + ? imp + : imp.toBuilder() + .banner(ObjectUtils.defaultIfNull(resolvedBanner, imp.getBanner())) + .bidfloor(ObjectUtils.defaultIfNull(resolvedBidFloor, imp.getBidfloor())) + .bidfloorcur(resolvedBidFloor == null ? imp.getBidfloorcur() : PUBNATIVE_CURRENCY) + .build(); + } + + private static Banner resolveBanner(Banner banner) { if (banner != null) { - final Integer bannerHeight = banner.getH(); - final Integer bannerWidth = banner.getW(); - if (bannerWidth == null || bannerWidth == 0 || bannerHeight == null || bannerHeight == 0) { - final List bannerFormats = banner.getFormat(); - if (CollectionUtils.isEmpty(bannerFormats)) { + final Integer width = banner.getW(); + final Integer height = banner.getH(); + if (width == null || width == 0 || height == null || height == 0) { + final List formats = banner.getFormat(); + if (CollectionUtils.isEmpty(formats)) { throw new PreBidException("Size information missing for banner"); } - final Format firstFormat = bannerFormats.get(0); - final Banner modifiedBanner = banner.toBuilder() - .h(firstFormat.getH()) + final Format firstFormat = formats.get(0); + return banner.toBuilder() .w(firstFormat.getW()) + .h(firstFormat.getH()) .build(); - outgoingImp = imp.toBuilder().banner(modifiedBanner).build(); } } + return null; + } - return bidRequest.toBuilder() - .imp(Collections.singletonList(outgoingImp)) - .build(); + private BigDecimal resolveBidFloor(BidRequest bidRequest, Imp imp) { + final BigDecimal bidFloor = imp.getBidfloor(); + final String bidFloorCur = resolveBidFloorCurrency(bidRequest, imp.getBidfloorcur()); + if (bidFloor == null || bidFloor.compareTo(BigDecimal.ZERO) <= 0 + || StringUtils.equals(bidFloorCur, PUBNATIVE_CURRENCY) + || StringUtils.isEmpty(bidFloorCur)) { + return null; + } + + return currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCur, PUBNATIVE_CURRENCY); + } + + private static String resolveBidFloorCurrency(BidRequest bidRequest, String bidFloorCurrency) { + if (StringUtils.isNotEmpty(bidFloorCurrency)) { + return bidFloorCurrency; + } + final List bidRequestCurrencies = bidRequest.getCur(); + return CollectionUtils.isNotEmpty(bidRequestCurrencies) ? bidRequestCurrencies.get(0) : null; } private HttpRequest createHttpRequest(BidRequest outgoingRequest, ExtImpPubnative impExt) { @@ -129,51 +167,88 @@ private HttpRequest createHttpRequest(BidRequest outgoingRequest, Ex @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final HttpResponse httpResponse = httpCall.getResponse(); - if (httpResponse.getStatusCode() == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - try { - final BidResponse bidResponse = mapper.decodeValue(httpResponse.getBody(), BidResponse.class); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); return Result.of(extractBids(bidResponse, httpCall.getRequest().getPayload()), Collections.emptyList()); } catch (DecodeException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private static List extractBids(BidResponse bidResponse, BidRequest bidRequest) { - return bidResponse == null || bidResponse.getSeatbid() == null + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() - : bidsFromResponse(bidResponse.getSeatbid(), bidRequest.getImp()); + : bidsFromResponse(bidResponse.getSeatbid(), bidRequest.getImp(), bidResponse.getCur()); } - private static List bidsFromResponse(List seatbid, List imps) { + private static List bidsFromResponse(List seatbid, List imps, String currency) { return seatbid.stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), imps), DEFAULT_BID_CURRENCY)) + .map(bid -> createBidderBid(imps, currency, bid)) .collect(Collectors.toList()); } - private static BidType resolveBidType(String impid, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impid)) { - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getXNative() != null) { - return BidType.xNative; - } - } + private static BidderBid createBidderBid(List imps, String currency, Bid bid) { + final Imp imp = findImpById(bid.getImpid(), imps); + return BidderBid.of(updateBidWithSize(bid, imp), resolveBidType(imp), currency); + } + + private static Bid updateBidWithSize(Bid bid, Imp imp) { + if (bid.getW() != null && bid.getH() != null) { + return bid; } - return BidType.banner; + + final Format format = imp != null && imp.getBanner() != null + ? resolveBidSizeFromBanner(imp.getBanner()) + : null; + return format != null + ? bid.toBuilder().w(format.getW()).h(format.getH()).build() + : bid; } - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); + private static Imp findImpById(String impId, List imps) { + return imps.stream() + .filter(imp -> imp.getId().equals(impId)) + .findFirst() + .orElse(null); + } + + private static BidType resolveBidType(Imp imp) { + if (imp == null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else { + return BidType.banner; + } + } + + private static Format resolveBidSizeFromBanner(Banner banner) { + Format result = null; + final Integer width = banner.getW(); + final Integer height = banner.getH(); + + final List formats = banner.getFormat(); + if (width != null && height != null) { + result = isOnlyOneSize(width, height, formats) + ? Format.builder().w(width).h(height).build() + : null; + } else if (formats.size() == 1) { + result = formats.get(0); + } + return result; + } + + private static boolean isOnlyOneSize(Integer width, Integer height, List formats) { + return CollectionUtils.isEmpty(formats) || (formats.size() == 1 && isSameFormat(width, height, formats.get(0))); + } + + private static boolean isSameFormat(Integer width, Integer height, Format format) { + return width.equals(format.getW()) && height.equals(format.getH()); } } diff --git a/src/main/java/org/prebid/server/bidder/pulsepoint/PulsepointAdapter.java b/src/main/java/org/prebid/server/bidder/pulsepoint/PulsepointAdapter.java deleted file mode 100644 index f08ba2bd813..00000000000 --- a/src/main/java/org/prebid/server/bidder/pulsepoint/PulsepointAdapter.java +++ /dev/null @@ -1,224 +0,0 @@ -package org.prebid.server.bidder.pulsepoint; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Banner; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Publisher; -import com.iab.openrtb.request.Site; -import com.iab.openrtb.response.BidResponse; -import io.vertx.core.http.HttpMethod; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AdUnitBid; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.bidder.Adapter; -import org.prebid.server.bidder.OpenrtbAdapter; -import org.prebid.server.bidder.model.AdUnitBidWithParams; -import org.prebid.server.bidder.model.AdapterHttpRequest; -import org.prebid.server.bidder.model.ExchangeCall; -import org.prebid.server.bidder.pulsepoint.model.NormalizedPulsepointParams; -import org.prebid.server.bidder.pulsepoint.proto.PulsepointParams; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.request.PreBidRequest; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.MediaType; -import org.prebid.server.util.HttpUtil; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Pulsepoint {@link Adapter} implementation. - */ -public class PulsepointAdapter extends OpenrtbAdapter { - - private static final Set ALLOWED_MEDIA_TYPES = Collections.singleton(MediaType.banner); - - private final String endpointUrl; - private final JacksonMapper mapper; - - public PulsepointAdapter(String cookieFamilyName, String endpointUrl, JacksonMapper mapper) { - super(cookieFamilyName); - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public List> makeHttpRequests(AdapterRequest adapterRequest, - PreBidRequestContext preBidRequestContext) { - final BidRequest bidRequest = createBidRequest(adapterRequest, preBidRequestContext); - final AdapterHttpRequest httpRequest = AdapterHttpRequest.of(HttpMethod.POST, endpointUrl, - bidRequest, headers()); - return Collections.singletonList(httpRequest); - } - - private BidRequest createBidRequest(AdapterRequest adapterRequest, PreBidRequestContext preBidRequestContext) { - final List adUnitBids = adapterRequest.getAdUnitBids(); - - validateAdUnitBidsMediaTypes(adUnitBids, ALLOWED_MEDIA_TYPES); - - final List> adUnitBidsWithParams = - createAdUnitBidsWithParams(adUnitBids); - final List imps = makeImps(adUnitBidsWithParams, preBidRequestContext); - validateImps(imps); - - final Publisher publisher = makePublisher(adUnitBidsWithParams); - - final PreBidRequest preBidRequest = preBidRequestContext.getPreBidRequest(); - return BidRequest.builder() - .id(preBidRequest.getTid()) - .at(1) - .tmax(preBidRequest.getTimeoutMillis()) - .imp(imps) - .app(makeApp(preBidRequestContext, publisher)) - .site(makeSite(preBidRequestContext, publisher)) - .device(deviceBuilder(preBidRequestContext).build()) - .user(makeUser(preBidRequestContext)) - .source(makeSource(preBidRequestContext)) - .regs(preBidRequest.getRegs()) - .build(); - } - - private List> createAdUnitBidsWithParams( - List adUnitBids) { - return adUnitBids.stream() - .map(adUnitBid -> AdUnitBidWithParams.of(adUnitBid, parseAndValidateParams(adUnitBid))) - .collect(Collectors.toList()); - } - - private NormalizedPulsepointParams parseAndValidateParams(AdUnitBid adUnitBid) { - final ObjectNode paramsNode = adUnitBid.getParams(); - if (paramsNode == null) { - throw new PreBidException("Pulsepoint params section is missing"); - } - - final PulsepointParams params; - try { - params = mapper.mapper().convertValue(paramsNode, PulsepointParams.class); - } catch (IllegalArgumentException e) { - // a weird way to pass parsing exception - throw new PreBidException(e.getMessage(), e.getCause()); - } - - final Integer publisherId = params.getPublisherId(); - if (publisherId == null || publisherId == 0) { - throw new PreBidException("Missing PublisherId param cp"); - } - final Integer tagId = params.getTagId(); - if (tagId == null || tagId == 0) { - throw new PreBidException("Missing TagId param ct"); - } - final String adSize = params.getAdSize(); - if (StringUtils.isEmpty(adSize)) { - throw new PreBidException("Missing AdSize param cf"); - } - - final String[] sizes = adSize.toLowerCase().split("x"); - if (sizes.length != 2) { - throw new PreBidException(String.format("Invalid AdSize param %s", adSize)); - } - final int width; - try { - width = Integer.parseInt(sizes[0]); - } catch (NumberFormatException e) { - throw new PreBidException(String.format("Invalid Width param %s", sizes[0])); - } - - final int height; - try { - height = Integer.parseInt(sizes[1]); - } catch (NumberFormatException e) { - throw new PreBidException(String.format("Invalid Height param %s", sizes[1])); - } - - return NormalizedPulsepointParams.of(String.valueOf(publisherId), String.valueOf(tagId), width, height); - } - - private static List makeImps(List> adUnitBidsWithParams, - PreBidRequestContext preBidRequestContext) { - return adUnitBidsWithParams.stream() - .filter(PulsepointAdapter::containsAnyAllowedMediaType) - .map(adUnitBidWithParams -> makeImp(adUnitBidWithParams, preBidRequestContext)) - .collect(Collectors.toList()); - } - - private static boolean containsAnyAllowedMediaType( - AdUnitBidWithParams adUnitBidWithParams) { - return CollectionUtils.containsAny(adUnitBidWithParams.getAdUnitBid().getMediaTypes(), ALLOWED_MEDIA_TYPES); - } - - private static Imp makeImp(AdUnitBidWithParams adUnitBidWithParams, - PreBidRequestContext preBidRequestContext) { - final AdUnitBid adUnitBid = adUnitBidWithParams.getAdUnitBid(); - final NormalizedPulsepointParams params = adUnitBidWithParams.getParams(); - - final Imp.ImpBuilder impBuilder = Imp.builder() - .id(adUnitBid.getAdUnitCode()) - .instl(adUnitBid.getInstl()) - .secure(preBidRequestContext.getSecure()) - .tagid(params.getTagId()); - - final Set mediaTypes = allowedMediaTypes(adUnitBid, ALLOWED_MEDIA_TYPES); - if (mediaTypes.contains(MediaType.banner)) { - impBuilder.banner(makeBanner(adUnitBid, params.getAdSizeWidth(), params.getAdSizeHeight())); - } - return impBuilder.build(); - } - - private static Banner makeBanner(AdUnitBid adUnitBid, Integer width, Integer height) { - return bannerBuilder(adUnitBid) - .w(width) - .h(height) - .build(); - } - - private Publisher makePublisher(List> adUnitBidsWithParams) { - final String publisherId = adUnitBidsWithParams.stream() - .map(adUnitBidWithParams -> adUnitBidWithParams.getParams().getPublisherId()) - .reduce((first, second) -> second).orElse(null); - return Publisher.builder().id(publisherId).build(); - } - - private static App makeApp(PreBidRequestContext preBidRequestContext, Publisher publisher) { - final App app = preBidRequestContext.getPreBidRequest().getApp(); - return app == null ? null : app.toBuilder() - .publisher(publisher) - .build(); - } - - private static Site makeSite(PreBidRequestContext preBidRequestContext, Publisher publisher) { - final Site.SiteBuilder siteBuilder = siteBuilder(preBidRequestContext); - return siteBuilder == null ? null : siteBuilder - .publisher(publisher) - .build(); - } - - @Override - public List extractBids(AdapterRequest adapterRequest, - ExchangeCall exchangeCall) { - return responseBidStream(exchangeCall.getResponse()) - .map(bid -> toBidBuilder(bid, adapterRequest)) - .collect(Collectors.toList()); - } - - private static Bid.BidBuilder toBidBuilder(com.iab.openrtb.response.Bid bid, AdapterRequest adapterRequest) { - final AdUnitBid adUnitBid = lookupBid(adapterRequest.getAdUnitBids(), bid.getImpid()); - return Bid.builder() - .bidder(adUnitBid.getBidderCode()) - .bidId(adUnitBid.getBidId()) - .code(bid.getImpid()) - .price(bid.getPrice()) - .adm(bid.getAdm()) - .creativeId(bid.getCrid()) - .width(bid.getW()) - .height(bid.getH()) - .mediaType(MediaType.banner); - } -} diff --git a/src/main/java/org/prebid/server/bidder/pulsepoint/PulsepointBidder.java b/src/main/java/org/prebid/server/bidder/pulsepoint/PulsepointBidder.java index 4f3a0d0e4c2..34d9760a27f 100644 --- a/src/main/java/org/prebid/server/bidder/pulsepoint/PulsepointBidder.java +++ b/src/main/java/org/prebid/server/bidder/pulsepoint/PulsepointBidder.java @@ -1,11 +1,11 @@ package org.prebid.server.bidder.pulsepoint; import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.OpenrtbBidder; import org.prebid.server.bidder.model.ImpWithExt; @@ -25,65 +25,33 @@ public PulsepointBidder(String endpointUrl, JacksonMapper mapper) { @Override protected Imp modifyImp(Imp imp, ExtImpPulsepoint extImpPulsepoint) throws PreBidException { - // imp validation - if (imp.getBanner() == null) { - throw new PreBidException(String.format("Invalid MediaType. Pulsepoint supports only Banner type. " - + "Ignoring ImpID=%s", imp.getId())); - } - - // imp.ext validation - final Integer publisherId = extImpPulsepoint.getPublisherId(); - if (publisherId == null || publisherId == 0) { - throw new PreBidException("Missing PublisherId param cp"); - } - final Integer tagId = extImpPulsepoint.getTagId(); - if (tagId == null || tagId == 0) { - throw new PreBidException("Missing TagId param ct"); - } - final String adSize = extImpPulsepoint.getAdSize(); - if (StringUtils.isEmpty(adSize)) { - throw new PreBidException("Missing AdSize param cf"); - } - if (adSize.toLowerCase().split("x").length != 2) { - throw new PreBidException(String.format("Invalid AdSize param %s", adSize)); - } - - // impression modifications and additional validation - final String[] sizes = extImpPulsepoint.getAdSize().toLowerCase().split("x"); - final int width; - final int height; - try { - width = Integer.parseInt(sizes[0]); - height = Integer.parseInt(sizes[1]); - } catch (NumberFormatException e) { - throw new PreBidException(String.format("Invalid Width or Height param %s x %s", sizes[0], sizes[1])); - } - final Banner modifiedBanner = imp.getBanner().toBuilder().w(width).h(height).build(); - - return imp.toBuilder() - .tagid(String.valueOf(extImpPulsepoint.getTagId())) - .banner(modifiedBanner) - .build(); + return imp.toBuilder().tagid(Objects.toString(extImpPulsepoint.getTagId())).build(); } @Override protected void modifyRequest(BidRequest bidRequest, BidRequest.BidRequestBuilder requestBuilder, List> impsWithExts) { - final Integer pubId = impsWithExts.stream() + final String pubId = impsWithExts.stream() .map(ImpWithExt::getImpExt) .map(ExtImpPulsepoint::getPublisherId) - .filter(Objects::nonNull) - .reduce((first, second) -> second) - .orElse(null); + .filter(this::isValidPublisherId) + .findFirst() + .map(Objects::toString) + .orElse(StringUtils.EMPTY); + final Site site = bidRequest.getSite(); final App app = bidRequest.getApp(); if (site != null) { - requestBuilder.site(modifySite(site, String.valueOf(pubId))); + requestBuilder.site(modifySite(site, pubId)); } else if (app != null) { - requestBuilder.app(modifyApp(app, String.valueOf(pubId))); + requestBuilder.app(modifyApp(app, pubId)); } } + private boolean isValidPublisherId(Integer publisherId) { + return publisherId != null && publisherId > 0; + } + private static Site modifySite(Site site, String publisherId) { return site.toBuilder() .publisher(site.getPublisher() == null @@ -100,8 +68,22 @@ private static App modifyApp(App app, String publisherId) { .build(); } - @Override - protected BidType getBidType(String impId, List imps) { - return BidType.banner; + protected BidType getBidType(Bid bid, List imps) { + final String impId = bid.getImpid(); + BidType bidType = null; + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getBanner() != null) { + bidType = BidType.banner; + } else if (imp.getVideo() != null) { + bidType = BidType.video; + } else if (imp.getAudio() != null) { + bidType = BidType.audio; + } else if (imp.getXNative() != null) { + bidType = BidType.xNative; + } + } + } + return bidType; } } diff --git a/src/main/java/org/prebid/server/bidder/revcontent/RevcontentBidder.java b/src/main/java/org/prebid/server/bidder/revcontent/RevcontentBidder.java new file mode 100644 index 00000000000..08b280234ac --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/revcontent/RevcontentBidder.java @@ -0,0 +1,43 @@ +package org.prebid.server.bidder.revcontent; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.OpenrtbBidder; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.List; + +public class RevcontentBidder extends OpenrtbBidder { + + public RevcontentBidder(String endpointUrl, JacksonMapper mapper) { + super(endpointUrl, RequestCreationStrategy.SINGLE_REQUEST, Void.class, mapper); + } + + @Override + protected void validateRequest(BidRequest bidRequest) throws PreBidException { + final App app = bidRequest.getApp(); + final Site site = bidRequest.getSite(); + final boolean hasAppName = app != null && StringUtils.isNotBlank(app.getName()); + final boolean hasSiteDomain = site != null && StringUtils.isNotBlank(site.getDomain()); + if (!hasAppName && !hasSiteDomain) { + throw new PreBidException("Impression is missing app name or site domain, and must contain one."); + } + } + + @Override + protected BidType getBidType(Bid bid, List imps) { + // native: {"ver":"1.1","assets":... + // banner:

{ new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; - private final String endpointUrl; private final JacksonMapper mapper; @@ -117,28 +115,29 @@ private ObjectNode impExtToObjectNode(ExtImpRhythmone extImpRhythmone) { public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - return bidResponse == null || bidResponse.getSeatbid() == null + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() : bidsFromResponse(bidRequest, bidResponse); } private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) .map(SeatBid::getBid) + .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getMediaTypes(bid.getImpid(), bidRequest.getImp()), - DEFAULT_BID_CURRENCY)) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); } - private static BidType getMediaTypes(String impId, List imps) { + private static BidType getBidType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId)) { if (imp.getBanner() != null) { @@ -150,9 +149,4 @@ private static BidType getMediaTypes(String impId, List imps) { } return BidType.banner; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconAdapter.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconAdapter.java deleted file mode 100644 index 25142a0069a..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconAdapter.java +++ /dev/null @@ -1,427 +0,0 @@ -package org.prebid.server.bidder.rubicon; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Banner; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Content; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Publisher; -import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.User; -import com.iab.openrtb.request.Video; -import com.iab.openrtb.response.BidResponse; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AdUnitBid; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.auction.model.Tuple2; -import org.prebid.server.bidder.Adapter; -import org.prebid.server.bidder.OpenrtbAdapter; -import org.prebid.server.bidder.model.AdapterHttpRequest; -import org.prebid.server.bidder.model.ExchangeCall; -import org.prebid.server.bidder.rubicon.proto.RubiconAppExt; -import org.prebid.server.bidder.rubicon.proto.RubiconBannerExt; -import org.prebid.server.bidder.rubicon.proto.RubiconBannerExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconDeviceExt; -import org.prebid.server.bidder.rubicon.proto.RubiconDeviceExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExt; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRpTrack; -import org.prebid.server.bidder.rubicon.proto.RubiconParams; -import org.prebid.server.bidder.rubicon.proto.RubiconPubExt; -import org.prebid.server.bidder.rubicon.proto.RubiconPubExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconSiteExt; -import org.prebid.server.bidder.rubicon.proto.RubiconSiteExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconTargeting; -import org.prebid.server.bidder.rubicon.proto.RubiconTargetingExt; -import org.prebid.server.bidder.rubicon.proto.RubiconTargetingExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconUserExt; -import org.prebid.server.bidder.rubicon.proto.RubiconUserExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconVideoExt; -import org.prebid.server.bidder.rubicon.proto.RubiconVideoExtRp; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.request.ExtApp; -import org.prebid.server.proto.openrtb.ext.request.ExtDevice; -import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; -import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; -import org.prebid.server.proto.openrtb.ext.request.rubicon.RubiconVideoParams; -import org.prebid.server.proto.request.PreBidRequest; -import org.prebid.server.proto.request.Sdk; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.MediaType; -import org.prebid.server.util.HttpUtil; - -import java.math.BigDecimal; -import java.util.Base64; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Rubicon Project {@link Adapter} implementation. - */ -public class RubiconAdapter extends OpenrtbAdapter { - - private static final Logger logger = LoggerFactory.getLogger(RubiconAdapter.class); - - private static final Set ALLOWED_MEDIA_TYPES = - Collections.unmodifiableSet(EnumSet.of(MediaType.banner, MediaType.video)); - - private static final String PREBID_SERVER_USER_AGENT = "prebid-server/1.0"; - - private final String endpointUrl; - private final JacksonMapper mapper; - - private final String authHeader; - - public RubiconAdapter(String cookieFamilyName, - String endpointUrl, - String xapiUsername, - String xapiPassword, - JacksonMapper mapper) { - - super(cookieFamilyName); - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - - this.authHeader = "Basic " + Base64.getEncoder().encodeToString((Objects.requireNonNull(xapiUsername) - + ':' + Objects.requireNonNull(xapiPassword)).getBytes()); - } - - @Override - public List> makeHttpRequests(AdapterRequest adapterRequest, - PreBidRequestContext preBidRequestContext) { - final MultiMap headers = headers() - .add(HttpUtil.AUTHORIZATION_HEADER, authHeader) - .add(HttpUtil.USER_AGENT_HEADER, PREBID_SERVER_USER_AGENT); - - final List adUnitBids = adapterRequest.getAdUnitBids(); - - validateAdUnitBidsMediaTypes(adUnitBids, ALLOWED_MEDIA_TYPES); - - final List requests = adUnitBids.stream() - .map(adUnitBid -> Tuple2.of(adUnitBid, mediaTypesFor(adUnitBid))) - .filter(tuple2 -> CollectionUtils.isNotEmpty(tuple2.getRight())) // skip adUnitBid with no mediaType - .map(tuple2 -> createBidRequests(tuple2.getLeft(), tuple2.getRight(), preBidRequestContext)) - .collect(Collectors.toList()); - - validateBidRequests(requests); - - return requests.stream() - .map(bidRequest -> AdapterHttpRequest.of(HttpMethod.POST, endpointUrl, bidRequest, headers)) - .collect(Collectors.toList()); - } - - private Set mediaTypesFor(AdUnitBid adUnitBid) { - return allowedMediaTypes(adUnitBid, ALLOWED_MEDIA_TYPES).stream() - .filter(mediaType -> isValidAdUnitBidMediaType(mediaType, adUnitBid)) - .collect(Collectors.toSet()); - } - - private static boolean isValidAdUnitBidMediaType(MediaType mediaType, AdUnitBid adUnitBid) { - switch (mediaType) { - case video: - final org.prebid.server.proto.request.Video video = adUnitBid.getVideo(); - return video != null && !CollectionUtils.isEmpty(video.getMimes()); - case banner: - return adUnitBid.getSizes().stream().map(RubiconSize::toId).anyMatch(id -> id > 0); - default: - return false; - } - } - - private BidRequest createBidRequests(AdUnitBid adUnitBid, Set mediaTypes, - PreBidRequestContext preBidRequestContext) { - final RubiconParams rubiconParams = parseAndValidateRubiconParams(adUnitBid); - final PreBidRequest preBidRequest = preBidRequestContext.getPreBidRequest(); - return BidRequest.builder() - .id(preBidRequest.getTid()) - .app(makeApp(rubiconParams, preBidRequestContext)) - .at(1) - .tmax(preBidRequest.getTimeoutMillis()) - .imp(Collections.singletonList(makeImp(adUnitBid, mediaTypes, rubiconParams, preBidRequestContext))) - .site(makeSite(rubiconParams, preBidRequestContext)) - .device(makeDevice(preBidRequestContext)) - .user(makeUser(rubiconParams, preBidRequestContext)) - .source(makeSource(preBidRequestContext)) - .regs(preBidRequest.getRegs()) - .build(); - } - - private RubiconParams parseAndValidateRubiconParams(AdUnitBid adUnitBid) { - final ObjectNode params = adUnitBid.getParams(); - if (params == null) { - throw new PreBidException("Rubicon params section is missing"); - } - - final RubiconParams rubiconParams; - try { - rubiconParams = mapper.mapper().convertValue(params, RubiconParams.class); - } catch (IllegalArgumentException e) { - // a weird way to pass parsing exception - throw new PreBidException(e.getMessage(), e.getCause()); - } - - final Integer accountId = rubiconParams.getAccountId(); - final Integer siteId = rubiconParams.getSiteId(); - final Integer zoneId = rubiconParams.getZoneId(); - if (accountId == null || accountId == 0) { - throw new PreBidException("Missing accountId param"); - } else if (siteId == null || siteId == 0) { - throw new PreBidException("Missing siteId param"); - } else if (zoneId == null || zoneId == 0) { - throw new PreBidException("Missing zoneId param"); - } - - return rubiconParams; - } - - private App makeApp(RubiconParams rubiconParams, PreBidRequestContext preBidRequestContext) { - final App app = preBidRequestContext.getPreBidRequest().getApp(); - return app == null ? null : app.toBuilder() - .publisher(makePublisher(rubiconParams)) - .ext(makeAppExt(rubiconParams)) - .build(); - } - - private Imp makeImp(AdUnitBid adUnitBid, - Set mediaTypes, - RubiconParams rubiconParams, - PreBidRequestContext preBidRequestContext) { - final Imp.ImpBuilder impBuilder = Imp.builder() - .id(adUnitBid.getAdUnitCode()) - .secure(preBidRequestContext.getSecure()) - .instl(adUnitBid.getInstl()) - .ext(mapper.mapper().valueToTree(makeImpExt(rubiconParams, preBidRequestContext))); - - if (mediaTypes.contains(MediaType.banner)) { - impBuilder.banner(makeBanner(adUnitBid)); - } - if (mediaTypes.contains(MediaType.video)) { - impBuilder.video(makeVideo(adUnitBid, rubiconParams.getVideo())); - } - return impBuilder.build(); - } - - private static RubiconImpExt makeImpExt(RubiconParams rubiconParams, PreBidRequestContext preBidRequestContext) { - return RubiconImpExt.of(RubiconImpExtRp.of(rubiconParams.getZoneId(), makeInventory(rubiconParams), - makeImpExtRpTrack(preBidRequestContext)), null); - } - - private static JsonNode makeInventory(RubiconParams rubiconParams) { - final JsonNode inventory = rubiconParams.getInventory(); - return inventory != null && !inventory.isNull() && inventory.size() != 0 ? inventory : null; - } - - private static RubiconImpExtRpTrack makeImpExtRpTrack(PreBidRequestContext preBidRequestContext) { - final Sdk sdk = preBidRequestContext.getPreBidRequest().getSdk(); - final String mintVersion; - if (sdk != null) { - mintVersion = String.format("%s_%s_%s", StringUtils.defaultString(sdk.getSource()), - StringUtils.defaultString(sdk.getPlatform()), StringUtils.defaultString(sdk.getVersion())); - } else { - mintVersion = "__"; - } - - return RubiconImpExtRpTrack.of("prebid", mintVersion); - } - - private Banner makeBanner(AdUnitBid adUnitBid) { - return bannerBuilder(adUnitBid) - .ext(mapper.mapper().valueToTree(makeBannerExt(adUnitBid.getSizes()))) - .build(); - } - - private Video makeVideo(AdUnitBid adUnitBid, RubiconVideoParams rubiconVideoParams) { - return videoBuilder(adUnitBid) - .ext(rubiconVideoParams != null ? mapper.mapper().valueToTree(makeVideoExt(rubiconVideoParams)) : null) - .build(); - } - - private static RubiconVideoExt makeVideoExt(RubiconVideoParams rubiconVideoParams) { - return RubiconVideoExt.of(rubiconVideoParams.getSkip(), rubiconVideoParams.getSkipdelay(), - RubiconVideoExtRp.of(rubiconVideoParams.getSizeId()), null); - } - - private static RubiconBannerExt makeBannerExt(List sizes) { - final List validRubiconSizeIds = sizes.stream() - .map(RubiconSize::toId) - .filter(id -> id > 0) - .sorted(RubiconSize.comparator()) - .collect(Collectors.toList()); - - return RubiconBannerExt.of(RubiconBannerExtRp.of( - validRubiconSizeIds.get(0), - validRubiconSizeIds.size() > 1 ? validRubiconSizeIds.subList(1, validRubiconSizeIds.size()) : null, - "text/html")); - } - - private Site makeSite(RubiconParams rubiconParams, PreBidRequestContext preBidRequestContext) { - Site.SiteBuilder siteBuilder = siteBuilder(preBidRequestContext); - if (siteBuilder == null) { - siteBuilder = Site.builder(); - } - - final PreBidRequest preBidRequest = preBidRequestContext.getPreBidRequest(); - if (preBidRequest.getApp() != null) { - final User user = preBidRequest.getUser(); - final String language = user != null ? user.getLanguage() : null; - siteBuilder - .content(Content.builder().language(language).build()); - } else { - siteBuilder - .publisher(makePublisher(rubiconParams)) - .ext(makeSiteExt(rubiconParams)); - } - - return siteBuilder.build(); - } - - private ExtSite makeSiteExt(RubiconParams rubiconParams) { - return mapper.fillExtension( - ExtSite.of(null, null), - RubiconSiteExt.of(RubiconSiteExtRp.of(rubiconParams.getSiteId()))); - } - - private ExtApp makeAppExt(RubiconParams rubiconParams) { - return mapper.fillExtension( - ExtApp.of(null, null), - RubiconAppExt.of(RubiconSiteExtRp.of(rubiconParams.getSiteId()))); - } - - private Publisher makePublisher(RubiconParams rubiconParams) { - return Publisher.builder() - .ext(makePublisherExt(rubiconParams)) - .build(); - } - - private ExtPublisher makePublisherExt(RubiconParams rubiconParams) { - return mapper.fillExtension( - ExtPublisher.empty(), - RubiconPubExt.of(RubiconPubExtRp.of(rubiconParams.getAccountId()))); - } - - private Device makeDevice(PreBidRequestContext preBidRequestContext) { - return deviceBuilder(preBidRequestContext) - .ext(makeDeviceExt(preBidRequestContext)) - .build(); - } - - private ExtDevice makeDeviceExt(PreBidRequestContext preBidRequestContext) { - final Device device = preBidRequestContext.getPreBidRequest().getDevice(); - final BigDecimal pixelratio = device != null ? device.getPxratio() : null; - - return mapper.fillExtension(ExtDevice.empty(), RubiconDeviceExt.of(RubiconDeviceExtRp.of(pixelratio))); - } - - private User makeUser(RubiconParams rubiconParams, PreBidRequestContext preBidRequestContext) { - User.UserBuilder userBuilder = userBuilder(preBidRequestContext); - final User user = preBidRequestContext.getPreBidRequest().getUser(); - if (userBuilder == null) { - userBuilder = user != null ? user.toBuilder() : User.builder(); - } - final ExtUser extUser = user == null ? null : user.getExt(); - final ExtUser rubiconUserExt = makeUserExt(rubiconParams, extUser); - return rubiconUserExt != null - ? userBuilder.ext(rubiconUserExt).build() - : userBuilder.build(); - } - - private ExtUser makeUserExt(RubiconParams rubiconParams, ExtUser extUser) { - final ExtUserDigiTrust digiTrust = extUser != null ? extUser.getDigitrust() : null; // will be removed - final JsonNode visitorNode = rubiconParams.getVisitor(); - final JsonNode visitor = visitorNode != null && !visitorNode.isNull() && visitorNode.size() != 0 - ? visitorNode - : null; - final boolean makeRp = visitor != null; - - if (digiTrust != null || visitor != null) { - final ExtUser userExt = extUser != null - ? ExtUser.builder().consent(extUser.getConsent()).eids(extUser.getEids()).build() - : ExtUser.builder().build(); - final RubiconUserExt rubiconUserExt = RubiconUserExt.builder() - .rp(makeRp ? RubiconUserExtRp.of(visitor) : null) - .build(); - return mapper.fillExtension(userExt, rubiconUserExt); - } - return null; - } - - private static void validateBidRequests(List bidRequests) { - if (bidRequests.isEmpty()) { - throw new PreBidException("Invalid ad unit/imp"); - } - } - - @Override - public List extractBids(AdapterRequest adapterRequest, - ExchangeCall exchangeCall) { - return responseBidStream(exchangeCall.getResponse()) - .filter(bid -> bid.getPrice() != null && bid.getPrice().compareTo(BigDecimal.ZERO) != 0) - .map(bid -> toBidBuilder(bid, adapterRequest, mediaTypeFor(exchangeCall.getRequest()))) - .limit(1) // one bid per request/response - .collect(Collectors.toList()); - } - - private static MediaType mediaTypeFor(BidRequest bidRequest) { - final MediaType mediaType = MediaType.banner; - if (bidRequest != null && CollectionUtils.isNotEmpty(bidRequest.getImp()) - && bidRequest.getImp().get(0).getVideo() != null) { - return MediaType.video; - } - return mediaType; - } - - private Bid.BidBuilder toBidBuilder(com.iab.openrtb.response.Bid bid, AdapterRequest adapterRequest, - MediaType mediaType) { - final AdUnitBid adUnitBid = lookupBid(adapterRequest.getAdUnitBids(), bid.getImpid()); - return Bid.builder() - .bidder(adUnitBid.getBidderCode()) - .bidId(adUnitBid.getBidId()) - .code(bid.getImpid()) - .price(bid.getPrice()) - .adm(bid.getAdm()) - .creativeId(bid.getCrid()) - .mediaType(mediaType) - .width(bid.getW()) - .height(bid.getH()) - .dealId(bid.getDealid()) - .adServerTargeting(toAdServerTargetingOrNull(bid)); - } - - private Map toAdServerTargetingOrNull(com.iab.openrtb.response.Bid bid) { - RubiconTargetingExt rubiconTargetingExt = null; - try { - rubiconTargetingExt = mapper.mapper().convertValue(bid.getExt(), RubiconTargetingExt.class); - } catch (IllegalArgumentException e) { - logger.warn("Exception occurred while de-serializing rubicon targeting extension", e); - } - - final RubiconTargetingExtRp rp = rubiconTargetingExt != null ? rubiconTargetingExt.getRp() : null; - final List targeting = rp != null ? rp.getTargeting() : null; - return targeting != null - ? targeting.stream().collect(Collectors.toMap(RubiconTargeting::getKey, t -> t.getValues().get(0))) - : null; - } - - @Override - public boolean tolerateErrors() { - return true; - } -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index bc0bd1d6181..13bb9f2edb6 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -1,28 +1,27 @@ package org.prebid.server.bidder.rubicon; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableSet; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Content; +import com.iab.openrtb.request.Data; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Metric; import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Segment; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Source; import com.iab.openrtb.request.User; import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import io.vertx.core.logging.Logger; @@ -31,6 +30,7 @@ import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.ViewabilityVendors; @@ -38,37 +38,40 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; -import org.prebid.server.bidder.rubicon.proto.RubiconAppExt; -import org.prebid.server.bidder.rubicon.proto.RubiconBannerExt; -import org.prebid.server.bidder.rubicon.proto.RubiconBannerExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconDeviceExt; -import org.prebid.server.bidder.rubicon.proto.RubiconDeviceExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconExtPrebidBidders; -import org.prebid.server.bidder.rubicon.proto.RubiconExtPrebidBiddersBidder; -import org.prebid.server.bidder.rubicon.proto.RubiconExtPrebidBiddersBidderDebug; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExt; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtPrebidBidder; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtPrebidRubiconDebug; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRpTrack; -import org.prebid.server.bidder.rubicon.proto.RubiconPubExt; -import org.prebid.server.bidder.rubicon.proto.RubiconPubExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconSiteExt; -import org.prebid.server.bidder.rubicon.proto.RubiconSiteExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconTargeting; -import org.prebid.server.bidder.rubicon.proto.RubiconTargetingExt; -import org.prebid.server.bidder.rubicon.proto.RubiconTargetingExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconUserExt; -import org.prebid.server.bidder.rubicon.proto.RubiconUserExtRp; -import org.prebid.server.bidder.rubicon.proto.RubiconVideoExt; -import org.prebid.server.bidder.rubicon.proto.RubiconVideoExtRp; +import org.prebid.server.bidder.rubicon.proto.request.RubiconAppExt; +import org.prebid.server.bidder.rubicon.proto.request.RubiconBannerExt; +import org.prebid.server.bidder.rubicon.proto.request.RubiconBannerExtRp; +import org.prebid.server.bidder.rubicon.proto.request.RubiconDeviceExt; +import org.prebid.server.bidder.rubicon.proto.request.RubiconDeviceExtRp; +import org.prebid.server.bidder.rubicon.proto.request.RubiconExtPrebidBidders; +import org.prebid.server.bidder.rubicon.proto.request.RubiconExtPrebidBiddersBidder; +import org.prebid.server.bidder.rubicon.proto.request.RubiconExtPrebidBiddersBidderDebug; +import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExt; +import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExtRp; +import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExtRpTrack; +import org.prebid.server.bidder.rubicon.proto.request.RubiconPubExt; +import org.prebid.server.bidder.rubicon.proto.request.RubiconPubExtRp; +import org.prebid.server.bidder.rubicon.proto.request.RubiconSiteExt; +import org.prebid.server.bidder.rubicon.proto.request.RubiconSiteExtRp; +import org.prebid.server.bidder.rubicon.proto.request.RubiconTargeting; +import org.prebid.server.bidder.rubicon.proto.request.RubiconTargetingExt; +import org.prebid.server.bidder.rubicon.proto.request.RubiconTargetingExtRp; +import org.prebid.server.bidder.rubicon.proto.request.RubiconUserExt; +import org.prebid.server.bidder.rubicon.proto.request.RubiconUserExtRp; +import org.prebid.server.bidder.rubicon.proto.request.RubiconVideoExt; +import org.prebid.server.bidder.rubicon.proto.request.RubiconVideoExtRp; +import org.prebid.server.bidder.rubicon.proto.response.RubiconBidResponse; +import org.prebid.server.bidder.rubicon.proto.response.RubiconSeatBid; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.ConditionalLogger; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtDeal; +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; import org.prebid.server.proto.openrtb.ext.request.ExtDevice; import org.prebid.server.proto.openrtb.ext.request.ExtImpContext; import org.prebid.server.proto.openrtb.ext.request.ExtImpContextDataAdserver; @@ -76,15 +79,18 @@ import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUidExt; import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubicon; +import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubiconDebug; import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtUserTpIdRubicon; import org.prebid.server.proto.openrtb.ext.request.rubicon.RubiconVideoParams; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.util.HttpUtil; import java.math.BigDecimal; @@ -113,11 +119,11 @@ public class RubiconBidder implements Bidder { private static final Logger logger = LoggerFactory.getLogger(RubiconBidder.class); + private static final ConditionalLogger MISSING_VIDEO_SIZE_LOGGER = + new ConditionalLogger("missing_video_size", logger); private static final String TK_XINT_QUERY_PARAMETER = "tk_xint"; private static final String PREBID_SERVER_USER_AGENT = "prebid-server/1.0"; - private static final String DEFAULT_BID_CURRENCY = "USD"; - private static final String PREBID_EXT = "prebid"; private static final String ADSERVER_EID = "adserver.org"; private static final String LIVEINTENT_EID = "liveintent.com"; @@ -128,25 +134,35 @@ public class RubiconBidder implements Bidder { private static final String FPD_PAGE_FIELD = "page"; private static final String FPD_REF_FIELD = "ref"; private static final String FPD_SEARCH_FIELD = "search"; - private static final String FPD_ADSLOT_FIELD = "adslot"; - private static final String FPD_PBADSLOT_FIELD = "pbadslot"; + private static final String FPD_CONTEXT_FIELD = "context"; + private static final String FPD_DATA_FIELD = "data"; private static final String FPD_ADSERVER_FIELD = "adserver"; private static final String FPD_ADSERVER_NAME_GAM = "gam"; - private static final String FPD_DFP_AD_UNIT_CODE_FIELD = "dfp_ad_unit_code"; private static final String FPD_KEYWORDS_FIELD = "keywords"; + private static final String PREBID_EXT = "prebid"; private static final String PPUID_STYPE = "ppuid"; private static final String SHA256EMAIL_STYPE = "sha256email"; private static final String DMP_STYPE = "dmp"; + private static final String XAPI_CURRENCY = "USD"; + private static final Set USER_SEGTAXES = ImmutableSet.of(4); + private static final Set SITE_SEGTAXES = ImmutableSet.of(1, 2, 5, 6); + private static final Set STYPE_TO_REMOVE = new HashSet<>(Arrays.asList(PPUID_STYPE, SHA256EMAIL_STYPE, DMP_STYPE)); private static final TypeReference> RUBICON_EXT_TYPE_REFERENCE = new TypeReference>() { }; + private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = + new TypeReference>() { + }; + private static final int PORTRAIT_MOBILE_SIZE_ID = 67; + private static final int LANDSCAPE_MOBILE_SIZE_ID = 101; private final String endpointUrl; private final Set supportedVendors; private final boolean generateBidId; + private final CurrencyConversionService currencyConversionService; private final JacksonMapper mapper; private final MultiMap headers; @@ -156,11 +172,13 @@ public RubiconBidder(String endpoint, String xapiPassword, List supportedVendors, boolean generateBidId, + CurrencyConversionService currencyConversionService, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); this.supportedVendors = new HashSet<>(supportedVendors); this.generateBidId = generateBidId; + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.mapper = Objects.requireNonNull(mapper); this.headers = headers(Objects.requireNonNull(xapiUsername), Objects.requireNonNull(xapiPassword)); @@ -171,24 +189,28 @@ public Result>> makeHttpRequests(BidRequest bidRequ final List> httpRequests = new ArrayList<>(); final List errors = new ArrayList<>(); + final List imps = extractValidImps(bidRequest, errors); + if (CollectionUtils.isEmpty(imps)) { + errors.add(BidderError.of("There are no valid impressions to create bid request to rubicon bidder", + BidderError.Type.bad_input)); + return Result.of(Collections.emptyList(), errors); + } final Map> impToImpExt = - parseRubiconImpExts(bidRequest.getImp(), errors); + parseRubiconImpExts(imps, errors); final String impLanguage = firstImpExtLanguage(impToImpExt.values()); + final String uri = makeUri(bidRequest); for (Map.Entry> impToExt : impToImpExt.entrySet()) { try { final Imp imp = impToExt.getKey(); final ExtPrebid ext = impToExt.getValue(); - final BidRequest singleRequest = createSingleRequest( - imp, ext.getPrebid(), ext.getBidder(), bidRequest, impLanguage); - final String body = mapper.encode(singleRequest); - httpRequests.add(HttpRequest.builder() - .method(HttpMethod.POST) - .uri(makeUri(bidRequest)) - .body(body) - .headers(headers) - .payload(singleRequest) - .build()); + final BidRequest singleImpRequest = createSingleRequest( + imp, ext.getPrebid(), ext.getBidder(), bidRequest, impLanguage, errors); + if (hasDeals(imp)) { + httpRequests.addAll(createDealsRequests(singleImpRequest, uri)); + } else { + httpRequests.add(createHttpRequest(singleImpRequest, uri)); + } } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } @@ -199,17 +221,13 @@ public Result>> makeHttpRequests(BidRequest bidRequ @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final HttpResponse response = httpCall.getResponse(); - if (response.getStatusCode() == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - try { - final BidResponse bidResponse = mapper.decodeValue(response.getBody(), BidResponse.class); - return Result.of(extractBids(bidRequest, httpCall.getRequest().getPayload(), bidResponse), - Collections.emptyList()); + final List errors = new ArrayList<>(); + final RubiconBidResponse bidResponse = + mapper.decodeValue(httpCall.getResponse().getBody(), RubiconBidResponse.class); + return Result.of(extractBids(bidRequest, httpCall.getRequest().getPayload(), bidResponse, errors), errors); } catch (DecodeException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } @@ -224,19 +242,55 @@ public Map extractTargeting(ObjectNode extBidBidder) { } final RubiconTargetingExtRp rp = rubiconTargetingExt.getRp(); - final List targeting = rp != null ? rp.getTargeting() : null; - return targeting != null - ? targeting.stream() - .filter(rubiconTargeting -> !CollectionUtils.isEmpty(rubiconTargeting.getValues())) - .collect(Collectors.toMap(RubiconTargeting::getKey, t -> t.getValues().get(0))) + final List targetings = rp != null ? rp.getTargeting() : null; + return targetings != null + ? targetings.stream() + .filter(targeting -> !CollectionUtils.isEmpty(targeting.getValues())) + .collect(Collectors.toMap(RubiconTargeting::getKey, targeting -> targeting.getValues().get(0))) : Collections.emptyMap(); } + private List extractValidImps(BidRequest bidRequest, List errors) { + final Map> isValidToImps = bidRequest.getImp().stream() + .collect(Collectors.groupingBy(RubiconBidder::isValidType)); + + isValidToImps.getOrDefault(false, Collections.emptyList()).stream() + .map(this::impTypeErrorMessage) + .forEach(errors::add); + + return isValidToImps.getOrDefault(true, Collections.emptyList()); + } + + private static boolean isValidType(Imp imp) { + return imp.getVideo() != null || imp.getBanner() != null; + } + + private BidderError impTypeErrorMessage(Imp imp) { + final BidType type = resolveExpectedBidType(imp); + return BidderError.of( + String.format("Impression with id %s rejected with invalid type `%s`." + " Allowed types are banner and" + + " video.", imp.getId(), type != null ? type.name() : "unknown"), BidderError.Type.bad_input); + } + + private static BidType resolveExpectedBidType(Imp imp) { + if (imp.getBanner() != null) { + return BidType.banner; + } + if (imp.getVideo() != null) { + return BidType.video; + } + if (imp.getAudio() != null) { + return BidType.audio; + } + if (imp.getXNative() != null) { + return BidType.xNative; + } + return null; + } + private static MultiMap headers(String xapiUsername, String xapiPassword) { - return MultiMap.caseInsensitiveMultiMap() + return HttpUtil.headers() .add(HttpUtil.AUTHORIZATION_HEADER, authHeader(xapiUsername, xapiPassword)) - .add(HttpUtil.CONTENT_TYPE_HEADER, HttpUtil.APPLICATION_JSON_CONTENT_TYPE) - .add(HttpUtil.ACCEPT_HEADER, HttpHeaderValues.APPLICATION_JSON) .add(HttpUtil.USER_AGENT_HEADER, PREBID_SERVER_USER_AGENT); } @@ -280,21 +334,19 @@ private static String firstImpExtLanguage(Collection errors) { return bidRequest.toBuilder() - .imp(Collections.singletonList(makeImp(imp, extPrebid, extRubicon, site, app))) - .user(makeUser(bidRequest.getUser(), extRubicon)) + .imp(Collections.singletonList(makeImp(imp, extImpPrebid, extImpRubicon, bidRequest, errors))) + .user(makeUser(bidRequest.getUser(), extImpRubicon)) .device(makeDevice(bidRequest.getDevice())) - .site(makeSite(site, impLanguage, extRubicon)) - .app(makeApp(app, extRubicon)) - .source(makeSource(bidRequest.getSource(), extRubicon.getPchain())) + .site(makeSite(bidRequest.getSite(), impLanguage, extImpRubicon)) + .app(makeApp(bidRequest.getApp(), extImpRubicon)) + .source(makeSource(bidRequest.getSource(), extImpRubicon.getPchain())) .cur(null) // suppress currencies .ext(null) // suppress ext .build(); @@ -337,18 +389,34 @@ private RubiconExtPrebidBiddersBidder extPrebidBiddersRubicon(ExtRequest extRequ return null; } - private Imp makeImp(Imp imp, ExtImpPrebid extPrebid, ExtImpRubicon extRubicon, Site site, App app) { + private Imp makeImp(Imp imp, + ExtImpPrebid extImpPrebid, + ExtImpRubicon extImpRubicon, + BidRequest bidRequest, + List errors) { + + final App app = bidRequest.getApp(); + final Site site = bidRequest.getSite(); + final ExtRequest extRequest = bidRequest.getExt(); + final Imp.ImpBuilder builder = imp.toBuilder() .metric(makeMetrics(imp)) - .ext(mapper.mapper().valueToTree(makeImpExt(imp, extRubicon, site, app))); + .ext(mapper.mapper().valueToTree(makeImpExt(imp, extImpRubicon, site, app, extRequest))); + + final BigDecimal resolvedBidFloor = resolveBidFloor(imp, bidRequest, errors); + if (resolvedBidFloor != null) { + builder + .bidfloorcur(XAPI_CURRENCY) + .bidfloor(resolvedBidFloor); + } if (isVideo(imp)) { builder .banner(null) - .video(makeVideo(imp.getVideo(), extRubicon.getVideo(), extPrebid)); + .video(makeVideo(imp, extImpRubicon.getVideo(), extImpPrebid, referer(site))); } else { builder - .banner(makeBanner(imp.getBanner(), overriddenSizes(extRubicon))) + .banner(makeBanner(imp, overriddenSizes(extImpRubicon))) .video(null); } @@ -378,27 +446,100 @@ private boolean isMetricSupported(Metric metric) { return supportedVendors.contains(metric.getVendor()) && Objects.equals(metric.getType(), "viewability"); } - private RubiconImpExt makeImpExt(Imp imp, ExtImpRubicon rubiconImpExt, Site site, App app) { + private BigDecimal resolveBidFloor(Imp imp, BidRequest bidRequest, List errors) { + final BigDecimal resolvedBidFloorPrice = resolveBidFloorPrice(imp); + if (resolvedBidFloorPrice == null) { + return null; + } + + final String resolvedBidFloorCurrency = resolveBidFloorCurrency(imp, bidRequest, errors); + return ObjectUtils.notEqual(resolvedBidFloorCurrency, XAPI_CURRENCY) + ? convertBidFloorCurrency(resolvedBidFloorPrice, resolvedBidFloorCurrency, imp, bidRequest) + : null; + } + + private static BigDecimal resolveBidFloorPrice(Imp imp) { + final BigDecimal bidFloor = imp.getBidfloor(); + return bidFloor != null && bidFloor.compareTo(BigDecimal.ZERO) > 0 ? bidFloor : null; + } + + private static String resolveBidFloorCurrency(Imp imp, BidRequest bidRequest, List errors) { + final String bidFloorCurrency = imp.getBidfloorcur(); + if (StringUtils.isBlank(bidFloorCurrency)) { + if (isDebugEnabled(bidRequest)) { + errors.add(BidderError.badInput(String.format("Imp `%s` floor provided with no currency, assuming %s", + imp.getId(), XAPI_CURRENCY))); + } + return XAPI_CURRENCY; + } + return bidFloorCurrency; + } + + /** + * Determines debug flag from {@link BidRequest} or {@link ExtRequest}. + */ + private static boolean isDebugEnabled(BidRequest bidRequest) { + if (Objects.equals(bidRequest.getTest(), 1)) { + return true; + } + final ExtRequest extRequest = bidRequest.getExt(); + final ExtRequestPrebid extRequestPrebid = extRequest != null ? extRequest.getPrebid() : null; + return extRequestPrebid != null && Objects.equals(extRequestPrebid.getDebug(), 1); + } + + private BigDecimal convertBidFloorCurrency(BigDecimal bidFloor, + String bidFloorCurrency, + Imp imp, + BidRequest bidRequest) { + try { + return currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCurrency, XAPI_CURRENCY); + } catch (PreBidException e) { + throw new PreBidException(String.format( + "Unable to convert provided bid floor currency from %s to %s for imp `%s` with a reason: %s", + bidFloorCurrency, XAPI_CURRENCY, imp.getId(), e.getMessage())); + } + } + + private RubiconImpExt makeImpExt(Imp imp, + ExtImpRubicon rubiconImpExt, + Site site, + App app, + ExtRequest extRequest) { + final ExtImpContext context = extImpContext(imp); return RubiconImpExt.of( RubiconImpExtRp.of( rubiconImpExt.getZoneId(), - makeTarget(imp, rubiconImpExt, site, app), + makeTarget(imp, rubiconImpExt, site, app, context), RubiconImpExtRpTrack.of("", "")), - mapVendorsNamesToUrls(imp.getMetric())); + mapVendorsNamesToUrls(imp.getMetric()), + getMaxBids(extRequest), + getAdSlot(imp, context)); } - private JsonNode makeTarget(Imp imp, ExtImpRubicon rubiconImpExt, Site site, App app) { + private JsonNode makeTarget(Imp imp, ExtImpRubicon rubiconImpExt, Site site, App app, ExtImpContext context) { final ObjectNode result = mapper.mapper().createObjectNode(); populateFirstPartyDataAttributes(rubiconImpExt.getInventory(), result); mergeFirstPartyDataFromSite(site, result); mergeFirstPartyDataFromApp(app, result); - mergeFirstPartyDataFromImp(imp, rubiconImpExt, result); + mergeFirstPartyDataFromImp(imp, rubiconImpExt, context, result); return result.size() > 0 ? result : null; } + private ExtImpContext extImpContext(Imp imp) { + final JsonNode context = imp.getExt().get(FPD_CONTEXT_FIELD); + if (context == null || context.isNull()) { + return null; + } + try { + return mapper.mapper().convertValue(context, ExtImpContext.class); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage(), e); + } + } + private void mergeFirstPartyDataFromSite(Site site, ObjectNode result) { // merge OPENRTB.site.ext.data.* to every impression – XAPI.imp[].ext.rp.target.* final ExtSite siteExt = site != null ? site.getExt() : null; @@ -431,65 +572,57 @@ private void mergeFirstPartyDataFromApp(App app, ObjectNode result) { mergeCollectionAttributeIntoArray(result, app, App::getPagecat, FPD_PAGECAT_FIELD); } - private void mergeFirstPartyDataFromImp(Imp imp, ExtImpRubicon rubiconImpExt, ObjectNode result) { - final ExtImpContext context = extImpContext(imp); + private void mergeFirstPartyDataFromImp(Imp imp, + ExtImpRubicon rubiconImpExt, + ExtImpContext context, + ObjectNode result) { + final JsonNode data = imp.getExt().get(FPD_DATA_FIELD); - mergeFirstPartyDataFromContextData(context, result); - mergeFirstPartyDataKeywords(context, result); + mergeFirstPartyDataFromData(imp, context, result); + mergeFirstPartyDataKeywords(imp, context, result); // merge OPENRTB.imp[].ext.rubicon.keywords to XAPI.imp[].ext.rp.target.keywords mergeCollectionAttributeIntoArray(result, rubiconImpExt, ExtImpRubicon::getKeywords, FPD_KEYWORDS_FIELD); // merge OPENRTB.imp[].ext.context.search to XAPI.imp[].ext.rp.target.search - mergeStringAttributeIntoArray(result, context, ExtImpContext::getSearch, FPD_SEARCH_FIELD); - } - - private ExtImpContext extImpContext(Imp imp) { - final JsonNode context = imp.getExt().get("context"); - if (context == null || context.isNull()) { - return null; - } - try { - return mapper.mapper().convertValue(context, ExtImpContext.class); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); - } - } - - private void mergeFirstPartyDataFromContextData(ExtImpContext context, ObjectNode result) { - if (context == null) { - return; - } - + mergeStringAttributeIntoArray( + result, + context, + extContext -> getTextValueFromNode(extContext.getProperty(FPD_SEARCH_FIELD)), + FPD_SEARCH_FIELD); + // merge OPENRTB.imp[].ext.data.search to XAPI.imp[].ext.rp.target.search + mergeStringAttributeIntoArray( + result, + data, + node -> getTextValueFromNodeByPath(node, FPD_SEARCH_FIELD), + FPD_SEARCH_FIELD); + } + + private void mergeFirstPartyDataFromData(Imp imp, ExtImpContext context, ObjectNode result) { // merge OPENRTB.imp[].ext.context.data.* to XAPI.imp[].ext.rp.target.* - final ObjectNode contextDataNode = context.getData(); + final ObjectNode contextDataNode = context != null ? context.getData() : null; if (contextDataNode != null) { populateFirstPartyDataAttributes(contextDataNode, result); } - copyAdslot(context, result); - } - - private void copyAdslot(ExtImpContext context, ObjectNode result) { - // copy OPENRTB.imp[].ext.context.data.adslot or imp[].ext.context.adserver.adslot to - // XAPI.imp[].ext.rp.target.dfp_ad_unit_code without leading slash - final ObjectNode contextDataNode = context.getData(); - - final String adSlot = ObjectUtils.firstNonNull( - getTextValueFromNodeByPath(contextDataNode, FPD_ADSLOT_FIELD), - getAdSlotFromAdServer(contextDataNode), - getTextValueFromNodeByPath(contextDataNode, FPD_PBADSLOT_FIELD)); - - if (StringUtils.isNotBlank(adSlot)) { - final String adUnitCode = adSlot.indexOf('/') == 0 ? adSlot.substring(1) : adSlot; - result.put(FPD_DFP_AD_UNIT_CODE_FIELD, adUnitCode); + // merge OPENRTB.imp[].ext.data.* to XAPI.imp[].ext.rp.target.* + final JsonNode dataNode = imp.getExt().get(FPD_DATA_FIELD); + if (dataNode != null && dataNode.isObject()) { + populateFirstPartyDataAttributes((ObjectNode) dataNode, result); } } - private void mergeFirstPartyDataKeywords(ExtImpContext context, ObjectNode result) { + private void mergeFirstPartyDataKeywords(Imp imp, ExtImpContext context, ObjectNode result) { // merge OPENRTB.imp[].ext.context.keywords to XAPI.imp[].ext.rp.target.keywords - final String keywords = context != null ? context.getKeywords() : null; + final JsonNode keywordsNode = context != null ? context.getProperty("keywords") : null; + final String keywords = getTextValueFromNode(keywordsNode); if (StringUtils.isNotBlank(keywords)) { mergeIntoArray(result, FPD_KEYWORDS_FIELD, keywords.split(",")); } + + // merge OPENRTB.imp[].ext.data.keywords to XAPI.imp[].ext.rp.target.keywords + final String dataKeywords = getTextValueFromNodeByPath(imp.getExt().get(FPD_DATA_FIELD), FPD_KEYWORDS_FIELD); + if (StringUtils.isNotBlank(dataKeywords)) { + mergeIntoArray(result, FPD_KEYWORDS_FIELD, dataKeywords.split(",")); + } } private > void mergeCollectionAttributeIntoArray( @@ -530,23 +663,8 @@ private static String getTextValueFromNodeByPath(JsonNode node, String path) { return nodeByPath != null && nodeByPath.isTextual() ? nodeByPath.textValue() : null; } - private String getAdSlotFromAdServer(ObjectNode contextDataNode) { - final ExtImpContextDataAdserver adServer = extImpContextDataAdserver(contextDataNode); - return adServer != null && Objects.equals(adServer.getName(), FPD_ADSERVER_NAME_GAM) - ? adServer.getAdslot() - : null; - } - - private ExtImpContextDataAdserver extImpContextDataAdserver(ObjectNode contextData) { - final JsonNode adServerNode = contextData != null ? contextData.get(FPD_ADSERVER_FIELD) : null; - if (adServerNode == null || adServerNode.isNull()) { - return null; - } - try { - return mapper.mapper().convertValue(adServerNode, ExtImpContextDataAdserver.class); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); - } + private static String getTextValueFromNode(JsonNode node) { + return node != null && node.isTextual() ? node.textValue() : null; } private void populateFirstPartyDataAttributes(ObjectNode sourceNode, ObjectNode targetNode) { @@ -565,6 +683,10 @@ private void populateFirstPartyDataAttributes(ObjectNode sourceNode, ObjectNode mergeIntoArray(targetNode, currentFieldName, currentField.textValue()); } else if (currentField.isIntegralNumber()) { mergeIntoArray(targetNode, currentFieldName, Long.toString(currentField.longValue())); + } else if (currentField.isBoolean()) { + mergeIntoArray(targetNode, currentFieldName, Boolean.toString(currentField.booleanValue())); + } else if (isBooleanArray(currentField)) { + mergeIntoArray(targetNode, currentFieldName, booleanArrayToStringList(currentField)); } } } @@ -573,6 +695,10 @@ private static boolean isTextualArray(JsonNode node) { return node.isArray() && StreamSupport.stream(node.spliterator(), false).allMatch(JsonNode::isTextual); } + private static boolean isBooleanArray(JsonNode node) { + return node.isArray() && StreamSupport.stream(node.spliterator(), false).allMatch(JsonNode::isBoolean); + } + private ArrayNode stringsToStringArray(String... values) { return stringsToStringArray(Arrays.asList(values)); } @@ -589,6 +715,13 @@ private static LinkedHashSet stringArrayToStringSet(JsonNode stringArray .collect(Collectors.toCollection(LinkedHashSet::new)); } + private static List booleanArrayToStringList(JsonNode booleanArray) { + return StreamSupport.stream(booleanArray.spliterator(), false) + .map(JsonNode::booleanValue) + .map(value -> Boolean.toString(value)) + .collect(Collectors.toList()); + } + private List mapVendorsNamesToUrls(List metrics) { if (metrics == null) { return null; @@ -600,6 +733,47 @@ private List mapVendorsNamesToUrls(List metrics) { return vendorsUrls.isEmpty() ? null : vendorsUrls; } + private Integer getMaxBids(ExtRequest extRequest) { + final ExtRequestPrebid extRequestPrebid = extRequest != null ? extRequest.getPrebid() : null; + final List multibids = extRequestPrebid != null + ? extRequestPrebid.getMultibid() : null; + final ExtRequestPrebidMultiBid extRequestPrebidMultiBid = + CollectionUtils.isNotEmpty(multibids) ? multibids.get(0) : null; + final Integer multibidMaxBids = extRequestPrebidMultiBid != null ? extRequestPrebidMultiBid.getMaxBids() : null; + + return multibidMaxBids != null ? multibidMaxBids : 1; + } + + private String getAdSlot(Imp imp, ExtImpContext context) { + final ObjectNode contextDataNode = context != null ? context.getData() : null; + final JsonNode dataNode = imp.getExt().get(FPD_DATA_FIELD); + + return ObjectUtils.firstNonNull( + // or imp[].ext.context.data.adserver.adslot + getAdSlotFromAdServer(contextDataNode), + // or imp[].ext.data.adserver.adslot + getAdSlotFromAdServer(dataNode)); + } + + private String getAdSlotFromAdServer(JsonNode dataNode) { + final ExtImpContextDataAdserver adServer = extImpContextDataAdserver(dataNode); + return adServer != null && Objects.equals(adServer.getName(), FPD_ADSERVER_NAME_GAM) + ? adServer.getAdslot() + : null; + } + + private ExtImpContextDataAdserver extImpContextDataAdserver(JsonNode contextData) { + final JsonNode adServerNode = contextData != null ? contextData.get(FPD_ADSERVER_FIELD) : null; + if (adServerNode == null || adServerNode.isNull()) { + return null; + } + try { + return mapper.mapper().convertValue(adServerNode, ExtImpContextDataAdserver.class); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage(), e); + } + } + private static boolean isVideo(Imp imp) { final Video video = imp.getVideo(); if (video != null) { @@ -615,23 +789,61 @@ private static boolean isFullyPopulatedVideo(Video video) { && video.getLinearity() != null && video.getApi() != null; } - private Video makeVideo(Video video, RubiconVideoParams rubiconVideoParams, ExtImpPrebid prebidImpExt) { + private static String referer(Site site) { + return site != null ? site.getPage() : null; + } + + private Video makeVideo(Imp imp, RubiconVideoParams rubiconVideoParams, ExtImpPrebid prebidImpExt, String referer) { + final Video video = imp.getVideo(); + + final Integer skip = rubiconVideoParams != null ? rubiconVideoParams.getSkip() : null; + final Integer skipDelay = rubiconVideoParams != null ? rubiconVideoParams.getSkipdelay() : null; + final Integer sizeId = rubiconVideoParams != null ? rubiconVideoParams.getSizeId() : null; + + final Integer resolvedSizeId = sizeId == null || sizeId == 0 + ? resolveVideoSizeId(video.getPlacement(), imp.getInstl()) + : sizeId; + validateVideoSizeId(resolvedSizeId, referer, imp.getId()); + final String videoType = prebidImpExt != null && prebidImpExt.getIsRewardedInventory() != null && prebidImpExt.getIsRewardedInventory() == 1 ? "rewarded" : null; - if (rubiconVideoParams == null && videoType == null) { + // optimization for empty ext params + if (skip == null && skipDelay == null && resolvedSizeId == null && videoType == null) { return video; } - final Integer skip = rubiconVideoParams != null ? rubiconVideoParams.getSkip() : null; - final Integer skipDelay = rubiconVideoParams != null ? rubiconVideoParams.getSkipdelay() : null; - final Integer sizeId = rubiconVideoParams != null ? rubiconVideoParams.getSizeId() : null; return video.toBuilder() .ext(mapper.mapper().valueToTree( - RubiconVideoExt.of(skip, skipDelay, RubiconVideoExtRp.of(sizeId), videoType))) + RubiconVideoExt.of(skip, skipDelay, RubiconVideoExtRp.of(resolvedSizeId), videoType))) .build(); } + private static void validateVideoSizeId(Integer resolvedSizeId, String referer, String impId) { + // log only 1% of cases to monitor how often video impressions does not have size id + if (resolvedSizeId == null) { + MISSING_VIDEO_SIZE_LOGGER.warn(String.format("RP adapter: video request with no size_id. Referrer URL = %s," + + " impId = %s", referer, impId), 0.01d); + } + } + + private static Integer resolveVideoSizeId(Integer placement, Integer instl) { + if (placement != null) { + if (placement == 1) { + return 201; + } + if (placement == 3) { + return 203; + } + } + + if (instl != null && instl == 1) { + return 202; + } + + return null; + } + private static List overriddenSizes(ExtImpRubicon rubiconImpExt) { final List overriddenSizes; @@ -649,7 +861,9 @@ private static List overriddenSizes(ExtImpRubicon rubiconImpExt) { return overriddenSizes; } - private Banner makeBanner(Banner banner, List overriddenSizes) { + private Banner makeBanner(Imp imp, List overriddenSizes) { + final Banner banner = imp.getBanner(); + final boolean isInterstitial = Objects.equals(imp.getInstl(), 1); final List sizes = ObjectUtils.defaultIfNull(overriddenSizes, banner.getFormat()); if (CollectionUtils.isEmpty(sizes)) { throw new PreBidException("rubicon imps must have at least one imp.format element"); @@ -657,12 +871,12 @@ private Banner makeBanner(Banner banner, List overriddenSizes) { return banner.toBuilder() .format(sizes) - .ext(mapper.mapper().valueToTree(makeBannerExt(sizes))) + .ext(mapper.mapper().valueToTree(makeBannerExt(sizes, isInterstitial))) .build(); } - private static RubiconBannerExt makeBannerExt(List sizes) { - final List rubiconSizeIds = mapToRubiconSizeIds(sizes); + private static RubiconBannerExt makeBannerExt(List sizes, boolean isInterstitial) { + final List rubiconSizeIds = mapToRubiconSizeIds(sizes, isInterstitial); final Integer primarySizeId = rubiconSizeIds.get(0); final List altSizeIds = rubiconSizeIds.size() > 1 ? rubiconSizeIds.subList(1, rubiconSizeIds.size()) @@ -671,7 +885,7 @@ private static RubiconBannerExt makeBannerExt(List sizes) { return RubiconBannerExt.of(RubiconBannerExtRp.of(primarySizeId, altSizeIds, "text/html")); } - private static List mapToRubiconSizeIds(List sizes) { + private static List mapToRubiconSizeIds(List sizes, boolean isInterstitial) { final List validRubiconSizeIds = sizes.stream() .map(RubiconSize::toId) .filter(id -> id > 0) @@ -679,11 +893,20 @@ private static List mapToRubiconSizeIds(List sizes) { .collect(Collectors.toList()); if (validRubiconSizeIds.isEmpty()) { - throw new PreBidException("No valid sizes"); + // FIXME: Added 11.11.2020. short term solution for full screen interstitial adunits (PR #1003) + if (isInterstitial) { + validRubiconSizeIds.add(resolveNotStandardSizeForInstl(sizes.get(0))); + } else { + throw new PreBidException("No valid sizes"); + } } return validRubiconSizeIds; } + private static int resolveNotStandardSizeForInstl(Format size) { + return size.getH() > size.getW() ? PORTRAIT_MOBILE_SIZE_ID : LANDSCAPE_MOBILE_SIZE_ID; + } + private User makeUser(User user, ExtImpRubicon rubiconImpExt) { final String userId = user != null ? user.getId() : null; final ExtUser extUser = user != null ? user.getExt() : null; @@ -711,7 +934,7 @@ private User makeUser(User user, ExtImpRubicon rubiconImpExt) { final ExtUser userExt = extUser != null ? ExtUser.builder() .consent(extUser.getConsent()) - .digitrust(extUser.getDigitrust()) + .eids(extUser.getEids()) .eids(resolvedExtUserEids) .build() : ExtUser.builder().build(); @@ -860,7 +1083,11 @@ private JsonNode rubiconUserExtRpTarget(Map> sourceToUs copyLiveintentSegment(sourceToUserEidExt, result); - mergeFirstPartyDataFromUser(user, result); + if (user != null) { + mergeFirstPartyDataFromUser(user.getExt(), result); + + enrichWithIabAttribute(result, user.getData(), USER_SEGTAXES); + } return result.size() > 0 ? result : null; } @@ -887,25 +1114,44 @@ private static void copyLiveintentSegment(Map> sourceTo } } - private void mergeFirstPartyDataFromUser(User user, ObjectNode result) { + private void mergeFirstPartyDataFromUser(ExtUser userExt, ObjectNode result) { // merge OPENRTB.user.ext.data.* to XAPI.user.ext.rp.target.* - final ExtUser userExt = user != null ? user.getExt() : null; if (userExt != null) { populateFirstPartyDataAttributes(userExt.getData(), result); } } + private static void enrichWithIabAttribute(ObjectNode target, List data, Set segtaxValues) { + final List iabValue = CollectionUtils.emptyIfNull(data).stream() + .filter(Objects::nonNull) + .filter(dataRecord -> containsSegtaxValue(dataRecord.getExt(), segtaxValues)) + .map(Data::getSegment) + .filter(Objects::nonNull) + .flatMap(segments -> segments.stream() + .map(Segment::getId)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (CollectionUtils.isNotEmpty(iabValue)) { + final ArrayNode iab = target.putArray("iab"); + iabValue.forEach(iab::add); + } + } + + private static boolean containsSegtaxValue(ObjectNode ext, Set segtaxValues) { + final JsonNode taxonomyName = ext != null ? ext.get("segtax") : null; + + return taxonomyName != null && taxonomyName.isInt() && segtaxValues.contains(taxonomyName.intValue()); + } + private static String extractLiverampId(Map> sourceToUserEidExt) { final List liverampEids = MapUtils.emptyIfNull(sourceToUserEidExt).get(LIVERAMP_EID); for (ExtUserEid extUserEid : CollectionUtils.emptyIfNull(liverampEids)) { - final ExtUserEidUid eidUid = extUserEid != null - ? CollectionUtils.emptyIfNull(extUserEid.getUids()).stream().findFirst().orElse(null) - : null; - - final String id = eidUid != null ? eidUid.getId() : null; - if (StringUtils.isNotEmpty(id)) { - return id; - } + return extUserEid.getUids().stream() + .map(ExtUserEidUid::getId) + .filter(StringUtils::isNotEmpty) + .findFirst() + .orElse(null); } return null; } @@ -962,10 +1208,29 @@ private ExtPublisher makePublisherExt(ExtImpRubicon rubiconImpExt) { private ExtSite makeSiteExt(Site site, ExtImpRubicon rubiconImpExt) { final ExtSite extSite = site != null ? site.getExt() : null; final Integer siteExtAmp = extSite != null ? extSite.getAmp() : null; + final Content siteContent = site != null ? site.getContent() : null; + final List siteContentData = siteContent != null ? siteContent.getData() : null; + ObjectNode target = null; + + if (CollectionUtils.isNotEmpty(siteContentData)) { + target = existingRubiconSiteExtRpTargetOrEmptyNode(extSite); + enrichWithIabAttribute(target, siteContentData, SITE_SEGTAXES); + } return mapper.fillExtension( ExtSite.of(siteExtAmp, null), - RubiconSiteExt.of(RubiconSiteExtRp.of(rubiconImpExt.getSiteId()))); + RubiconSiteExt.of(RubiconSiteExtRp.of(rubiconImpExt.getSiteId(), + target != null && !target.isEmpty() ? target : null))); + } + + private ObjectNode existingRubiconSiteExtRpTargetOrEmptyNode(ExtSite siteExt) { + final RubiconSiteExt rubiconSiteExt = siteExt != null + ? mapper.mapper().convertValue(siteExt, RubiconSiteExt.class) + : null; + final RubiconSiteExtRp rubiconSiteExtRp = rubiconSiteExt != null ? rubiconSiteExt.getRp() : null; + final JsonNode target = rubiconSiteExtRp != null ? rubiconSiteExtRp.getTarget() : null; + + return target != null && target.isObject() ? (ObjectNode) target : mapper.mapper().createObjectNode(); } private App makeApp(App app, ExtImpRubicon rubiconImpExt) { @@ -977,7 +1242,7 @@ private App makeApp(App app, ExtImpRubicon rubiconImpExt) { private ExtApp makeAppExt(ExtImpRubicon rubiconImpExt) { return mapper.fillExtension(ExtApp.of(null, null), - RubiconAppExt.of(RubiconSiteExtRp.of(rubiconImpExt.getSiteId()))); + RubiconAppExt.of(RubiconSiteExtRp.of(rubiconImpExt.getSiteId(), null))); } private static Source makeSource(Source source, String pchain) { @@ -988,86 +1253,176 @@ private static Source makeSource(Source source, String pchain) { return source; } - private List extractBids(BidRequest prebidRequest, BidRequest bidRequest, BidResponse bidResponse) { - return bidResponse == null || bidResponse.getSeatbid() == null + private HttpRequest createHttpRequest(BidRequest bidRequest, String uri) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(uri) + .body(mapper.encode(bidRequest)) + .headers(headers) + .payload(bidRequest) + .build(); + } + + private static boolean hasDeals(Imp imp) { + return imp.getPmp() != null && CollectionUtils.isNotEmpty(imp.getPmp().getDeals()); + } + + private List> createDealsRequests(BidRequest bidRequest, String uri) { + final Imp singleImp = bidRequest.getImp().get(0); + return singleImp.getPmp().getDeals().stream() + .map(deal -> mapper.mapper().convertValue(deal.getExt(), ExtDeal.class)) + .filter(Objects::nonNull) + .map(ExtDeal::getLine) + .filter(Objects::nonNull) + .map(lineItem -> createLineItemBidRequest(lineItem, bidRequest, singleImp)) + .map((BidRequest request) -> createHttpRequest(request, uri)) + .collect(Collectors.toList()); + } + + private BidRequest createLineItemBidRequest(ExtDealLine lineItem, BidRequest bidRequest, Imp imp) { + final Imp dealsImp = imp.toBuilder() + .banner(modifyBanner(imp.getBanner(), lineItem.getSizes())) + .ext(modifyRubiconImpExt(imp.getExt(), bidRequest.getExt(), lineItem.getExtLineItemId(), + getAdSlot(imp, extImpContext(imp)))) + .build(); + + return bidRequest.toBuilder() + .imp(Collections.singletonList(dealsImp)) + .build(); + } + + private static Banner modifyBanner(Banner banner, List sizes) { + return CollectionUtils.isEmpty(sizes) || banner == null ? banner : banner.toBuilder().format(sizes).build(); + } + + private ObjectNode modifyRubiconImpExt(ObjectNode impExtNode, ExtRequest extRequest, String extLineItemId, + String adSlot) { + final RubiconImpExt rubiconImpExt = mapper.mapper().convertValue(impExtNode, RubiconImpExt.class); + final RubiconImpExtRp impExtRp = rubiconImpExt.getRp(); + + final ObjectNode targetNode = impExtRp.getTarget() == null || impExtRp.getTarget().isNull() + ? mapper.mapper().createObjectNode() : (ObjectNode) impExtRp.getTarget(); + + final ObjectNode modifiedTargetNode = targetNode.put("line_item", extLineItemId); + final RubiconImpExtRp modifiedImpExtRp = RubiconImpExtRp.of(impExtRp.getZoneId(), modifiedTargetNode, + impExtRp.getTrack()); + + return mapper.mapper().valueToTree(RubiconImpExt.of(modifiedImpExtRp, rubiconImpExt.getViewabilityvendors(), + getMaxBids(extRequest), adSlot)); + } + + private List extractBids(BidRequest prebidRequest, + BidRequest bidRequest, + RubiconBidResponse bidResponse, + List errors) { + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() - : bidsFromResponse(prebidRequest, bidRequest, bidResponse); + : bidsFromResponse(prebidRequest, bidRequest, bidResponse, errors); } - private List bidsFromResponse(BidRequest prebidRequest, BidRequest bidRequest, BidResponse bidResponse) { + private List bidsFromResponse(BidRequest prebidRequest, + BidRequest bidRequest, + RubiconBidResponse bidResponse, + List errors) { final Map idToImp = prebidRequest.getImp().stream() .collect(Collectors.toMap(Imp::getId, Function.identity())); - final Float cmpOverrideFromRequest = cmpOverrideFromRequest(prebidRequest); + final Float cpmOverrideFromRequest = cpmOverrideFromRequest(prebidRequest); final BidType bidType = bidType(bidRequest); return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) - .map(SeatBid::getBid) + .map(seatBid -> updateSeatBids(seatBid, errors)) + .map(RubiconSeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> updateBid(bid, idToImp.get(bid.getImpid()), cmpOverrideFromRequest, bidResponse)) - .filter(RubiconBidder::validatePrice) - .map(bid -> BidderBid.of(bid, bidType, DEFAULT_BID_CURRENCY)) + .map(bid -> updateBid(bid, idToImp.get(bid.getImpid()), cpmOverrideFromRequest, bidResponse)) + .map(bid -> BidderBid.of(bid, bidType, bidResponse.getCur())) .collect(Collectors.toList()); } - private static boolean validatePrice(Bid bid) { - final BigDecimal price = bid.getPrice(); - return bid.getDealid() != null ? price.compareTo(BigDecimal.ZERO) >= 0 : price.compareTo(BigDecimal.ZERO) > 0; + private RubiconSeatBid updateSeatBids(RubiconSeatBid seatBid, List errors) { + final String buyer = seatBid.getBuyer(); + final int networkId = NumberUtils.toInt(buyer, 0); + if (networkId <= 0) { + return seatBid; + } + final List updatedBids = seatBid.getBid().stream() + .map(bid -> insertNetworkIdToMeta(bid, networkId, errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return seatBid.toBuilder().bid(updatedBids).build(); } - private Bid updateBid(Bid bid, Imp imp, Float cmpOverrideFromRequest, BidResponse bidResponse) { + private Bid insertNetworkIdToMeta(Bid bid, int networkId, List errors) { + final ObjectNode bidExt = bid.getExt(); + final ExtPrebid extPrebid; + try { + extPrebid = getExtPrebid(bidExt, bid.getId()); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + final ExtBidPrebid extBidPrebid = extPrebid != null ? extPrebid.getPrebid() : null; + final ObjectNode meta = extBidPrebid != null ? extBidPrebid.getMeta() : null; + + final ObjectNode updatedMeta = meta != null ? meta : mapper.mapper().createObjectNode(); + updatedMeta.set("networkId", IntNode.valueOf(networkId)); + + final ExtBidPrebid modifiedExtBidPrebid = extBidPrebid != null + ? extBidPrebid.toBuilder().meta(updatedMeta).build() + : ExtBidPrebid.builder().meta(updatedMeta).build(); + + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); + updatedBidExt.set(PREBID_EXT, mapper.mapper().valueToTree(modifiedExtBidPrebid)); + + return bid.toBuilder().ext(updatedBidExt).build(); + } + + private ExtPrebid getExtPrebid(ObjectNode bidExt, String bidId) { + try { + return bidExt != null ? mapper.mapper().convertValue(bidExt, EXT_PREBID_TYPE_REFERENCE) : null; + } catch (IllegalArgumentException e) { + throw new PreBidException(String.format("Invalid ext passed in bid with id: %s", bidId)); + } + } + + private Bid updateBid(Bid bid, Imp imp, Float cpmOverrideFromRequest, RubiconBidResponse bidResponse) { + String bidId = bid.getId(); if (generateBidId) { // Since Rubicon XAPI returns openrtb_response.seatbid.bid.id not unique enough // generate new value for it - bid.setId(UUID.randomUUID().toString()); + bidId = UUID.randomUUID().toString(); } else if (Objects.equals(bid.getId(), "0")) { // Since Rubicon XAPI returns only one bid per response // copy bidResponse.bidid to openrtb_response.seatbid.bid.id - bid.setId(bidResponse.getBidid()); + bidId = bidResponse.getBidid(); } // Unconditionally set price if coming from CPM override - final Float cpmOverride = ObjectUtils.defaultIfNull(cpmOverrideFromImp(imp), cmpOverrideFromRequest); - if (cpmOverride != null) { - bid.setPrice(new BigDecimal(String.valueOf(cpmOverride))); - } - - return bid; + final Float cpmOverride = ObjectUtils.defaultIfNull(cpmOverrideFromImp(imp), cpmOverrideFromRequest); + final BigDecimal bidPrice = cpmOverride != null + ? new BigDecimal(String.valueOf(cpmOverride)) + : bid.getPrice(); + + return bid.toBuilder() + .id(bidId) + .price(bidPrice) + .build(); } - private Float cmpOverrideFromRequest(BidRequest bidRequest) { + private Float cpmOverrideFromRequest(BidRequest bidRequest) { final RubiconExtPrebidBiddersBidder bidder = extPrebidBiddersRubicon(bidRequest.getExt()); final RubiconExtPrebidBiddersBidderDebug debug = bidder != null ? bidder.getDebug() : null; return debug != null ? debug.getCpmoverride() : null; } private Float cpmOverrideFromImp(Imp imp) { - final JsonNode extImpPrebidNode = imp.getExt().get(PREBID_EXT); - final ExtImpPrebid prebid = extImpPrebidNode != null ? extImpPrebid(extImpPrebidNode) : null; - final RubiconImpExtPrebidBidder bidder = prebid != null - ? extImpPrebidBidder(prebid.getBidder()) - : null; - final RubiconImpExtPrebidRubiconDebug debug = bidder != null ? bidder.getDebug() : null; + final ExtPrebid extPrebid = imp != null ? parseRubiconExt(imp) : null; + final ExtImpRubicon bidder = extPrebid != null ? extPrebid.getBidder() : null; + final ExtImpRubiconDebug debug = bidder != null ? bidder.getDebug() : null; return debug != null ? debug.getCpmoverride() : null; } - private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { - try { - return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); - } catch (JsonProcessingException e) { - throw new PreBidException(String.format("Error decoding imp.ext.prebid: %s", e.getMessage()), e); - } - } - - private RubiconImpExtPrebidBidder extImpPrebidBidder(ObjectNode extImpPrebidBidder) { - try { - return mapper.mapper().treeToValue(extImpPrebidBidder, RubiconImpExtPrebidBidder.class); - } catch (JsonProcessingException e) { - throw new PreBidException(String.format("Error decoding imp.ext.prebid.bidder: %s", e.getMessage()), e); - } - } - private static BidType bidType(BidRequest bidRequest) { return isVideo(bidRequest.getImp().get(0)) ? BidType.video : BidType.banner; } diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconSize.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconSize.java index d8b9d674bdd..8f08668aef1 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconSize.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconSize.java @@ -101,6 +101,8 @@ final class RubiconSize { SIZES.put(size(320, 500), 278); SIZES.put(size(320, 400), 282); SIZES.put(size(640, 380), 288); + SIZES.put(size(500, 1000), 548); + SIZES.put(size(300, 200), 552); } private final Integer w; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconAppExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconAppExt.java deleted file mode 100644 index 843032b908b..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconAppExt.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class RubiconAppExt { - - RubiconSiteExtRp rp; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconBannerExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconBannerExt.java deleted file mode 100644 index 8fb6f455415..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconBannerExt.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class RubiconBannerExt { - - RubiconBannerExtRp rp; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconDeviceExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconDeviceExt.java deleted file mode 100644 index 8645db08b74..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconDeviceExt.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class RubiconDeviceExt { - - RubiconDeviceExtRp rp; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconExtPrebidBiddersBidderDebug.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconExtPrebidBiddersBidderDebug.java deleted file mode 100644 index 7a55694b70b..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconExtPrebidBiddersBidderDebug.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.Value; - -@Value(staticConstructor = "of") -public class RubiconExtPrebidBiddersBidderDebug { - - Float cpmoverride; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExt.java deleted file mode 100644 index c045c6fde11..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExt.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class RubiconImpExt { - - RubiconImpExtRp rp; - - List viewabilityvendors; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidBidder.java deleted file mode 100644 index 9a8aa550c61..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidBidder.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.Value; - -@Value(staticConstructor = "of") -public class RubiconImpExtPrebidBidder { - - RubiconImpExtPrebidRubiconDebug debug; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidRubiconDebug.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidRubiconDebug.java deleted file mode 100644 index 7f3c601f1e0..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtPrebidRubiconDebug.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.Value; - -@Value(staticConstructor = "of") -public class RubiconImpExtPrebidRubiconDebug { - - Float cpmoverride; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconPubExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconPubExt.java deleted file mode 100644 index affe339349b..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconPubExt.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class RubiconPubExt { - - RubiconPubExtRp rp; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconPubExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconPubExtRp.java deleted file mode 100644 index 81c4b12069a..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconPubExtRp.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class RubiconPubExtRp { - - Integer accountId; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconSiteExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconSiteExt.java deleted file mode 100644 index 43a8df6ac82..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconSiteExt.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.Value; - -@Value(staticConstructor = "of") -public class RubiconSiteExt { - - RubiconSiteExtRp rp; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconSiteExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconSiteExtRp.java deleted file mode 100644 index d96a1680103..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconSiteExtRp.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class RubiconSiteExtRp { - - Integer siteId; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconUserExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconUserExtRp.java deleted file mode 100644 index fe5e6dbc37b..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconUserExtRp.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Value; - -@Value(staticConstructor = "of") -public class RubiconUserExtRp { - - JsonNode target; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconVideoExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconVideoExtRp.java deleted file mode 100644 index 8e102fbd51d..00000000000 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconVideoExtRp.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.rubicon.proto; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class RubiconVideoExtRp { - - Integer sizeId; -} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconAppExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconAppExt.java new file mode 100644 index 00000000000..64460e5a725 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconAppExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class RubiconAppExt { + + RubiconSiteExtRp rp; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExt.java new file mode 100644 index 00000000000..146d302d7b1 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class RubiconBannerExt { + + RubiconBannerExtRp rp; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconBannerExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExtRp.java similarity index 80% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconBannerExtRp.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExtRp.java index cc64eba1e49..eb082f99c29 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconBannerExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExtRp.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import lombok.AllArgsConstructor; import lombok.Value; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExt.java new file mode 100644 index 00000000000..bfc6c1c6f2b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class RubiconDeviceExt { + + RubiconDeviceExtRp rp; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconDeviceExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExtRp.java similarity index 77% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconDeviceExtRp.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExtRp.java index 6d7a6402866..35ddea95331 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconDeviceExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExtRp.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import lombok.AllArgsConstructor; import lombok.Value; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconExtPrebidBidders.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBidders.java similarity index 76% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconExtPrebidBidders.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBidders.java index 42ed64aa6da..786ca0f2a1d 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconExtPrebidBidders.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBidders.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import lombok.AllArgsConstructor; import lombok.Value; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconExtPrebidBiddersBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBiddersBidder.java similarity index 79% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconExtPrebidBiddersBidder.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBiddersBidder.java index 4b375403dd0..059c7ac7439 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconExtPrebidBiddersBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBiddersBidder.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import lombok.AllArgsConstructor; import lombok.Value; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBiddersBidderDebug.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBiddersBidderDebug.java new file mode 100644 index 00000000000..0947162a9f9 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBiddersBidderDebug.java @@ -0,0 +1,9 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class RubiconExtPrebidBiddersBidderDebug { + + Float cpmoverride; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java new file mode 100644 index 00000000000..28b96274ebf --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java @@ -0,0 +1,19 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value +public class RubiconImpExt { + + RubiconImpExtRp rp; + + List viewabilityvendors; + + Integer maxbids; + + String gpid; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRp.java similarity index 82% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtRp.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRp.java index 14c200a3f53..bc6f3e6795d 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRp.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtRpTrack.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRpTrack.java similarity index 76% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtRpTrack.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRpTrack.java index 10bdcd49c88..8902cf6aefd 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconImpExtRpTrack.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRpTrack.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import lombok.AllArgsConstructor; import lombok.Value; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconParams.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconParams.java similarity index 90% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconParams.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconParams.java index 3daedc4b8f1..97c66909d55 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconParams.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconParams.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExt.java new file mode 100644 index 00000000000..af3026a696d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class RubiconPubExt { + + RubiconPubExtRp rp; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExtRp.java new file mode 100644 index 00000000000..d1b92192638 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExtRp.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class RubiconPubExtRp { + + Integer accountId; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconSiteExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconSiteExt.java new file mode 100644 index 00000000000..ba3bd531196 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconSiteExt.java @@ -0,0 +1,9 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class RubiconSiteExt { + + RubiconSiteExtRp rp; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconSiteExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconSiteExtRp.java new file mode 100644 index 00000000000..7cda21ef7ef --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconSiteExtRp.java @@ -0,0 +1,14 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class RubiconSiteExtRp { + + Integer siteId; + + JsonNode target; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconTargeting.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargeting.java similarity index 78% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconTargeting.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargeting.java index 96c9944b453..ece72cb4198 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconTargeting.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargeting.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import lombok.AllArgsConstructor; import lombok.Value; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconTargetingExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExt.java similarity index 75% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconTargetingExt.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExt.java index 683297d5ebf..f1f1878d721 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconTargetingExt.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExt.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import lombok.AllArgsConstructor; import lombok.Value; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconTargetingExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExtRp.java similarity index 78% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconTargetingExtRp.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExtRp.java index 63072fa3f2b..65bcf0d58e0 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconTargetingExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExtRp.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import lombok.AllArgsConstructor; import lombok.Value; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconUserExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconUserExt.java similarity index 83% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconUserExt.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconUserExt.java index 8f66bd573b5..a7274d21bd6 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconUserExt.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconUserExt.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import lombok.Builder; import lombok.Value; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconUserExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconUserExtRp.java new file mode 100644 index 00000000000..a3f16f3a5f2 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconUserExtRp.java @@ -0,0 +1,10 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class RubiconUserExtRp { + + JsonNode target; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconVideoExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExt.java similarity index 80% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconVideoExt.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExt.java index 3dd3a47a4b1..1d545488b4e 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconVideoExt.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExt.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import lombok.AllArgsConstructor; import lombok.Value; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExtRp.java new file mode 100644 index 00000000000..88b81e54932 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExtRp.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.rubicon.proto.request; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class RubiconVideoExtRp { + + Integer sizeId; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconVideoParams.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoParams.java similarity index 86% rename from src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconVideoParams.java rename to src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoParams.java index 009aab9d0ea..8bfdf0aaeb3 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/RubiconVideoParams.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoParams.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.rubicon.proto; +package org.prebid.server.bidder.rubicon.proto.request; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/response/RubiconBidResponse.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/response/RubiconBidResponse.java new file mode 100644 index 00000000000..23317236057 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/response/RubiconBidResponse.java @@ -0,0 +1,26 @@ +package org.prebid.server.bidder.rubicon.proto.response; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder +@Value +public class RubiconBidResponse { + + String id; + + List seatbid; + + String bidid; + + String cur; + + String customdata; + + Integer nbr; + + ObjectNode ext; +} diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/response/RubiconSeatBid.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/response/RubiconSeatBid.java new file mode 100644 index 00000000000..e79c02dc445 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/response/RubiconSeatBid.java @@ -0,0 +1,23 @@ +package org.prebid.server.bidder.rubicon.proto.response; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder(toBuilder = true) +@Value +public class RubiconSeatBid { + + List bid; + + String seat; + + int group; + + String buyer; + + ObjectNode ext; +} diff --git a/src/main/java/org/prebid/server/bidder/salunamedia/SaLunamediaBidder.java b/src/main/java/org/prebid/server/bidder/salunamedia/SaLunamediaBidder.java new file mode 100644 index 00000000000..162dbd4280e --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/salunamedia/SaLunamediaBidder.java @@ -0,0 +1,88 @@ +package org.prebid.server.bidder.salunamedia; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * SaLunamedia {@link Bidder} implementation + */ +public class SaLunamediaBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public SaLunamediaBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + return Result.withValue(HttpRequest.builder() + .uri(endpointUrl) + .method(HttpMethod.POST) + .headers(HttpUtil.headers()) + .payload(request) + .body(mapper.encode(request)) + .build()); + } + + @Override + public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse), Collections.emptyList()); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse) { + final List seatBids = bidResponse != null ? bidResponse.getSeatbid() : null; + if (CollectionUtils.isEmpty(seatBids)) { + throw new PreBidException("Empty SeatBid"); + } + + final SeatBid firstSeatBid = seatBids.get(0); + final List bids = firstSeatBid != null ? firstSeatBid.getBid() : null; + if (CollectionUtils.isEmpty(bids)) { + throw new PreBidException("Empty SeatBid.Bids"); + } + + final Bid firstBid = bids.get(0); + final ObjectNode firstBidExt = firstBid != null ? firstBid.getExt() : null; + if (firstBidExt == null) { + throw new PreBidException("Missing BidExt"); + } + + return Collections.singletonList(BidderBid.of(firstBid, getBidType(firstBidExt), bidResponse.getCur())); + } + + private BidType getBidType(ObjectNode bidExt) { + try { + return mapper.mapper().convertValue(bidExt.get("mediaType"), BidType.class); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughBidder.java b/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughBidder.java index 589e31b6638..af8f15fc95d 100644 --- a/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughBidder.java +++ b/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughBidder.java @@ -1,7 +1,6 @@ package org.prebid.server.bidder.sharethrough; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; @@ -25,6 +24,7 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.sharethrough.ExtData; import org.prebid.server.proto.openrtb.ext.request.sharethrough.ExtImpSharethrough; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -35,7 +35,6 @@ import java.util.Collections; import java.util.Date; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -64,18 +63,13 @@ public SharethroughBidder(String endpointUrl, JacksonMapper mapper) { this.requestUtil = new SharethroughRequestUtil(); } - /** - * Makes the HTTP requests which should be made to fetch bids. - *

- * Creates POST http request with all parameters in url and headers with empty body. - */ @Override public Result>> makeHttpRequests(BidRequest request) { final String page = requestUtil.getPage(request.getSite()); // site.page validation is already performed by {@link RequestValidator#validate} if (page == null) { - return Result.emptyWithError(BidderError.badInput("site.page is required")); + return Result.withError(BidderError.badInput("site.page is required")); } final boolean test = Objects.equals(request.getTest(), 1); @@ -85,15 +79,17 @@ public Result>> makeHttpRequests(BidRe try { strUriParameters = parseBidRequestToUriParameters(request, date, test); } catch (IllegalArgumentException e) { - return Result.emptyWithError(BidderError.badInput( + return Result.withError(BidderError.badInput( String.format("Error occurred parsing sharethrough parameters %s", e.getMessage()))); } final MultiMap headers = makeHeaders(request.getDevice(), page); + + final List errors = new ArrayList<>(); final List> httpRequests = strUriParameters.stream() - .map(strUriParameter -> makeHttpRequest(headers, date, strUriParameter)) + .map(strUriParameter -> makeHttpRequest(headers, date, strUriParameter, errors)) + .filter(Objects::nonNull) .collect(Collectors.toList()); - - return Result.of(httpRequests, Collections.emptyList()); + return Result.of(httpRequests, errors); } /** @@ -130,19 +126,22 @@ private List parseBidRequestToUriParameters(BidRequest request /** * Populate {@link StrUriParameters} with publisher request, imp, imp.ext values. */ - private StrUriParameters createStrUriParameters(ExtImpSharethrough extImpStr, Imp imp, boolean isConsentRequired, + private StrUriParameters createStrUriParameters(ExtImpSharethrough extImpSharethrough, Imp imp, + boolean isConsentRequired, String consentString, String usPrivacy, boolean canBrowserAutoPlayVideo, String ttdUid, String buyeruid, SharethroughRequestBody body) { - final Size size = requestUtil.getSize(imp, extImpStr); + final Size size = requestUtil.getSize(imp, extImpSharethrough); + final ExtData extData = extImpSharethrough.getData(); return StrUriParameters.builder() - .pkey(extImpStr.getPkey()) + .pkey(extImpSharethrough.getPkey()) .bidID(imp.getId()) + .gpid(extData != null ? extData.getPbAdSlot() : null) .consentRequired(isConsentRequired) .consentString(consentString) .usPrivacySignal(usPrivacy) .instantPlayCapable(canBrowserAutoPlayVideo) - .iframe(extImpStr.getIframe()) + .iframe(extImpSharethrough.getIframe()) .height(size.getHeight()) .width(size.getWidth()) .theTradeDeskUserId(ttdUid) @@ -174,17 +173,24 @@ private MultiMap makeHeaders(Device device, String page) { * Make {@link HttpRequest} from uri and headers. */ private HttpRequest makeHttpRequest( - MultiMap headers, Date date, StrUriParameters strUriParameter) { + MultiMap headers, Date date, StrUriParameters strUriParameter, List errors) { + + final String uri; + try { + uri = SharethroughUriBuilderUtil.buildSharethroughUrl( + endpointUrl, SUPPLY_ID, VERSION, DATE_FORMAT.format(date), strUriParameter); + } catch (IllegalArgumentException e) { + errors.add(BidderError.badInput(e.getMessage())); + return null; + } - final String uri = SharethroughUriBuilderUtil.buildSharethroughUrl( - endpointUrl, SUPPLY_ID, VERSION, DATE_FORMAT.format(date), strUriParameter); final SharethroughRequestBody body = strUriParameter.getBody(); return HttpRequest.builder() .method(HttpMethod.POST) .uri(uri) - .body(mapper.encode(body)) .headers(headers) + .body(mapper.encode(body)) .payload(body) .build(); } @@ -195,10 +201,9 @@ public Result> makeBids(HttpCall httpCa final String responseBody = httpCall.getResponse().getBody(); final ExtImpSharethroughResponse sharethroughBid = mapper.mapper().readValue(responseBody, ExtImpSharethroughResponse.class); - return Result.of(toBidderBid(responseBody, sharethroughBid, httpCall.getRequest()), - Collections.emptyList()); + return Result.withValues(toBidderBid(responseBody, sharethroughBid, httpCall.getRequest())); } catch (IOException | IllegalArgumentException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } @@ -236,9 +241,4 @@ private List toBidderBid(String responseBody, ExtImpSharethroughRespo DEFAULT_BID_TYPE, DEFAULT_BID_CURRENCY)); } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtil.java b/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtil.java index 9d814a3670d..5b41dbb9e7a 100644 --- a/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtil.java +++ b/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtil.java @@ -25,7 +25,7 @@ class SharethroughRequestUtil { private static final int MIN_SAFARI_VERSION = 10; /** - * Retrieves size from imp.ext.sharethrough.iframeSize or from im.banner.format. + * Retrieves size from iframeSize or from imp.banner.format. */ Size getSize(Imp imp, ExtImpSharethrough extImpSharethrough) { final List iframeSize = extImpSharethrough.getIframeSize(); diff --git a/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughUriBuilderUtil.java b/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughUriBuilderUtil.java index c979fb8796b..f5505e10c15 100644 --- a/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughUriBuilderUtil.java +++ b/src/main/java/org/prebid/server/bidder/sharethrough/SharethroughUriBuilderUtil.java @@ -15,12 +15,19 @@ private SharethroughUriBuilderUtil() { } /** - * Creates uri with parameters for sharethrough request + * Creates uri with parameters for Sharethrough request. */ static String buildSharethroughUrl(String baseUri, String supplyId, String strVersion, String formattedDate, StrUriParameters params) { - final URIBuilder uriBuilder = new URIBuilder() - .setPath(baseUri) + + final URIBuilder uriBuilder; + try { + uriBuilder = new URIBuilder(baseUri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(String.format("Invalid url: %s, error: %s", baseUri, e.getMessage())); + } + + uriBuilder .addParameter("placement_key", params.getPkey()) .addParameter("bidId", params.getBidID()) .addParameter("consent_required", getBooleanStringValue(params.getConsentRequired())) @@ -42,6 +49,10 @@ static String buildSharethroughUrl(String baseUri, String supplyId, String strVe if (StringUtils.isNotBlank(stxuid)) { uriBuilder.addParameter("stxuid", stxuid); } + final String gpid = params.getGpid(); + if (StringUtils.isNotBlank(gpid)) { + uriBuilder.addParameter("gpid", gpid); + } return uriBuilder.toString(); } @@ -51,7 +62,7 @@ private static String getBooleanStringValue(Boolean bool) { } /** - * Creates uri with parameters for sharethrough request + * Creates uri with parameters for Sharethrough request. */ static StrUriParameters buildSharethroughUrlParameters(String uri) { try { @@ -67,7 +78,6 @@ static StrUriParameters buildSharethroughUrlParameters(String uri) { .bidID(getValueByKey(queryParams, "bidId")) .consentString(getValueByKey(queryParams, "consent_string")) .build(); - } catch (URISyntaxException e) { throw new IllegalArgumentException("Cant resolve uri: " + uri, e); } @@ -99,4 +109,3 @@ private static String getValueByKey(List nameValuePairs, String k .orElse(""); } } - diff --git a/src/main/java/org/prebid/server/bidder/sharethrough/model/StrUriParameters.java b/src/main/java/org/prebid/server/bidder/sharethrough/model/StrUriParameters.java index e4b1d4089af..76a784657ba 100644 --- a/src/main/java/org/prebid/server/bidder/sharethrough/model/StrUriParameters.java +++ b/src/main/java/org/prebid/server/bidder/sharethrough/model/StrUriParameters.java @@ -11,6 +11,8 @@ public class StrUriParameters { String bidID; + String gpid; + Boolean consentRequired; String consentString; diff --git a/src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java b/src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java new file mode 100644 index 00000000000..b0a668e8eec --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java @@ -0,0 +1,169 @@ +package org.prebid.server.bidder.silvermob; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.silvermob.ExtImpSilvermob; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Silvermob {@link Bidder} implementation. + */ +public class SilvermobBidder implements Bidder { + + private static final TypeReference> SILVERMOB_EXT_TYPE_REFERENCE = + new TypeReference>() { + }; + + private static final String URL_HOST_MACRO = "{{Host}}"; + private static final String URL_ZONE_ID_MACRO = "{{ZoneID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public SilvermobBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List> requests = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + requests.add(createRequestForImp(imp, request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private HttpRequest createRequestForImp(Imp imp, BidRequest request) { + final ExtImpSilvermob extImp = parseImpExt(imp); + + final BidRequest outgoingRequest = request.toBuilder() + .imp(Collections.singletonList(imp)) + .build(); + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(resolveEndpoint(extImp)) + .headers(resolveHeaders(request.getDevice())) + .payload(outgoingRequest) + .body(mapper.encode(outgoingRequest)) + .build(); + } + + private ExtImpSilvermob parseImpExt(Imp imp) { + final ExtImpSilvermob extImp; + try { + extImp = mapper.mapper().convertValue(imp.getExt(), SILVERMOB_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(String.format("error unmarshalling imp.ext.bidder: %s", e.getMessage())); + } + if (StringUtils.isBlank(extImp.getHost())) { + throw new PreBidException("host is a required silvermob ext.imp param"); + } + + if (StringUtils.isBlank(extImp.getZoneId())) { + throw new PreBidException("zoneId is a required silvermob ext.imp param"); + } + return extImp; + } + + private String resolveEndpoint(ExtImpSilvermob extImp) { + return endpointUrl + .replace(URL_HOST_MACRO, extImp.getHost()) + .replace(URL_ZONE_ID_MACRO, HttpUtil.encodeUrl(extImp.getZoneId())); + } + + private static MultiMap resolveHeaders(Device device) { + final MultiMap headers = HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + } + + return headers; + } + + @Override + public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + return Result.of(extractBids(httpCall), Collections.emptyList()); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(HttpCall httpCall) { + final BidResponse bidResponse; + try { + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + throw new PreBidException(String.format("Error unmarshalling server Response: %s", e.getMessage())); + } + if (bidResponse == null) { + throw new PreBidException("Response in not present"); + } + if (CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("Empty SeatBid array"); + } + return bidsFromResponse(bidResponse, httpCall.getRequest().getPayload()); + } + + private static List bidsFromResponse(BidResponse bidResponse, BidRequest bidRequest) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getVideo() != null) { + return BidType.video; + } + if (imp.getXNative() != null) { + return BidType.xNative; + } + } + } + return BidType.banner; + } +} diff --git a/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java b/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java index b3a3e234eb2..7321e257b80 100644 --- a/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java +++ b/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; @@ -9,20 +11,23 @@ import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.User; +import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.Endpoint; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.smaato.proto.SmaatoBidExt; import org.prebid.server.bidder.smaato.proto.SmaatoBidRequestExt; import org.prebid.server.bidder.smaato.proto.SmaatoImage; import org.prebid.server.bidder.smaato.proto.SmaatoImageAd; @@ -37,164 +42,117 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidPbs; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.smaato.ExtImpSmaato; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.util.HttpUtil; +import java.time.Clock; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.IntStream; public class SmaatoBidder implements Bidder { private static final TypeReference> SMAATO_EXT_TYPE_REFERENCE = new TypeReference>() { }; - - private static final String CLIENT_VERSION = "prebid_server_0.1"; + private static final String CLIENT_VERSION = "prebid_server_0.4"; + private static final String SMT_ADTYPE_HEADER = "X-Smt-Adtype"; + private static final String SMT_EXPIRES_HEADER = "X-Smt-Expires"; private static final String SMT_AD_TYPE_IMG = "Img"; private static final String SMT_ADTYPE_RICHMEDIA = "Richmedia"; private static final String SMT_ADTYPE_VIDEO = "Video"; + private static final int DEFAULT_TTL = 300; + private final String endpointUrl; private final JacksonMapper mapper; + private final Clock clock; - public SmaatoBidder(String endpointUrl, JacksonMapper mapper) { + public SmaatoBidder(String endpointUrl, JacksonMapper mapper, Clock clock) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); + this.clock = Objects.requireNonNull(clock); } @Override public Result>> makeHttpRequests(BidRequest request) { - final List errors = new ArrayList<>(); - final List imps = new ArrayList<>(); - - String firstPublisherId = null; - for (Imp imp : request.getImp()) { - try { - final ExtImpSmaato extImpSmaato = parseImpExt(imp); - firstPublisherId = firstPublisherId == null ? extImpSmaato.getPublisherId() : firstPublisherId; - final Imp modifiedImp = modifyImp(imp, extImpSmaato.getAdspaceId()); - imps.add(modifiedImp); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - } - } - - final Site modifiedSite; - final User modifiedUser; + final BidRequest enrichedRequest; try { - modifiedSite = request.getSite() != null ? modifySite(request.getSite(), firstPublisherId) : null; - modifiedUser = request.getUser() != null ? modifyUser(request.getUser()) : null; + enrichedRequest = enrichRequestWithCommonProperties(request); } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - return Result.of(Collections.emptyList(), errors); - } - - final BidRequest outgoingRequest = request.toBuilder() - .imp(imps) - .site(modifiedSite) - .user(modifiedUser) - .ext(mapper.fillExtension(ExtRequest.empty(), SmaatoBidRequestExt.of(CLIENT_VERSION))) - .build(); - - return Result.of(Collections.singletonList( - HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(HttpUtil.headers()) - .payload(outgoingRequest) - .body(mapper.encode(outgoingRequest)) - .build()), - errors); - } - - private ExtImpSmaato parseImpExt(Imp imp) { - try { - return mapper.mapper().convertValue(imp.getExt(), SMAATO_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); - } - } - - private static Imp modifyImp(Imp imp, String adspaceId) { - final Imp.ImpBuilder impBuilder = imp.toBuilder(); - if (imp.getBanner() != null) { - return impBuilder.banner(modifyBanner(imp.getBanner())).tagid(adspaceId).ext(null).build(); - } - - if (imp.getVideo() != null) { - return impBuilder.tagid(adspaceId).ext(null).build(); + return Result.withError(BidderError.badInput(e.getMessage())); } - throw new PreBidException(String.format( - "invalid MediaType. SMAATO only supports Banner and Video. Ignoring ImpID=%s", imp.getId())); - } - private static Banner modifyBanner(Banner banner) { - if (banner.getW() != null && banner.getH() != null) { - return banner; - } - final List format = banner.getFormat(); - if (CollectionUtils.isEmpty(format)) { - throw new PreBidException(String.format("No sizes provided for Banner %s", format)); + final List errors = new ArrayList<>(); + if (isVideoRequest(request)) { + return Result.of(constructPodRequests(enrichedRequest, errors), errors); } - final Format firstFormat = format.get(0); - return banner.toBuilder().w(firstFormat.getW()).h(firstFormat.getH()).build(); + return Result.of(constructIndividualRequests(enrichedRequest, errors), errors); } - private Site modifySite(Site site, String firstPublisherId) { - final Site.SiteBuilder siteBuilder = site.toBuilder() - .publisher(Publisher.builder().id(firstPublisherId).build()); - - final ExtSite siteExt = site.getExt(); - if (siteExt != null) { - final SmaatoSiteExtData data = convertExt(siteExt.getData(), SmaatoSiteExtData.class); - final String keywords = data != null ? data.getKeywords() : null; - siteBuilder.keywords(keywords).ext(null); - } - - return siteBuilder.build(); + private BidRequest enrichRequestWithCommonProperties(BidRequest bidRequest) { + return bidRequest.toBuilder() + .user(modifyUser(bidRequest.getUser())) + .site(modifySite(bidRequest.getSite())) + .ext(mapper.fillExtension(ExtRequest.empty(), SmaatoBidRequestExt.of(CLIENT_VERSION))) + .build(); } private User modifyUser(User user) { - final ExtUser ext = user.getExt(); - if (ext == null) { + final ExtUser userExt = getIfNotNull(user, User::getExt); + if (userExt == null) { return user; } - final ObjectNode extDataNode = ext.getData(); - if (extDataNode == null) { + final ObjectNode extDataNode = userExt.getData(); + if (extDataNode == null || extDataNode.isEmpty()) { return user; } + final SmaatoUserExtData smaatoUserExtData = convertExt(extDataNode, SmaatoUserExtData.class); final User.UserBuilder userBuilder = user.toBuilder(); - final SmaatoUserExtData data = convertExt(extDataNode, SmaatoUserExtData.class); - final String gender = data != null ? data.getGender() : null; + final String gender = smaatoUserExtData.getGender(); if (StringUtils.isNotBlank(gender)) { userBuilder.gender(gender); } - final Integer yob = data != null ? data.getYob() : null; + final Integer yob = smaatoUserExtData.getYob(); if (yob != null && yob != 0) { userBuilder.yob(yob); } - final String keywords = data != null ? data.getKeywords() : null; + final String keywords = smaatoUserExtData.getKeywords(); if (StringUtils.isNotBlank(keywords)) { userBuilder.keywords(keywords); } - userBuilder.ext(ext.toBuilder().data(null).build()); + return userBuilder + .ext(userExt.toBuilder().data(null).build()) + .build(); + } - return userBuilder.build(); + private Site modifySite(Site site) { + final ExtSite siteExt = getIfNotNull(site, Site::getExt); + if (siteExt != null) { + final SmaatoSiteExtData data = convertExt(siteExt.getData(), SmaatoSiteExtData.class); + final String keywords = getIfNotNull(data, SmaatoSiteExtData::getKeywords); + return Site.builder().keywords(keywords).ext(null).build(); + } + return site; } private T convertExt(ObjectNode ext, Class className) { @@ -205,24 +163,176 @@ private T convertExt(ObjectNode ext, Class className) { } } - @Override - public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); + private static boolean isVideoRequest(BidRequest bidRequest) { + final ExtRequestPrebid prebid = getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); + final ExtRequestPrebidPbs pbs = getIfNotNull(prebid, ExtRequestPrebid::getPbs); + final String endpointName = getIfNotNull(pbs, ExtRequestPrebidPbs::getEndpoint); + + return StringUtils.equals(endpointName, Endpoint.openrtb2_video.value()); + } + + private List> constructPodRequests(BidRequest bidRequest, List errors) { + final List validImps = new ArrayList<>(); + for (Imp imp : bidRequest.getImp()) { + if (imp.getVideo() == null) { + errors.add(BidderError.badInput("Invalid MediaType. Smaato only supports Video for AdPod.")); + continue; + } + validImps.add(imp); + } + + return validImps.stream() + .collect(Collectors.groupingBy(SmaatoBidder::extractPod, Collectors.toList())) + .values().stream() + .map(impsPod -> preparePodRequest(bidRequest, impsPod, errors)) + .filter(Objects::nonNull) + .map(this::constructHttpRequest) + .collect(Collectors.toList()); + } + + private static String extractPod(Imp imp) { + return imp.getId().split("_")[0]; + } + + private BidRequest preparePodRequest(BidRequest bidRequest, List imps, List errors) { + try { + final ExtImpSmaato extImpSmaato = mapper.mapper().convertValue(imps.get(0).getExt(), + SMAATO_EXT_TYPE_REFERENCE).getBidder(); + + final String publisherId = getIfNotNullOrThrow(extImpSmaato, ExtImpSmaato::getPublisherId, "publisherId"); + final String adBreakId = getIfNotNullOrThrow(extImpSmaato, ExtImpSmaato::getAdbreakId, "adbreakId"); + + return modifyBidRequest(bidRequest, publisherId, () -> modifyImpsForAdBreak(imps, adBreakId)); + } catch (PreBidException | IllegalArgumentException e) { + errors.add(BidderError.badInput(e.getMessage())); + return null; + } + } + + private BidRequest modifyBidRequest(BidRequest bidRequest, String publisherId, Supplier> impSupplier) { + final Publisher publisher = Publisher.builder().id(publisherId).build(); + final Site site = bidRequest.getSite(); + final App app = bidRequest.getApp(); + + final BidRequest.BidRequestBuilder bidRequestBuilder = bidRequest.toBuilder(); + if (site != null) { + bidRequestBuilder.site(site.toBuilder().publisher(publisher).build()); + } else if (app != null) { + bidRequestBuilder.app(app.toBuilder().publisher(publisher).build()); + } else { + throw new PreBidException("Missing Site/App."); + } + + return bidRequestBuilder.imp(impSupplier.get()).build(); + } + + private List modifyImpsForAdBreak(List imps, String adBreakId) { + return IntStream.range(0, imps.size()) + .mapToObj(idx -> modifyImpForAdBreak(imps.get(idx), idx + 1, adBreakId)) + .collect(Collectors.toList()); + } + + private Imp modifyImpForAdBreak(Imp imp, Integer sequence, String adBreakId) { + final Video modifiedVideo = imp.getVideo().toBuilder() + .sequence(sequence) + .ext(mapper.mapper().createObjectNode().set("context", TextNode.valueOf("adpod"))) + .build(); + return imp.toBuilder() + .tagid(adBreakId) + .video(modifiedVideo) + .ext(null) + .build(); + } + + private List> constructIndividualRequests(BidRequest bidRequest, List errors) { + return splitImps(bidRequest.getImp(), errors).stream() + .map(imp -> prepareIndividualRequest(bidRequest, imp, errors)) + .filter(Objects::nonNull) + .map(this::constructHttpRequest) + .collect(Collectors.toList()); + } + + private List splitImps(List imps, List errors) { + final List splitImps = new ArrayList<>(); + + for (Imp imp : imps) { + final Banner banner = imp.getBanner(); + final Video video = imp.getVideo(); + if (video == null && banner == null) { + errors.add(BidderError.badInput("Invalid MediaType. Smaato only supports Banner and Video.")); + continue; + } + + if (video != null) { + splitImps.add(imp.toBuilder().banner(null).build()); + } + if (banner != null) { + splitImps.add(imp.toBuilder().video(null).build()); + } + } + + return splitImps; + } + + private BidRequest prepareIndividualRequest(BidRequest bidRequest, Imp imp, List errors) { + try { + final ExtImpSmaato extImpSmaato = mapper.mapper().convertValue(imp.getExt(), + SMAATO_EXT_TYPE_REFERENCE).getBidder(); + final String publisherId = getIfNotNullOrThrow(extImpSmaato, ExtImpSmaato::getPublisherId, "publisherId"); + final String adSpaceId = getIfNotNullOrThrow(extImpSmaato, ExtImpSmaato::getAdspaceId, "adspaceId"); + + return modifyBidRequest(bidRequest, publisherId, () -> modifyImpForAdSpace(imp, adSpaceId)); + } catch (PreBidException | IllegalArgumentException e) { + errors.add(BidderError.badInput(e.getMessage())); + return null; + } + } + + private List modifyImpForAdSpace(Imp imp, String adSpaceId) { + final Imp modifiedImp = imp.toBuilder() + .tagid(adSpaceId) + .banner(getIfNotNull(imp.getBanner(), SmaatoBidder::modifyBanner)) + .ext(null) + .build(); + + return Collections.singletonList(modifiedImp); + } + + private static Banner modifyBanner(Banner banner) { + if (banner.getW() != null && banner.getH() != null) { + return banner; } + final List format = banner.getFormat(); + if (CollectionUtils.isEmpty(format)) { + throw new PreBidException("No sizes provided for Banner."); + } + final Format firstFormat = format.get(0); + return banner.toBuilder().w(firstFormat.getW()).h(firstFormat.getH()).build(); + } + private HttpRequest constructHttpRequest(BidRequest bidRequest) { + return HttpRequest.builder() + .uri(endpointUrl) + .method(HttpMethod.POST) + .headers(HttpUtil.headers()) + .body(mapper.encode(bidRequest)) + .payload(bidRequest) + .build(); + } + + @Override + public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); return extractBids(bidResponse, httpCall.getResponse().getHeaders()); - } catch (DecodeException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + } catch (PreBidException | DecodeException e) { + return Result.withError(BidderError.badInput(e.getMessage())); } } private Result> extractBids(BidResponse bidResponse, MultiMap headers) { - if (bidResponse == null || bidResponse.getSeatbid() == null) { - return Result.of(Collections.emptyList(), Collections.emptyList()); + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Result.empty(); } final List errors = new ArrayList<>(); @@ -234,35 +344,74 @@ private Result> extractBids(BidResponse bidResponse, MultiMap he .map(bid -> bidderBid(bid, bidResponse.getCur(), headers, errors)) .filter(Objects::nonNull) .collect(Collectors.toList()); + return Result.of(bidderBids, errors); } private BidderBid bidderBid(Bid bid, String currency, MultiMap headers, List errors) { try { - final String markupType = getAdMarkupType(headers, bid.getAdm()); - final Bid updateBid = bid.toBuilder().adm(renderAdMarkup(markupType, bid.getAdm())).build(); - return BidderBid.of(updateBid, getBidType(markupType), currency); + final String bidAdm = bid.getAdm(); + if (StringUtils.isBlank(bidAdm)) { + throw new PreBidException(String.format("Empty ad markup in bid with id: %s", bid.getId())); + } + final String markupType = getAdMarkupType(headers, bidAdm); + final BidType bidType = getBidType(markupType); + final Bid updatedBid = bid.toBuilder() + .adm(renderAdMarkup(markupType, bidAdm)) + .exp(getTtl(headers)) + .ext(buildExtPrebid(bid, bidType)) + .build(); + return BidderBid.of(updatedBid, bidType, currency); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); return null; } } + private ObjectNode buildExtPrebid(Bid bid, BidType bidType) { + final ExtBidPrebidVideo extBidPrebidVideo = getExtBidPrebidVideo(bid, bidType); + final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().video(extBidPrebidVideo).build(); + return mapper.mapper().valueToTree(ExtPrebid.of(extBidPrebid, null)); + } + + private ExtBidPrebidVideo getExtBidPrebidVideo(Bid bid, BidType bidType) { + final ObjectNode bidExt = bid.getExt(); + if (bidType != BidType.video || bidExt == null) { + return null; + } + + final List categories = bid.getCat(); + final String primaryCategory = CollectionUtils.isNotEmpty(categories) ? categories.get(0) : null; + try { + final SmaatoBidExt smaatoBidExt = mapper.mapper().convertValue(bidExt, SmaatoBidExt.class); + return ExtBidPrebidVideo.of(smaatoBidExt.getDuration(), primaryCategory); + } catch (IllegalArgumentException e) { + throw new PreBidException("Invalid bid.ext."); + } + } + + private int getTtl(MultiMap headers) { + try { + final long expiresAtMillis = Long.parseLong(headers.get(SMT_EXPIRES_HEADER)); + final long currentTimeMillis = clock.millis(); + return (int) Math.max((expiresAtMillis - currentTimeMillis) / 1000, 0); + } catch (NumberFormatException e) { + return DEFAULT_TTL; + } + } + private static String getAdMarkupType(MultiMap headers, String adm) { - final String adMarkupType = headers.get("X-SMT-ADTYPE"); + final String adMarkupType = headers.get(SMT_ADTYPE_HEADER); if (StringUtils.isNotBlank(adMarkupType)) { return adMarkupType; - } - if (adm.startsWith("image")) { + } else if (adm.startsWith("{\"image\":")) { return SMT_AD_TYPE_IMG; - } - if (adm.startsWith("richmedia")) { + } else if (adm.startsWith("{\"richmedia\":")) { return SMT_ADTYPE_RICHMEDIA; - } - if (adm.startsWith(" clickEvent.append(String.format( "fetch(decodeURIComponent('%s'.replace(/\\+/g, ' ')), {cache: 'no-cache'});", - HttpUtil.encodeUrl(tracker)))); + HttpUtil.encodeUrl(StringUtils.stripToEmpty(tracker))))); final StringBuilder impressionTracker = new StringBuilder(); CollectionUtils.emptyIfNull(image.getImpressionTrackers()) @@ -309,43 +446,41 @@ private String extractAdmImage(String adm) { String.format("\"\"", tracker))); final SmaatoImg img = image.getImg(); - return String.format("

%s
", - clickEvent.toString(), + clickEvent, HttpUtil.encodeUrl(StringUtils.stripToEmpty(getIfNotNull(img, SmaatoImg::getCtaurl))), StringUtils.stripToEmpty(getIfNotNull(img, SmaatoImg::getUrl)), - getIfNotNull(img, SmaatoImg::getW), - getIfNotNull(img, SmaatoImg::getH), - impressionTracker.toString()); + stripToZero(getIfNotNull(img, SmaatoImg::getW)), + stripToZero(getIfNotNull(img, SmaatoImg::getH)), + impressionTracker); } private String extractAdmRichMedia(String adm) { - final SmaatoRichMediaAd richMediaAd = admToAd(adm, SmaatoRichMediaAd.class); + final SmaatoRichMediaAd richMediaAd = convertAdmToAd(adm, SmaatoRichMediaAd.class); final SmaatoRichmedia richmedia = richMediaAd.getRichmedia(); if (richmedia == null) { - return adm; + throw new PreBidException("bid.adm.richmedia is empty"); } final StringBuilder clickEvent = new StringBuilder(); CollectionUtils.emptyIfNull(richmedia.getClickTrackers()) .forEach(tracker -> clickEvent.append( String.format("fetch(decodeURIComponent('%s'), {cache: 'no-cache'});", - HttpUtil.encodeUrl(tracker)))); + HttpUtil.encodeUrl(StringUtils.stripToEmpty(tracker))))); final StringBuilder impressionTracker = new StringBuilder(); CollectionUtils.emptyIfNull(richmedia.getImpressionTrackers()) .forEach(tracker -> impressionTracker.append( - String.format("\"\"", - tracker))); + String.format("\"\"", tracker))); return String.format("
%s%s
", - clickEvent.toString(), + clickEvent, StringUtils.stripToEmpty(getIfNotNull(richmedia.getMediadata(), SmaatoMediaData::getContent)), - impressionTracker.toString()); + impressionTracker); } - private T admToAd(String value, Class className) { + private T convertAdmToAd(String value, Class className) { try { return mapper.decodeValue(value, className); } catch (DecodeException e) { @@ -353,12 +488,31 @@ private T admToAd(String value, Class className) { } } + private static BidType getBidType(String markupType) { + switch (markupType) { + case SMT_AD_TYPE_IMG: + case SMT_ADTYPE_RICHMEDIA: + return BidType.banner; + case SMT_ADTYPE_VIDEO: + return BidType.video; + default: + throw new PreBidException(String.format("Invalid markupType %s", markupType)); + } + } + + private static R getIfNotNullOrThrow(T target, Function getter, String propertyName) { + final R result = getIfNotNull(target, getter); + if (result == null) { + throw new PreBidException(String.format("Missing %s property.", propertyName)); + } + return result; + } + private static R getIfNotNull(T target, Function getter) { return target != null ? getter.apply(target) : null; } - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); + private static int stripToZero(Integer target) { + return ObjectUtils.defaultIfNull(target, 0); } } diff --git a/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidExt.java b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidExt.java new file mode 100644 index 00000000000..b278d2a084e --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/smaato/proto/SmaatoBidExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.smaato.proto; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class SmaatoBidExt { + + Integer duration; +} 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 64f7e9b3410..1bf06829c6b 100644 --- a/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java +++ b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java @@ -1,16 +1,15 @@ package org.prebid.server.bidder.smartadserver; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Site; -import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -26,11 +25,12 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -54,71 +54,83 @@ public SmartadserverBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { final List> result = new ArrayList<>(); + final List errors = new ArrayList<>(); for (Imp imp : request.getImp()) { try { final ExtImpSmartadserver extImpSmartadserver = parseImpExt(imp); - final BidRequest updateRequest = request.toBuilder() + final BidRequest updatedRequest = request.toBuilder() .imp(Collections.singletonList(imp)) - .site(Site.builder() - .publisher(Publisher.builder() - .id(String.valueOf(extImpSmartadserver.getNetworkId())) - .build()) - .build()) + .site(modifySite(request.getSite(), extImpSmartadserver.getNetworkId())) .build(); - result.add(createSingleRequest(updateRequest, getUri())); + result.add(createSingleRequest(updatedRequest)); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badInput(e.getMessage())); + errors.add(BidderError.badInput(e.getMessage())); } } - return Result.of(result, Collections.emptyList()); + return Result.of(result, errors); } private ExtImpSmartadserver parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), SMARTADSERVER_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException("Error parsing smartadserverExt parameters"); } } - private String getUri() { - final URIBuilder uriBuilder = new URIBuilder() - .setPath(String.join("api/bid", endpointUrl)) - .addParameter("callerId", "5"); - - return uriBuilder.toString(); - } + private HttpRequest createSingleRequest(BidRequest request) { - private HttpRequest createSingleRequest(BidRequest request, String url) { - final BidRequest outgoingRequest = request.toBuilder().build(); - final String body = mapper.encode(outgoingRequest); return HttpRequest.builder() .method(HttpMethod.POST) - .uri(url) + .uri(getUri()) .headers(HttpUtil.headers()) - .body(body) - .payload(outgoingRequest) + .body(mapper.encode(request)) + .payload(request) .build(); } - @Override - public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - if (httpCall.getResponse().getStatusCode() == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); + private String getUri() { + final URI uri; + try { + uri = new URI(endpointUrl); + } catch (URISyntaxException e) { + throw new PreBidException(String.format("Malformed URL: %s.", endpointUrl)); } + return new URIBuilder(uri) + .setPath(String.format("%s/api/bid", StringUtils.removeEnd(uri.getPath(), "/"))) + .addParameter("callerId", "5") + .toString(); + } + + private static Site modifySite(Site site, Integer networkId) { + final Site.SiteBuilder siteBuilder = site != null ? site.toBuilder() : Site.builder(); + final Publisher sitePublisher = site != null ? site.getPublisher() : null; + + return siteBuilder.publisher(modifyPublisher(sitePublisher, networkId)).build(); + } + + private static Publisher modifyPublisher(Publisher publisher, Integer networkId) { + final Publisher.PublisherBuilder publisherBuilder = publisher != null + ? publisher.toBuilder() + : Publisher.builder(); + return publisherBuilder.id(String.valueOf(networkId)).build(); + } + + @Override + public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); return extractBids(httpCall.getRequest().getPayload(), bidResponse); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private Result> extractBids(BidRequest bidRequest, BidResponse bidResponse) { - if (bidResponse == null || bidResponse.getSeatbid() == null) { - return Result.of(Collections.emptyList(), Collections.emptyList()); + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Result.empty(); } final List errors = new ArrayList<>(); final List bidderBids = bidResponse.getSeatbid().stream() @@ -126,22 +138,11 @@ private Result> extractBids(BidRequest bidRequest, BidResponse b .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> toBidderBid(bidRequest, bid, bidResponse.getCur(), errors)) - .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); return Result.of(bidderBids, errors); } - private BidderBid toBidderBid(BidRequest bidRequest, Bid bid, String currency, List errors) { - try { - final BidType bidType = getBidType(bid.getImpid(), bidRequest.getImp()); - return BidderBid.of(bid, bidType, currency); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - return null; - } - } - private static BidType getBidType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId)) { @@ -150,9 +151,4 @@ private static BidType getBidType(String impId, List imps) { } return BidType.banner; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/smartrtb/SmartrtbBidder.java b/src/main/java/org/prebid/server/bidder/smartrtb/SmartrtbBidder.java index ced0aa21bb6..f5946494219 100644 --- a/src/main/java/org/prebid/server/bidder/smartrtb/SmartrtbBidder.java +++ b/src/main/java/org/prebid/server/bidder/smartrtb/SmartrtbBidder.java @@ -8,7 +8,6 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.lang3.StringUtils; @@ -31,7 +30,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; /** @@ -43,7 +41,6 @@ public class SmartrtbBidder implements Bidder { new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; private static final String CREATIVE_TYPE_BANNER = "BANNER"; private static final String CREATIVE_TYPE_VIDEO = "VIDEO"; @@ -75,7 +72,6 @@ public Result>> makeHttpRequests(BidRequest request ? validImp.toBuilder().tagid(zoneId).build() : imp; validImps.add(updatedImp); - } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } @@ -91,7 +87,7 @@ public Result>> makeHttpRequests(BidRequest request final BidRequest outgoingRequest = request.toBuilder().imp(validImps).build(); final String body = mapper.encode(outgoingRequest); final String requestUrl = endpointUrl + pubId; - final MultiMap headers = HttpUtil.headers().add("x-openrtb-version", "2.5"); + final MultiMap headers = HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); return Result.of(Collections.singletonList( HttpRequest.builder() @@ -121,16 +117,11 @@ private ExtImpSmartrtb parseImpExt(Imp imp) { @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - final BidResponse bidResponse; try { bidResponse = decodeBodyToBidResponse(httpCall); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } final List bidderBids = new ArrayList<>(); @@ -141,7 +132,7 @@ public Result> makeBids(HttpCall httpCall, BidReques try { smartrtbResponseExt = parseResponseExt(ext); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse("Invalid bid extension from endpoint.")); + return Result.withError(BidderError.badServerResponse("Invalid bid extension from endpoint.")); } final BidType bidType; switch (smartrtbResponseExt.getFormat()) { @@ -152,15 +143,15 @@ public Result> makeBids(HttpCall httpCall, BidReques bidType = BidType.video; break; default: - return Result.emptyWithError(BidderError.badServerResponse(String.format( + return Result.withError(BidderError.badServerResponse(String.format( "Unsupported creative type %s.", smartrtbResponseExt.getFormat()))); } final Bid updatedBid = bid.toBuilder().ext(null).build(); - final BidderBid bidderBid = BidderBid.of(updatedBid, bidType, DEFAULT_BID_CURRENCY); + final BidderBid bidderBid = BidderBid.of(updatedBid, bidType, bidResponse.getCur()); bidderBids.add(bidderBid); } } - return Result.of(bidderBids, Collections.emptyList()); + return Result.withValues(bidderBids); } private BidResponse decodeBodyToBidResponse(HttpCall httpCall) { @@ -181,9 +172,4 @@ private SmartrtbResponseExt parseResponseExt(ObjectNode ext) { throw new PreBidException(e.getMessage(), e); } } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java b/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java new file mode 100644 index 00000000000..f1c5eb0e555 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java @@ -0,0 +1,173 @@ +package org.prebid.server.bidder.smartyads; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.smartyads.ExtImpSmartyAds; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Between {@link Bidder} implementation. + */ +public class SmartyAdsBidder implements Bidder { + + private static final TypeReference> SMARTYADS_EXT_TYPE_REFERENCE = + new TypeReference>() { + }; + private static final String URL_HOST_MACRO = "{{Host}}"; + private static final String URL_SOURCE_ID_MACRO = "{{SourceId}}"; + private static final String URL_ACCOUNT_ID_MACRO = "{{AccountID}}"; + private static final int FIRST_SEAT_BID_INDEX = 0; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public SmartyAdsBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List validImps = new ArrayList<>(); + + ExtImpSmartyAds extImpSmartyAds = null; + for (Imp imp : request.getImp()) { + try { + extImpSmartyAds = parseImpExt(imp); + validImps.add(updateImp(imp)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest outgoingRequest = request.toBuilder().imp(validImps).build(); + + return Result.withValues(Collections.singletonList( + HttpRequest.builder() + .method(HttpMethod.POST) + .uri(resolveUrl(extImpSmartyAds)) + .headers(resolveHeaders(request.getDevice())) + .payload(outgoingRequest) + .body(mapper.encode(outgoingRequest)) + .build())); + } + + private ExtImpSmartyAds parseImpExt(Imp imp) { + final ExtImpSmartyAds extImpSmartyAds; + try { + extImpSmartyAds = mapper.mapper().convertValue(imp.getExt(), SMARTYADS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("ext.bidder not provided"); + } + if (StringUtils.isBlank(extImpSmartyAds.getHost())) { + throw new PreBidException("host is a required ext.bidder param"); + } + if (StringUtils.isBlank(extImpSmartyAds.getAccountId())) { + throw new PreBidException("accountId is a required ext.bidder param"); + } + if (StringUtils.isBlank(extImpSmartyAds.getSourceId())) { + throw new PreBidException("sourceId is a required ext.bidder param"); + } + return extImpSmartyAds; + } + + private static Imp updateImp(Imp imp) { + return imp.toBuilder().ext(null).build(); + } + + private String resolveUrl(ExtImpSmartyAds extImp) { + return endpointUrl + .replace(URL_HOST_MACRO, extImp.getHost()) + .replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(extImp.getSourceId())) + .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(extImp.getAccountId())); + } + + private static MultiMap resolveHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + headers.add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); + + if (device.getDnt() != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.DNT_HEADER, device.getDnt().toString()); + } + } + return headers; + } + + @Override + public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + return Result.of(extractBids(httpCall), Collections.emptyList()); + } catch (PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(HttpCall httpCall) { + final BidResponse bidResponse; + try { + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + throw new PreBidException("Bad Server Response"); + } + if (bidResponse == null) { + throw new PreBidException("Bad Server Response"); + } + if (CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("Empty SeatBid array"); + } + return bidsFromResponse(httpCall.getRequest().getPayload(), bidResponse); + } + + private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + final SeatBid firstSeatBid = bidResponse.getSeatbid().get(FIRST_SEAT_BID_INDEX); + return CollectionUtils.emptyIfNull(firstSeatBid.getBid()).stream() + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getVideo() != null) { + return BidType.video; + } + if (imp.getXNative() != null) { + return BidType.xNative; + } + } + } + return BidType.banner; + } +} diff --git a/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java b/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java new file mode 100644 index 00000000000..bd7cd1b4b74 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java @@ -0,0 +1,95 @@ +package org.prebid.server.bidder.smilewanted; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * SmileWanted {@link Bidder} implementation + */ +public class SmileWantedBidder implements Bidder { + + private static final String SW_INTEGRATION_TYPE = "prebid_server"; + private static final String X_OPENRTB_VERSION = "2.5"; + private static final int DEFAULT_AT = 1; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public SmileWantedBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().at(DEFAULT_AT).build(); + + return Result.withValue(HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(createHeaders()) + .payload(outgoingRequest) + .body(mapper.encode(outgoingRequest)) + .build()); + } + + private static MultiMap createHeaders() { + return HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION) + .add("sw-integration-type", SW_INTEGRATION_TYPE); + } + + @Override + public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidRequest, bidResponse); + } + + private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + return CollectionUtils.emptyIfNull(firstSeatBid.getBid()).stream() + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private static BidType getBidType(String impid, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impid) && imp.getVideo() != null) { + return BidType.video; + } + } + return BidType.banner; + } +} diff --git a/src/main/java/org/prebid/server/bidder/somoaudience/SomoaudienceBidder.java b/src/main/java/org/prebid/server/bidder/somoaudience/SomoaudienceBidder.java index cd624628496..29c1e4d2b64 100644 --- a/src/main/java/org/prebid/server/bidder/somoaudience/SomoaudienceBidder.java +++ b/src/main/java/org/prebid/server/bidder/somoaudience/SomoaudienceBidder.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.somoaudience; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.MultiMap; @@ -32,10 +30,12 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +/** + * Somoaudience {@link Bidder} implementation. + */ public class SomoaudienceBidder implements Bidder { private static final TypeReference> SOMOAUDIENCE_EXT_TYPE_REFERENCE = @@ -69,7 +69,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ videoAndNativeImps.add(imp); } else { errors.add(BidderError.badInput(String.format( - "SomoAudience only supports banner and video imps. Ignoring imp id=%s", imp.getId()))); + "SomoAudience only supports [banner, video, native] imps. Ignoring imp id : %s", imp.getId()))); } } final List> httpRequests = new ArrayList<>(); @@ -96,7 +96,7 @@ private HttpRequest makeRequest(BidRequest bidRequest, List imp String placementHash = null; for (Imp imp : imps) { try { - final ExtImpSomoaudience extImpSomoaudience = parseAndValidateImpExt(imp); + final ExtImpSomoaudience extImpSomoaudience = parseImpExt(imp); placementHash = extImpSomoaudience.getPlacementHash(); final BigDecimal bidFloor = extImpSomoaudience.getBidFloor(); final Imp modifiedImp = imp.toBuilder() @@ -108,7 +108,7 @@ private HttpRequest makeRequest(BidRequest bidRequest, List imp errors.add(BidderError.badInput(e.getMessage())); } } - if (CollectionUtils.isEmpty(validImps)) { + if (validImps.size() == 0) { return null; } final BidRequest.BidRequestBuilder requestBuilder = bidRequest.toBuilder(); @@ -117,80 +117,61 @@ private HttpRequest makeRequest(BidRequest bidRequest, List imp requestBuilder.ext(requestExtension); final BidRequest outgoingRequest = requestBuilder.build(); - final String body = mapper.encode(outgoingRequest); - final MultiMap headers = basicHeaders(); - final Device requestDevice = outgoingRequest.getDevice(); - if (requestDevice != null) { - addDeviceHeaders(headers, requestDevice); - } final String url = String.format("%s?s=%s", endpointUrl, placementHash); return HttpRequest.builder() .method(HttpMethod.POST) .uri(url) - .body(body) - .headers(headers) + .body(mapper.encode(outgoingRequest)) + .headers(headers(outgoingRequest.getDevice())) .payload(outgoingRequest) .build(); } - private ExtImpSomoaudience parseAndValidateImpExt(Imp imp) { - final ObjectNode impExt = imp.getExt(); - if (impExt == null || impExt.size() == 0) { - throw new PreBidException(String.format("ignoring imp id=%s, extImpBidder is empty", imp.getId())); - } - - final ExtImpSomoaudience extImpSomoaudience; + private ExtImpSomoaudience parseImpExt(Imp imp) { try { - extImpSomoaudience = mapper.mapper().convertValue(imp.getExt(), + return mapper.mapper().convertValue(imp.getExt(), SOMOAUDIENCE_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { throw new PreBidException(String.format( "ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.getId(), e.getMessage())); } - return extImpSomoaudience; } - private static MultiMap basicHeaders() { - return HttpUtil.headers() - .add("x-openrtb-version", "2.5"); - } + private static MultiMap headers(Device device) { + final MultiMap headers = HttpUtil.headers(); + headers.add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); - private static void addDeviceHeaders(MultiMap headers, Device device) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); - final Integer dnt = device.getDnt(); - headers.add("DNT", dnt != null ? dnt.toString() : "0"); + final Integer dnt = device.getDnt(); + if (dnt != null) { + headers.add(HttpUtil.DNT_HEADER, dnt.toString()); + } + } + return headers; } - /** - * Converts response to {@link List} of {@link BidderBid}s with {@link List} of errors. - */ @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); return extractBids(bidResponse, bidRequest.getImp()); } catch (DecodeException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - /** - * Extracts {@link Bid}s from response. - */ private static Result> extractBids(BidResponse bidResponse, List imps) { - return bidResponse == null || bidResponse.getSeatbid() == null - ? Result.of(Collections.emptyList(), Collections.emptyList()) - : Result.of(createBiddersBid(bidResponse, imps), Collections.emptyList()); + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) + ? Result.empty() + : Result.withValues(createBiddersBid(bidResponse, imps)); } - /** - * Extracts {@link Bid}s from response. - */ private static List createBiddersBid(BidResponse bidResponse, List imps) { return bidResponse.getSeatbid().stream() @@ -198,14 +179,11 @@ private static List createBiddersBid(BidResponse bidResponse, List BidderBid.of(bid, getBidderType(imps, bid.getImpid()), bidResponse.getCur())) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), imps), bidResponse.getCur())) .collect(Collectors.toList()); } - /** - * Defines {@link BidType} depends on {@link Imp} with the same impId - */ - private static BidType getBidderType(List imps, String impId) { + private static BidType getBidType(String impId, List imps) { return imps.stream() .filter(imp -> Objects.equals(imp.getId(), impId)) .findAny() @@ -213,9 +191,6 @@ private static BidType getBidderType(List imps, String impId) { .orElse(BidType.banner); } - /** - * Returns {@link BidType} depends on {@link Imp}s banner, video or native types. - */ private static BidType bidTypeFromImp(Imp imp) { BidType bidType = BidType.banner; if (imp.getBanner() == null) { @@ -227,9 +202,4 @@ private static BidType bidTypeFromImp(Imp imp) { } return bidType; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/sovrn/SovrnAdapter.java b/src/main/java/org/prebid/server/bidder/sovrn/SovrnAdapter.java deleted file mode 100644 index 80ff7c94fff..00000000000 --- a/src/main/java/org/prebid/server/bidder/sovrn/SovrnAdapter.java +++ /dev/null @@ -1,175 +0,0 @@ -package org.prebid.server.bidder.sovrn; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.User; -import com.iab.openrtb.response.BidResponse; -import io.vertx.core.MultiMap; -import io.vertx.core.http.Cookie; -import io.vertx.core.http.HttpMethod; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.AdUnitBid; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.bidder.Adapter; -import org.prebid.server.bidder.OpenrtbAdapter; -import org.prebid.server.bidder.model.AdUnitBidWithParams; -import org.prebid.server.bidder.model.AdapterHttpRequest; -import org.prebid.server.bidder.model.ExchangeCall; -import org.prebid.server.bidder.sovrn.proto.SovrnParams; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.request.PreBidRequest; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.MediaType; -import org.prebid.server.util.HttpUtil; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -/** - * Sovrn {@link Adapter} implementation. - */ -public class SovrnAdapter extends OpenrtbAdapter { - - private static final String LJT_READER_COOKIE_NAME = "ljt_reader"; - - private final String endpointUrl; - private final JacksonMapper mapper; - - public SovrnAdapter(String cookieFamilyName, String endpointUrl, JacksonMapper mapper) { - super(cookieFamilyName); - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public List> makeHttpRequests(AdapterRequest adapterRequest, - PreBidRequestContext preBidRequestContext) { - final BidRequest bidRequest = createBidRequest(adapterRequest, preBidRequestContext); - final AdapterHttpRequest httpRequest = AdapterHttpRequest.of( - HttpMethod.POST, - endpointUrl, - createBidRequest(adapterRequest, preBidRequestContext), - headers(bidRequest)); - - return Collections.singletonList(httpRequest); - } - - private BidRequest createBidRequest(AdapterRequest adapterRequest, PreBidRequestContext preBidRequestContext) { - final List adUnitBids = adapterRequest.getAdUnitBids(); - - final List> adUnitBidsWithParams = createAdUnitBidsWithParams(adUnitBids); - final List imps = makeImps(adUnitBidsWithParams, preBidRequestContext); - validateImps(imps); - - final PreBidRequest preBidRequest = preBidRequestContext.getPreBidRequest(); - return BidRequest.builder() - .id(preBidRequest.getTid()) - .at(1) - .tmax(preBidRequest.getTimeoutMillis()) - .imp(imps) - .app(preBidRequest.getApp()) - .site(makeSite(preBidRequestContext)) - .device(deviceBuilder(preBidRequestContext).build()) - .user(makeUser(preBidRequestContext)) - .source(makeSource(preBidRequestContext)) - .regs(preBidRequest.getRegs()) - .build(); - } - - private List> createAdUnitBidsWithParams(List adUnitBids) { - return adUnitBids.stream() - .map(adUnitBid -> AdUnitBidWithParams.of(adUnitBid, parseAndValidateParams(adUnitBid))) - .collect(Collectors.toList()); - } - - private SovrnParams parseAndValidateParams(AdUnitBid adUnitBid) { - final ObjectNode paramsNode = adUnitBid.getParams(); - if (paramsNode == null) { - throw new PreBidException("Sovrn params section is missing"); - } - - try { - return mapper.mapper().convertValue(paramsNode, SovrnParams.class); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e.getCause()); - } - } - - private static List makeImps(List> adUnitBidsWithParams, - PreBidRequestContext preBidRequestContext) { - return adUnitBidsWithParams.stream() - .filter(SovrnAdapter::isSupportedMediaType) - .map(adUnitBidWithParams -> makeImp(adUnitBidWithParams, preBidRequestContext)) - .collect(Collectors.toList()); - } - - private static boolean isSupportedMediaType(AdUnitBidWithParams sovrnAdUnitBidWithParams) { - return sovrnAdUnitBidWithParams.getAdUnitBid().getMediaTypes().contains(MediaType.banner); - } - - private static Imp makeImp(AdUnitBidWithParams adUnitBidWithParams, - PreBidRequestContext preBidRequestContext) { - final AdUnitBid adUnitBid = adUnitBidWithParams.getAdUnitBid(); - final SovrnParams params = adUnitBidWithParams.getParams(); - - return Imp.builder() - .id(adUnitBid.getAdUnitCode()) - .banner(bannerBuilder(adUnitBid).build()) - .instl(adUnitBid.getInstl()) - .secure(preBidRequestContext.getSecure()) - .tagid(params.getTagId()) - .build(); - } - - private MultiMap headers(BidRequest bidRequest) { - final MultiMap headers = headers(); - - final Device device = bidRequest.getDevice(); - if (device != null) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER.toString(), device.getUa()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER.toString(), - device.getLanguage()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER.toString(), device.getIp()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.DNT_HEADER.toString(), - Objects.toString(device.getDnt(), null)); - } - - final User user = bidRequest.getUser(); - final String buyeruid = user != null ? StringUtils.trimToNull(user.getBuyeruid()) : null; - if (buyeruid != null) { - headers.add(HttpUtil.COOKIE_HEADER.toString(), Cookie.cookie(LJT_READER_COOKIE_NAME, buyeruid).encode()); - } - return headers; - } - - @Override - public List extractBids(AdapterRequest adapterRequest, - ExchangeCall exchangeCall) { - return responseBidStream(exchangeCall.getResponse()) - .sorted(Comparator.comparingDouble(bid -> bid.getPrice() != null ? bid.getPrice().doubleValue() : 0d)) - .map(bid -> toBidBuilder(bid, adapterRequest)) - .collect(Collectors.toList()); - } - - private static Bid.BidBuilder toBidBuilder(com.iab.openrtb.response.Bid bid, AdapterRequest adapterRequest) { - final AdUnitBid adUnitBid = lookupBid(adapterRequest.getAdUnitBids(), bid.getImpid()); - return Bid.builder() - .bidId(adUnitBid.getBidId()) - .code(bid.getImpid()) - .bidder(adUnitBid.getBidderCode()) - .price(bid.getPrice()) - .adm(HttpUtil.decodeUrl(bid.getAdm())) - .creativeId(bid.getCrid()) - .width(bid.getW()) - .height(bid.getH()) - .dealId(bid.getDealid()) - .nurl(bid.getNurl()); - } -} diff --git a/src/main/java/org/prebid/server/bidder/sovrn/SovrnBidder.java b/src/main/java/org/prebid/server/bidder/sovrn/SovrnBidder.java index cf16141ff17..c138de011fa 100644 --- a/src/main/java/org/prebid/server/bidder/sovrn/SovrnBidder.java +++ b/src/main/java/org/prebid/server/bidder/sovrn/SovrnBidder.java @@ -1,7 +1,6 @@ package org.prebid.server.bidder.sovrn; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; @@ -29,11 +28,11 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -59,7 +58,7 @@ public SovrnBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest bidRequest) { if (CollectionUtils.isEmpty(bidRequest.getImp())) { - return Result.of(Collections.emptyList(), Collections.emptyList()); + return Result.empty(); } final List errors = new ArrayList<>(); @@ -90,17 +89,12 @@ public Result>> makeHttpRequests(BidRequest bidRequ public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(bidResponse), Collections.emptyList()); + return Result.withValues(extractBids(bidResponse)); } catch (DecodeException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } - private Imp makeImp(Imp imp) { if (imp.getXNative() != null || imp.getAudio() != null || imp.getVideo() != null) { throw new PreBidException( @@ -110,7 +104,7 @@ private Imp makeImp(Imp imp) { final ExtImpSovrn sovrnExt = parseExtImpSovrn(imp); return imp.toBuilder() - .bidfloor(sovrnExt.getBidfloor()) + .bidfloor(resolveBidFloor(imp.getBidfloor(), sovrnExt.getBidfloor())) .tagid(ObjectUtils.defaultIfNull(sovrnExt.getTagid(), sovrnExt.getLegacyTagId())) .build(); } @@ -127,29 +121,36 @@ private ExtImpSovrn parseExtImpSovrn(Imp imp) { } } - private MultiMap headers(BidRequest bidRequest) { + private static BigDecimal resolveBidFloor(BigDecimal impBidFloor, BigDecimal extBidFloor) { + return !isValidBidFloor(impBidFloor) && isValidBidFloor(extBidFloor) ? extBidFloor : impBidFloor; + } + + private static boolean isValidBidFloor(BigDecimal bidFloor) { + return bidFloor != null && bidFloor.compareTo(BigDecimal.ZERO) > 0; + } + + private static MultiMap headers(BidRequest bidRequest) { final MultiMap headers = HttpUtil.headers(); final Device device = bidRequest.getDevice(); if (device != null) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER.toString(), device.getUa()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER.toString(), - device.getLanguage()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER.toString(), device.getIp()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.DNT_HEADER.toString(), - Objects.toString(device.getDnt(), null)); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.DNT_HEADER, Objects.toString(device.getDnt(), null)); } final User user = bidRequest.getUser(); final String buyeruid = user != null ? StringUtils.trimToNull(user.getBuyeruid()) : null; if (buyeruid != null) { - headers.add(HttpUtil.COOKIE_HEADER.toString(), Cookie.cookie(LJT_READER_COOKIE_NAME, buyeruid).encode()); + headers.add(HttpUtil.COOKIE_HEADER, Cookie.cookie(LJT_READER_COOKIE_NAME, buyeruid).encode()); } + return headers; } private static List extractBids(BidResponse bidResponse) { - return bidResponse == null || bidResponse.getSeatbid() == null + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() : bidsFromResponse(bidResponse); } @@ -165,7 +166,8 @@ private static List bidsFromResponse(BidResponse bidResponse) { } private static Bid updateBid(Bid bid) { - bid.setAdm(HttpUtil.decodeUrl(bid.getAdm())); - return bid; + return bid.toBuilder() + .adm(HttpUtil.decodeUrl(bid.getAdm())) + .build(); } } diff --git a/src/main/java/org/prebid/server/bidder/synacormedia/SynacormediaBidder.java b/src/main/java/org/prebid/server/bidder/synacormedia/SynacormediaBidder.java index bca94a497f3..0cd8b905132 100644 --- a/src/main/java/org/prebid/server/bidder/synacormedia/SynacormediaBidder.java +++ b/src/main/java/org/prebid/server/bidder/synacormedia/SynacormediaBidder.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; @@ -21,6 +22,7 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.synacormedia.ExtImpSynacormedia; +import org.prebid.server.proto.openrtb.ext.request.synacormedia.ExtRequestSynacormedia; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -28,16 +30,17 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +/** + * Synacormedia {@link Bidder} implementation. + */ public class SynacormediaBidder implements Bidder { private static final TypeReference> SYNACORMEDIA_EXT_TYPE_REFERENCE = new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; private final String endpointUrl; private final JacksonMapper mapper; @@ -50,55 +53,56 @@ public SynacormediaBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest bidRequest) { final List errors = new ArrayList<>(); - final List validImps = new ArrayList<>(); ExtImpSynacormedia firstExtImp = null; + for (Imp imp : bidRequest.getImp()) { + final ExtImpSynacormedia extImpSynacormedia; try { - final ExtImpSynacormedia extImpSynacormedia = parseExtImp(imp.getExt()); - if (StringUtils.isBlank(extImpSynacormedia.getSeatId()) - || StringUtils.isBlank(extImpSynacormedia.getTagId())) { - errors.add(BidderError.badInput("Invalid Impression")); - continue; - } - final Imp updatedImp = imp.toBuilder().tagid(extImpSynacormedia.getTagId()).build(); - validImps.add(updatedImp); - - if (firstExtImp == null) { - firstExtImp = extImpSynacormedia; - } + extImpSynacormedia = parseAndValidateExtImp(imp.getExt()); } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); + errors.add(BidderError.badInput(String.format("Invalid Impression: %s", e.getMessage()))); + continue; } - } - if (validImps.isEmpty()) { - return Result.of(Collections.emptyList(), errors); + final Imp updatedImp = imp.toBuilder().tagid(extImpSynacormedia.getTagId()).build(); + validImps.add(updatedImp); + + if (firstExtImp == null) { + firstExtImp = extImpSynacormedia; + } } - if (firstExtImp == null || StringUtils.isBlank(firstExtImp.getSeatId()) - || StringUtils.isBlank(firstExtImp.getTagId())) { - errors.add(BidderError.badInput("Invalid Impression")); - return Result.of(Collections.emptyList(), errors); + if (validImps.isEmpty()) { + return Result.withErrors(errors); } final BidRequest outgoingRequest = bidRequest.toBuilder() .imp(validImps) - .ext(mapper.fillExtension(ExtRequest.empty(), firstExtImp)) + .ext(mapper.fillExtension(ExtRequest.empty(), ExtRequestSynacormedia.of(firstExtImp.getSeatId()))) .build(); - final String body = mapper.encode(outgoingRequest); return Result.of(Collections.singletonList( HttpRequest.builder() .method(HttpMethod.POST) .headers(HttpUtil.headers()) .uri(endpointUrl.replaceAll("\\{\\{Host}}", firstExtImp.getSeatId())) - .body(body) + .body(mapper.encode(outgoingRequest)) .payload(outgoingRequest) .build()), errors); } + private ExtImpSynacormedia parseAndValidateExtImp(ObjectNode impExt) { + final ExtImpSynacormedia extImp = parseExtImp(impExt); + + if (StringUtils.isBlank(extImp.getSeatId()) || StringUtils.isBlank(extImp.getTagId())) { + throw new PreBidException("imp.ext has no seatId or tagId"); + } + + return extImp; + } + private ExtImpSynacormedia parseExtImp(ObjectNode impExt) { try { return mapper.mapper().convertValue(impExt, SYNACORMEDIA_EXT_TYPE_REFERENCE).getBidder(); @@ -111,9 +115,9 @@ private ExtImpSynacormedia parseExtImp(ObjectNode impExt) { public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(bidResponse, httpCall.getRequest().getPayload()), Collections.emptyList()); + return Result.withValues(extractBids(bidResponse, httpCall.getRequest().getPayload())); } catch (DecodeException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } @@ -128,16 +132,25 @@ private static List bidsFromResponse(BidResponse bidResponse, BidRequ .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getMediaTypeForImp(bid.getImpid(), bidRequest.getImp()), - DEFAULT_BID_CURRENCY)) + .map(bid -> mapBidToBidderBid(bid, bidRequest.getImp(), bidResponse.getCur())) + .filter(Objects::nonNull) .collect(Collectors.toList()); } - private static BidType getMediaTypeForImp(String impId, List imps) { + private static BidderBid mapBidToBidderBid(Bid bid, List imps, String currency) { + final BidType bidType = getBidType(bid.getImpid(), imps); + + if (bidType == BidType.banner || bidType == BidType.video) { + return BidderBid.of(bid, bidType, currency); + } + return null; + } + + private static BidType getBidType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId)) { if (imp.getBanner() != null) { - break; + return BidType.banner; } if (imp.getVideo() != null) { return BidType.video; @@ -152,9 +165,4 @@ private static BidType getMediaTypeForImp(String impId, List imps) { } return BidType.banner; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } 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 2cc9cfbdc1b..b18861e21e4 100644 --- a/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java +++ b/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java @@ -1,44 +1,47 @@ package org.prebid.server.bidder.tappx; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.tappx.model.TappxBidderExt; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.tappx.ExtImpTappx; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; import java.math.BigDecimal; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; public class TappxBidder implements Bidder { + private static final String VERSION = "1.3"; + private static final String TYPE_CNN = "prebid"; + private static final TypeReference> TAPX_EXT_TYPE_REFERENCE = new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; - private static final String VERSION = "1.1"; - private static final String TYPE_CNN = "prebid"; private final String endpointUrl; private final JacksonMapper mapper; @@ -48,36 +51,25 @@ public TappxBidder(String endpointUrl, JacksonMapper mapper) { this.mapper = Objects.requireNonNull(mapper); } - /** - * Makes the HTTP requests which should be made to fetch bids. - *

- * Creates POST http request with all parameters in url and headers with encoded request in body. - */ @Override public Result>> makeHttpRequests(BidRequest request) { final ExtImpTappx extImpTappx; final String url; try { extImpTappx = parseBidRequestToExtImpTappx(request); - url = buildEndpointUrl(extImpTappx, request.getTest()); + url = resolveUrl(extImpTappx, request.getTest()); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badInput(e.getMessage())); + return Result.withError(BidderError.badInput(e.getMessage())); } - - final BigDecimal extBidfloor = extImpTappx.getBidfloor(); - final BidRequest outgoingRequest = extBidfloor != null && extBidfloor.signum() > 0 - ? modifyRequest(request, extBidfloor) - : request; - - return Result.of(Collections.singletonList( - HttpRequest.builder() - .method(HttpMethod.POST) - .headers(HttpUtil.headers()) - .uri(url) - .body(mapper.encode(outgoingRequest)) - .payload(outgoingRequest) - .build()), - Collections.emptyList()); + final BidRequest outgoingRequest = modifyRequest(request, extImpTappx); + + return Result.withValue(HttpRequest.builder() + .method(HttpMethod.POST) + .headers(HttpUtil.headers()) + .uri(url) + .body(mapper.encode(outgoingRequest)) + .payload(outgoingRequest) + .build()); } /** @@ -94,7 +86,7 @@ private ExtImpTappx parseBidRequestToExtImpTappx(BidRequest request) { /** * Builds endpoint url based on adapter-specific pub settings from imp.ext. */ - private String buildEndpointUrl(ExtImpTappx extImpTappx, Integer test) { + private String resolveUrl(ExtImpTappx extImpTappx, Integer test) { final String host = extImpTappx.getHost(); if (StringUtils.isBlank(host)) { throw new PreBidException("Tappx host undefined"); @@ -110,47 +102,78 @@ private String buildEndpointUrl(ExtImpTappx extImpTappx, Integer test) { throw new PreBidException("Tappx tappxkey undefined"); } - String url = String.format("%s%s/%s?tappxkey=%s", endpointUrl, host, endpoint, tappxkey); - if (test != null && test == 0) { - int t = (int) System.nanoTime(); - url += "&ts=" + t; - } - - url += "&v=" + VERSION; - url += "&type_cnn=" + TYPE_CNN; + return buildUrl(host, endpoint, tappxkey, test); + } + private String buildUrl(String host, String endpoint, String tappxkey, Integer test) { try { - HttpUtil.validateUrl(url); - } catch (IllegalArgumentException e) { - throw new PreBidException("Not valid url: " + url, e); + final String baseUri = resolveBaseUri(host); + final URIBuilder uriBuilder = new URIBuilder(baseUri); + + if (!StringUtils.containsIgnoreCase(host, endpoint)) { + final List pathSegments = new ArrayList<>(); + uriBuilder.getPathSegments().stream() + .filter(StringUtils::isNotBlank) + .forEach(pathSegments::add); + pathSegments.add(StringUtils.strip(endpoint, "/")); + uriBuilder.setPathSegments(pathSegments); + } + + uriBuilder.addParameter("tappxkey", tappxkey); + uriBuilder.addParameter("v", VERSION); + uriBuilder.addParameter("type_cnn", TYPE_CNN); + + if (test != null && test == 0) { + final String ts = String.valueOf(System.nanoTime()); + uriBuilder.addParameter("ts", ts); + } + return uriBuilder.build().toString(); + } catch (URISyntaxException e) { + throw new PreBidException(String.format("Failed to build endpoint URL: %s", e.getMessage())); } + } - return url; + private String resolveBaseUri(String host) { + return StringUtils.startsWithAny(host.toLowerCase(), "http://", "https://") + ? host + : endpointUrl + host; } /** * Modify request's first imp. */ - private static BidRequest modifyRequest(BidRequest request, BigDecimal extBidfloor) { - final Imp modifiedFirstImp = request.getImp().get(0).toBuilder().bidfloor(extBidfloor).build(); + private BidRequest modifyRequest(BidRequest request, ExtImpTappx extImpTappx) { final List modifiedImps = new ArrayList<>(request.getImp()); - modifiedImps.set(0, modifiedFirstImp); + final BigDecimal extBidfloor = extImpTappx.getBidfloor(); + if (extBidfloor != null && extBidfloor.signum() > 0) { + final Imp modifiedFirstImp = request.getImp().get(0).toBuilder().bidfloor(extBidfloor).build(); + modifiedImps.set(0, modifiedFirstImp); + } + + return request.toBuilder().imp(modifiedImps).ext(getExtRequest(extImpTappx)).build(); + } + + private ExtRequest getExtRequest(ExtImpTappx extImpTappx) { + final ExtRequest extRequest = ExtRequest.empty(); + final TappxBidderExt tappxBidderExt = TappxBidderExt.of(extImpTappx.getTappxkey(), extImpTappx.getMktag(), + extImpTappx.getBcid(), extImpTappx.getBcrid()); + extRequest.addProperty("bidder", mapper.mapper().valueToTree(tappxBidderExt)); - return request.toBuilder().imp(modifiedImps).build(); + return extRequest; } @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - return bidResponse == null || bidResponse.getSeatbid() == null + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() : bidsFromResponse(bidRequest, bidResponse); } @@ -159,8 +182,7 @@ private static List bidsFromResponse(BidRequest bidRequest, BidRespon return bidResponse.getSeatbid().stream() .map(SeatBid::getBid) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), - DEFAULT_BID_CURRENCY)) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); } @@ -172,10 +194,4 @@ private static BidType getBidType(String impId, List imps) { } return BidType.banner; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } - diff --git a/src/main/java/org/prebid/server/bidder/tappx/model/TappxBidderExt.java b/src/main/java/org/prebid/server/bidder/tappx/model/TappxBidderExt.java new file mode 100644 index 00000000000..afe96df8855 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/tappx/model/TappxBidderExt.java @@ -0,0 +1,19 @@ +package org.prebid.server.bidder.tappx.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@Value +@AllArgsConstructor(staticName = "of") +public class TappxBidderExt { + + String tappxkey; + + String mktag; + + List bcid; + + List bcrid; +} diff --git a/src/main/java/org/prebid/server/bidder/telaria/TelariaBidder.java b/src/main/java/org/prebid/server/bidder/telaria/TelariaBidder.java index 03b539a9fb2..e1b2cc38d24 100644 --- a/src/main/java/org/prebid/server/bidder/telaria/TelariaBidder.java +++ b/src/main/java/org/prebid/server/bidder/telaria/TelariaBidder.java @@ -1,29 +1,25 @@ package org.prebid.server.bidder.telaria; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Publisher; -import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.MultiMap; -import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; -import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.telaria.model.TelariaRequestExt; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; @@ -35,20 +31,17 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; -import java.io.FileInputStream; -import java.io.IOException; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import java.util.zip.GZIPInputStream; +/** + * Telaria {@link Bidder} implementation. + */ public class TelariaBidder implements Bidder { - private static final String DEFAULT_BID_CURRENCY = "USD"; private static final TypeReference> TELARIA_EXT_TYPE_REFERENCE = new TypeReference>() { }; @@ -64,13 +57,10 @@ public TelariaBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest bidRequest) { final List validImps = new ArrayList<>(); - if (CollectionUtils.isEmpty(bidRequest.getImp())) { - return Result.emptyWithError(BidderError.badInput("Telaria: Missing Imp Object")); - } try { validateImp(bidRequest.getImp()); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badInput(e.getMessage())); + return Result.withError(BidderError.badInput(e.getMessage())); } final String publisherId = getPublisherId(bidRequest); @@ -83,7 +73,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ seatCode = extImp.getSeatCode(); validImps.add(updateImp(imp, extImp, publisherId)); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badInput(e.getMessage())); + return Result.withError(BidderError.badInput(e.getMessage())); } } @@ -92,26 +82,24 @@ public Result>> makeHttpRequests(BidRequest bidRequ } if (bidRequest.getSite() != null) { - requestBuilder.site(modifySite(seatCode, bidRequest.getSite())); + requestBuilder.site(modifySite(bidRequest.getSite(), seatCode)); } else if (bidRequest.getApp() != null) { - requestBuilder.app(modifyApp(seatCode, bidRequest.getApp())); + requestBuilder.app(modifyApp(bidRequest.getApp(), seatCode)); } final BidRequest outgoingRequest = requestBuilder.imp(validImps).build(); final String body = mapper.encode(outgoingRequest); - return Result.of(Collections.singletonList( - HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(headers(bidRequest)) - .payload(outgoingRequest) - .body(body) - .build()), - Collections.emptyList()); + return Result.withValue(HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(headers(bidRequest)) + .payload(outgoingRequest) + .body(body) + .build()); } - private void validateImp(List imps) { + private static void validateImp(List imps) { boolean hasVideoObject = false; for (Imp imp : imps) { if (imp.getBanner() != null) { @@ -125,17 +113,7 @@ private void validateImp(List imps) { } } - private Imp updateImp(Imp imp, ExtImpTelaria extImp, String publisherId) { - if (StringUtils.isBlank(extImp.getSeatCode())) { - throw new PreBidException("Telaria: Seat Code required"); - } - return imp.toBuilder() - .tagid(extImp.getAdCode()) - .ext(mapper.mapper().valueToTree(ExtImpOutTelaria.of(imp.getTagid(), publisherId))) - .build(); - } - - private String getPublisherId(BidRequest bidRequest) { + private static String getPublisherId(BidRequest bidRequest) { if (bidRequest.getSite() != null && bidRequest.getSite().getPublisher() != null) { return bidRequest.getSite().getPublisher().getId(); } else if (bidRequest.getApp() != null && bidRequest.getApp().getPublisher() != null) { @@ -152,22 +130,34 @@ private ExtImpTelaria parseImpExt(Imp imp) { } } - private Site modifySite(String seatCode, Site site) { - return site.toBuilder().publisher(createPublisher(seatCode, site.getPublisher())).build(); + private Imp updateImp(Imp imp, ExtImpTelaria extImp, String publisherId) { + if (StringUtils.isBlank(extImp.getSeatCode())) { + throw new PreBidException("Telaria: Seat Code required"); + } + return imp.toBuilder() + .tagid(extImp.getAdCode()) + .ext(mapper.mapper().valueToTree(ExtImpOutTelaria.of(imp.getTagid(), publisherId))) + .build(); + } + + private static Site modifySite(Site site, String seatCode) { + return site.toBuilder().publisher(createPublisher(site.getPublisher(), seatCode)).build(); } - private App modifyApp(String seatCode, App app) { - return app.toBuilder().publisher(createPublisher(seatCode, app.getPublisher())).build(); + private static App modifyApp(App app, String seatCode) { + return app.toBuilder().publisher(createPublisher(app.getPublisher(), seatCode)).build(); } - private Publisher createPublisher(String seatCode, Publisher publisher) { + private static Publisher createPublisher(Publisher publisher, String seatCode) { return publisher != null ? publisher.toBuilder().id(seatCode).build() : Publisher.builder().id(seatCode).build(); } - private MultiMap headers(BidRequest bidRequest) { - final MultiMap headers = HttpUtil.headers().add("x-openrtb-version", "2.5").add("Accept-Encoding", "gzip"); + private static MultiMap headers(BidRequest bidRequest) { + final MultiMap headers = HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + final Device device = bidRequest.getDevice(); if (device != null) { HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); @@ -175,56 +165,37 @@ private MultiMap headers(BidRequest bidRequest) { HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.DNT_HEADER, Objects.toString(device.getDnt(), null)); } + return headers; } @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - try { - return Result.of(extractBids(httpCall.getRequest().getPayload(), getBidResponse(httpCall.getResponse())), - Collections.emptyList()); - } catch (DecodeException | PreBidException | IOException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (PreBidException | DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - return bidResponse == null || bidResponse.getSeatbid() == null - ? Collections.emptyList() - : bidsFromResponse(bidRequest, bidResponse); - } - - private BidResponse getBidResponse(HttpResponse response) throws IOException { - if ("gzip".equals(response.getHeaders().get("Content-Encoding"))) { - response.getHeaders().remove("Content-Encoding"); - return mapper.decodeValue(Buffer.buffer(decompress(response.getBody())), BidResponse.class); + private List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); } - return mapper.decodeValue(response.getBody(), BidResponse.class); + return bidsFromResponse(bidResponse); } - private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BidType.video, DEFAULT_BID_CURRENCY)) - .collect(Collectors.toList()); - } + private static List bidsFromResponse(BidResponse bidResponse) { + final SeatBid firstSeatBid = bidResponse.getSeatbid().get(0); + final List bids = firstSeatBid.getBid(); - public static byte[] decompress(String file) throws IOException { - try (GZIPInputStream gzipInput = new GZIPInputStream(new FileInputStream(file))) { - return IOUtils.toByteArray(gzipInput); + if (CollectionUtils.isEmpty(bids)) { + return Collections.emptyList(); } - } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); + return bids.stream() + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, BidType.video, bidResponse.getCur())) + .collect(Collectors.toList()); } } diff --git a/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java b/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java index 9d6410ca457..ad099079b3e 100644 --- a/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java +++ b/src/main/java/org/prebid/server/bidder/triplelift/TripleliftBidder.java @@ -9,6 +9,8 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -28,12 +30,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; +/** + * Triplelift {@link Bidder} implementation. + */ public class TripleliftBidder implements Bidder { - private static final String DEFAULT_BID_CURRENCY = "USD"; private static final TypeReference> TRIPLELIFT_EXT_TYPE_REFERENCE = new TypeReference>() { }; @@ -60,7 +63,7 @@ public final Result>> makeHttpRequests(BidRequest b if (validImps.isEmpty()) { errors.add(BidderError.badInput("No valid impressions for triplelift")); - return Result.of(Collections.emptyList(), errors); + return Result.withErrors(errors); } final BidRequest updatedRequest = bidRequest.toBuilder() @@ -83,19 +86,16 @@ private Imp modifyImp(Imp imp) throws PreBidException { throw new PreBidException("neither Banner nor Video object specified"); } - final ExtImpTriplelift impExt = parseExtImpTriplelift(imp); - final Imp.ImpBuilder impBuilder = imp.toBuilder().tagid(impExt.getInventoryCode()); - if (impExt.getFloor() != null) { - impBuilder.bidfloor(impExt.getFloor()); - } - - return impBuilder.build(); + final ExtImpTriplelift impExt = parseImpExt(imp); + return imp.toBuilder() + .tagid(impExt.getInventoryCode()) + .bidfloor(ObjectUtils.defaultIfNull(impExt.getFloor(), imp.getBidfloor())) + .build(); } - private ExtImpTriplelift parseExtImpTriplelift(Imp imp) { + private ExtImpTriplelift parseImpExt(Imp imp) { try { - return mapper.mapper().convertValue(imp.getExt(), - TRIPLELIFT_EXT_TYPE_REFERENCE).getBidder(); + return mapper.mapper().convertValue(imp.getExt(), TRIPLELIFT_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { throw new PreBidException(e.getMessage(), e); } @@ -105,30 +105,31 @@ private ExtImpTriplelift parseExtImpTriplelift(Imp imp) { public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { final BidResponse bidResponse; try { - bidResponse = decodeBodyToBidResponse(httpCall); + bidResponse = decodeBody(httpCall); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } - if (bidResponse == null || bidResponse.getSeatbid() == null) { - return Result.of(Collections.emptyList(), Collections.emptyList()); + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Result.empty(); } final List errors = new ArrayList<>(); final List bidderBids = new ArrayList<>(); for (SeatBid seatBid : bidResponse.getSeatbid()) { for (Bid bid : seatBid.getBid()) { - final ObjectNode ext = bid.getExt(); - if (ext == null) { + final ObjectNode bidExt = bid.getExt(); + if (bidExt == null) { errors.add(BidderError.badServerResponse(String.format("Empty ext in bid %s", bid.getId()))); break; } + try { - final TripleliftResponseExt tripleliftResponseExt = mapper.mapper().treeToValue(ext, + final TripleliftResponseExt tripleliftResponseExt = mapper.mapper().treeToValue(bidExt, TripleliftResponseExt.class); - final BidderBid bidderBid = BidderBid.of(bid, getBidType(tripleliftResponseExt), - DEFAULT_BID_CURRENCY); - bidderBids.add(bidderBid); + final BidType bidType = getBidType(tripleliftResponseExt); + + bidderBids.add(BidderBid.of(bid, bidType, bidResponse.getCur())); } catch (JsonProcessingException e) { errors.add(BidderError.badServerResponse(e.getMessage())); } @@ -137,7 +138,7 @@ public Result> makeBids(HttpCall httpCall, BidReques return Result.of(bidderBids, errors); } - private BidResponse decodeBodyToBidResponse(HttpCall httpCall) { + private BidResponse decodeBody(HttpCall httpCall) { try { return mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); } catch (DecodeException e) { @@ -150,15 +151,8 @@ private static BidType getBidType(TripleliftResponseExt tripleliftResponseExt) { ? tripleliftResponseExt.getTripleliftPb() : null; - if (tripleliftInnerExt != null && tripleliftInnerExt.getFormat() == 11) { - return BidType.video; - } - return BidType.banner; - } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); + return tripleliftInnerExt != null && Objects.equals(tripleliftInnerExt.getFormat(), 11) + ? BidType.video + : BidType.banner; } } - diff --git a/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidder.java b/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidder.java index 66d991d59e0..9f4a214edd2 100644 --- a/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidder.java +++ b/src/main/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidder.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -10,15 +9,14 @@ import com.iab.openrtb.request.Site; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; @@ -40,7 +38,6 @@ public class TripleliftNativeBidder implements Bidder { - private static final String DEFAULT_BID_CURRENCY = "USD"; private static final String UNKONWN_PUBLSIHER_ID = "unknown"; private static final TypeReference> TRIPLELIFT_EXT_TYPE_REFERENCE = @@ -174,21 +171,16 @@ private static Publisher findPublisher(BidRequest bidRequest) { @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final HttpResponse response = httpCall.getResponse(); - if (response.getStatusCode() == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - try { - final BidResponse bidResponse = mapper.decodeValue(response.getBody(), BidResponse.class); - return Result.of(extractBids(bidResponse), Collections.emptyList()); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private static List extractBids(BidResponse bidResponse) { - return bidResponse == null || bidResponse.getSeatbid() == null + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() : bidsFromResponse(bidResponse); } @@ -199,13 +191,7 @@ private static List bidsFromResponse(BidResponse bidResponse) { .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BidType.xNative, DEFAULT_BID_CURRENCY)) + .map(bid -> BidderBid.of(bid, BidType.xNative, bidResponse.getCur())) .collect(Collectors.toList()); } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } - diff --git a/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java b/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java index 968b73166e3..23ad4fdaf01 100644 --- a/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java +++ b/src/main/java/org/prebid/server/bidder/ttx/TtxBidder.java @@ -1,64 +1,197 @@ package org.prebid.server.bidder.ttx; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.bidder.OpenrtbBidder; -import org.prebid.server.bidder.model.ImpWithExt; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.ttx.proto.TtxImpExt; import org.prebid.server.bidder.ttx.proto.TtxImpExtTtx; +import org.prebid.server.bidder.ttx.response.TtxBidExt; +import org.prebid.server.bidder.ttx.response.TtxBidExtTtx; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ttx.ExtImpTtx; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; -public class TtxBidder extends OpenrtbBidder { +/** + * 33across {@link Bidder} implementation. + */ +public class TtxBidder implements Bidder { + + private static final TypeReference> TTX_EXT_TYPE_REFERENCE = + new TypeReference>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; public TtxBidder(String endpointUrl, JacksonMapper mapper) { - super(endpointUrl, RequestCreationStrategy.SINGLE_REQUEST, ExtImpTtx.class, mapper); + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); } @Override - protected void modifyRequest(BidRequest bidRequest, BidRequest.BidRequestBuilder requestBuilder, - List> impsWithExts) { - final List modifiedImps = impsWithExts.stream() - .map(ImpWithExt::getImp) - .collect(Collectors.toList()); - final Imp firstImp = modifiedImps.get(0); - final ExtImpTtx firstImpExt = impsWithExts.get(0).getImpExt(); + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List> requests = new ArrayList<>(); - final String zoneId = firstImpExt.getZoneId(); - final TtxImpExt ttxImpExt = TtxImpExt.of( - TtxImpExtTtx.of(firstImpExt.getProductId(), StringUtils.isNotBlank(zoneId) ? zoneId : null)); - - final Imp modifiedFirstImp = firstImp.toBuilder().ext(mapper.mapper().valueToTree(ttxImpExt)).build(); - - if (modifiedImps.size() == 1) { - requestBuilder.imp(Collections.singletonList(modifiedFirstImp)); - } else { - final List subList = modifiedImps.subList(1, modifiedImps.size()); - final List finalizedImps = new ArrayList<>(subList.size() + 1); - finalizedImps.add(modifiedFirstImp); - finalizedImps.addAll(subList); - requestBuilder.imp(finalizedImps); + for (Imp imp : request.getImp()) { + try { + validateImp(imp); + final ExtImpTtx extImpTtx = parseImpExt(imp); + final Imp updatedImp = updateImp(imp, extImpTtx); + requests.add(createRequest(request, updatedImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + return Result.of(requests, errors); + } + + private void validateImp(Imp imp) { + if (imp.getBanner() == null && imp.getVideo() == null) { + throw new PreBidException( + String.format("Imp ID %s must have at least one of [Banner, Video] defined", imp.getId())); + } + } + + private ExtImpTtx parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TTX_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp updateImp(Imp imp, ExtImpTtx extImpTtx) { + final String productId = extImpTtx.getProductId(); + return imp.toBuilder() + .video(updatedVideo(imp.getVideo(), productId)) + .ext(createImpExt(productId, extImpTtx.getZoneId(), extImpTtx.getSiteId())) + .build(); + } + + private static Video updatedVideo(Video video, String productId) { + if (video == null) { + return null; + } + if (isZeroOrNullInteger(video.getW()) + || isZeroOrNullInteger(video.getH()) + || CollectionUtils.isEmpty(video.getProtocols()) + || CollectionUtils.isEmpty(video.getMimes()) + || CollectionUtils.isEmpty(video.getPlaybackmethod())) { + throw new PreBidException("One or more invalid or missing video field(s) " + + "w, h, protocols, mimes, playbackmethod"); + } + final Integer videoPlacement = video.getPlacement(); + + return video.toBuilder() + .startdelay(resolveStartDelay(video.getStartdelay(), productId)) + .placement(resolvePlacement(videoPlacement, productId)) + .build(); + } + + private static boolean isZeroOrNullInteger(Integer integer) { + return integer == null || integer == 0; + } + + private static Integer resolveStartDelay(Integer startDelay, String productId) { + return Objects.equals(productId, "instream") ? Integer.valueOf(0) : startDelay; + } + + private static Integer resolvePlacement(Integer videoPlacement, String productId) { + if (Objects.equals(productId, "instream")) { + return 1; + } + if (isZeroOrNullInteger(videoPlacement)) { + return 2; } - requestBuilder.site(modifySite(bidRequest.getSite(), firstImpExt.getSiteId())); + return videoPlacement; } - private static Site modifySite(Site site, String siteId) { - final Site.SiteBuilder siteBuilder = site == null ? Site.builder() : site.toBuilder(); - return siteBuilder - .id(siteId) + private ObjectNode createImpExt(String productId, String zoneId, String siteId) { + final TtxImpExt ttxImpExt = TtxImpExt.of( + TtxImpExtTtx.of(productId, StringUtils.isNotEmpty(zoneId) ? zoneId : siteId)); + return mapper.mapper().valueToTree(ttxImpExt); + } + + private HttpRequest createRequest(BidRequest request, Imp requestImp) { + final BidRequest modifiedRequest = request.toBuilder() + .imp(Collections.singletonList(requestImp)) + .build(); + + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .payload(modifiedRequest) + .body(mapper.encode(modifiedRequest)) .build(); } @Override - protected BidType getBidType(String impId, List imps) { - return BidType.banner; + public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse), Collections.emptyList()); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse); + } + + private List bidsFromResponse(BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private BidType getBidType(Bid + bid) { + try { + final TtxBidExt ttxBidExt = mapper.mapper().convertValue(bid.getExt(), TtxBidExt.class); + return ttxBidExt != null ? getBidTypeByTtx(ttxBidExt.getTtx()) : BidType.banner; + } catch (IllegalArgumentException e) { + return BidType.banner; + } + } + + private static BidType getBidTypeByTtx(TtxBidExtTtx bidExt) { + return bidExt != null && Objects.equals(bidExt.getMediaType(), "video") + ? BidType.video + : BidType.banner; } } diff --git a/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExt.java b/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExt.java new file mode 100644 index 00000000000..6f4ba1b5702 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.ttx.response; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class TtxBidExt { + + TtxBidExtTtx ttx; +} diff --git a/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExtTtx.java b/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExtTtx.java new file mode 100644 index 00000000000..6a66b3322f7 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ttx/response/TtxBidExtTtx.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidder.ttx.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class TtxBidExtTtx { + + @JsonProperty("mediaType") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + String mediaType; +} diff --git a/src/main/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidder.java b/src/main/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidder.java index 4eb428c3942..eeb6d9a4e4a 100644 --- a/src/main/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidder.java +++ b/src/main/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidder.java @@ -1,13 +1,11 @@ package org.prebid.server.bidder.ucfunnel; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -28,7 +26,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; /** @@ -40,7 +37,6 @@ public class UcfunnelBidder implements Bidder { new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; private final String endpointUrl; private final JacksonMapper mapper; @@ -54,7 +50,7 @@ public Result>> makeHttpRequests(BidRequest request final List errors = new ArrayList<>(); if (CollectionUtils.isEmpty(request.getImp())) { - return Result.emptyWithError(BidderError.badInput("No valid impressions in the bid request")); + return Result.withError(BidderError.badInput("No valid impressions in the bid request")); } String partnerId = null; @@ -64,7 +60,7 @@ public Result>> makeHttpRequests(BidRequest request partnerId = extImpUcfunnel.getPartnerid(); if (StringUtils.isEmpty(partnerId) || StringUtils.isEmpty(adUnitId)) { errors.add(BidderError.badInput("No PartnerId or AdUnitId in the bid request")); - return Result.of(Collections.emptyList(), errors); + return Result.withErrors(errors); } } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); @@ -94,16 +90,11 @@ private ExtImpUcfunnel parseImpExt(Imp imp) { @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - final BidResponse bidResponse; try { bidResponse = decodeBodyToBidResponse(httpCall); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badInput(e.getMessage())); + return Result.withError(BidderError.badInput(e.getMessage())); } final List bidderBids = new ArrayList<>(); @@ -111,12 +102,12 @@ public Result> makeBids(HttpCall httpCall, BidReques for (Bid bid : seatBid.getBid()) { final BidType bidType = getBidType(bid.getImpid(), bidRequest.getImp()); if (bidType == BidType.banner || bidType == BidType.video) { - final BidderBid bidderBid = BidderBid.of(bid, bidType, DEFAULT_BID_CURRENCY); + final BidderBid bidderBid = BidderBid.of(bid, bidType, bidResponse.getCur()); bidderBids.add(bidderBid); } } } - return Result.of(bidderBids, Collections.emptyList()); + return Result.withValues(bidderBids); } private BidResponse decodeBodyToBidResponse(HttpCall httpCall) { @@ -143,9 +134,4 @@ private static BidType getBidType(String impId, List imps) { } return BidType.xNative; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/unicorn/UnicornBidder.java b/src/main/java/org/prebid/server/bidder/unicorn/UnicornBidder.java new file mode 100644 index 00000000000..7ca76a27333 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/unicorn/UnicornBidder.java @@ -0,0 +1,238 @@ +package org.prebid.server.bidder.unicorn; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.unicorn.model.UnicornImpExt; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; +import org.prebid.server.proto.openrtb.ext.request.unicorn.ExtImpUnicorn; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Unicorn {@link Bidder} implementation. + */ +public class UnicornBidder implements Bidder { + + private static final TypeReference> UNICORN_EXT_TYPE_REFERENCE = + new TypeReference>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public UnicornBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List requestImps = request.getImp(); + final List modifiedImps; + final Source source; + final Integer firstImpAccountId; + try { + validateRegs(request.getRegs()); + modifiedImps = modifyImps(requestImps); + source = updateSource(request.getSource()); + firstImpAccountId = parseImpExtBidder(requestImps.get(0)).getAccountId(); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + final ExtRequest modifiedExtRequest = modifyExtRequest(request.getExt(), firstImpAccountId); + return Result.withValue(createRequest(request, modifiedImps, source, modifiedExtRequest)); + } + + private static void validateRegs(Regs regs) { + if (regs != null) { + if (Objects.equals(regs.getCoppa(), 1)) { + throw new PreBidException("COPPA is not supported"); + } + final ExtRegs extRegs = regs.getExt(); + if (extRegs != null) { + if (Objects.equals(extRegs.getGdpr(), 1)) { + throw new PreBidException("GDPR is not supported"); + } + if (StringUtils.isNotEmpty(extRegs.getUsPrivacy())) { + throw new PreBidException("CCPA is not supported"); + } + } + } + } + + private List modifyImps(List imps) { + final List modifiedImps = new ArrayList<>(); + for (Imp imp : imps) { + final UnicornImpExt unicornImpExt = parseImpExt(imp); + final ExtImpUnicorn extImpBidder = unicornImpExt.getBidder(); + final Imp.ImpBuilder impBuilder = imp.toBuilder().secure(1); + final String placementId = extImpBidder.getPlacementId(); + + if (StringUtils.isEmpty(placementId)) { + final String resolvedPlacementId = getStoredRequestImpId(imp); + final UnicornImpExt updatedExt = unicornImpExt.toBuilder() + .bidder(extImpBidder.toBuilder().placementId(resolvedPlacementId).build()) + .build(); + impBuilder + .tagid(resolvedPlacementId) + .ext(mapper.mapper().convertValue(updatedExt, ObjectNode.class)); + } else { + impBuilder + .tagid(placementId) + .ext(mapper.mapper().convertValue(unicornImpExt, ObjectNode.class)); + } + + modifiedImps.add(impBuilder.build()); + } + return modifiedImps; + } + + private UnicornImpExt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), UnicornImpExt.class); + } catch (IllegalArgumentException e) { + throw new PreBidException(String.format( + "Error while decoding ext of imp with id: %s, error: %s ", imp.getId(), e.getMessage())); + } + } + + private static String getStoredRequestImpId(Imp imp) { + final JsonNode extPrebid = imp.getExt().get("prebid"); + final JsonNode storedRequestNode = isNotEmptyNode(extPrebid) ? extPrebid.get("storedrequest") : null; + final JsonNode storedRequestIdNode = isNotEmptyNode(storedRequestNode) ? storedRequestNode.get("id") : null; + final String storedRequestId = storedRequestIdNode != null && storedRequestIdNode.isTextual() + ? storedRequestIdNode.textValue() + : null; + if (StringUtils.isNotEmpty(storedRequestId)) { + return storedRequestId; + } else { + throw new PreBidException(String.format("stored request id not found in imp: %s", imp.getId())); + } + } + + private static boolean isNotEmptyNode(JsonNode node) { + return node != null && !node.isEmpty(); + } + + private static Source updateSource(Source source) { + return source != null + ? source.toBuilder().ext(createExtSource()).build() + : Source.builder().ext(createExtSource()).build(); + } + + private static ExtSource createExtSource() { + final ExtSource extSource = ExtSource.of(null); + extSource.addProperty("stype", new TextNode("prebid_server_uncn")); + extSource.addProperty("bidder", new TextNode("unicorn")); + return extSource; + } + + private ExtImpUnicorn parseImpExtBidder(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), UNICORN_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static ExtRequest modifyExtRequest(ExtRequest extRequest, Integer accountId) { + final ExtRequest modifiedRequest = extRequest != null + ? ExtRequest.of(extRequest.getPrebid()) + : ExtRequest.of(null); + final int resolvedAccountId = accountId == null ? 0 : accountId; + modifiedRequest.addProperty("accountId", new IntNode(resolvedAccountId)); + + return modifiedRequest; + } + + private HttpRequest createRequest(BidRequest request, + List imps, Source source, + ExtRequest extRequest) { + final BidRequest outgoingRequest = request.toBuilder() + .imp(imps) + .source(source) + .ext(extRequest) + .build(); + + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(resolveHeaders(request.getDevice())) + .payload(outgoingRequest) + .body(mapper.encode(outgoingRequest)) + .build(); + } + + private static MultiMap resolveHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + } + + return headers; + } + + @Override + public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse), Collections.emptyList()); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse); + } + + private static List bidsFromResponse(BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExt.java b/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExt.java new file mode 100644 index 00000000000..b4f534d1aad --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExt.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.unicorn.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.unicorn.ExtImpUnicorn; + +@AllArgsConstructor(staticName = "of") +@Value +@Builder(toBuilder = true) +public class UnicornImpExt { + + UnicornImpExtContext context; + + ExtImpUnicorn bidder; +} diff --git a/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExtContext.java b/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExtContext.java new file mode 100644 index 00000000000..05e6f9c36a6 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExtContext.java @@ -0,0 +1,12 @@ +package org.prebid.server.bidder.unicorn.model; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class UnicornImpExtContext { + + ObjectNode data; +} diff --git a/src/main/java/org/prebid/server/bidder/unruly/UnrulyBidder.java b/src/main/java/org/prebid/server/bidder/unruly/UnrulyBidder.java index f91babe767d..6fe52f327c8 100644 --- a/src/main/java/org/prebid/server/bidder/unruly/UnrulyBidder.java +++ b/src/main/java/org/prebid/server/bidder/unruly/UnrulyBidder.java @@ -1,13 +1,13 @@ package org.prebid.server.bidder.unruly; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -27,7 +27,6 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -40,8 +39,6 @@ public class UnrulyBidder implements Bidder { new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; - private final String endpointUrl; private final JacksonMapper mapper; @@ -115,14 +112,14 @@ private static MultiMap getHeaders() { public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - return bidResponse == null || bidResponse.getSeatbid() == null + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() : bidsFromResponse(bidRequest, bidResponse); } @@ -133,8 +130,7 @@ private static List bidsFromResponse(BidRequest bidRequest, BidRespon .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), - DEFAULT_BID_CURRENCY)) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); } @@ -146,9 +142,4 @@ private static BidType getBidType(String impId, List imps) { } throw new PreBidException(String.format("Failed to find impression %s", impId)); } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/valueimpression/ValueImpressionBidder.java b/src/main/java/org/prebid/server/bidder/valueimpression/ValueImpressionBidder.java index a709f359ac6..3982c5c7d82 100644 --- a/src/main/java/org/prebid/server/bidder/valueimpression/ValueImpressionBidder.java +++ b/src/main/java/org/prebid/server/bidder/valueimpression/ValueImpressionBidder.java @@ -1,14 +1,13 @@ package org.prebid.server.bidder.valueimpression; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -22,13 +21,11 @@ import org.prebid.server.proto.openrtb.ext.request.valueimpression.ExtImpValueImpression; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; -import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -38,8 +35,6 @@ public class ValueImpressionBidder implements Bidder { new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; - private final String endpointUrl; private final JacksonMapper mapper; @@ -61,10 +56,9 @@ public Result>> makeHttpRequests(BidRequest request .body(mapper.encode(bidRequest)) .payload(request) .build()), - Collections.emptyList() - ); + Collections.emptyList()); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badInput(e.getMessage())); + return Result.withError(BidderError.badInput(e.getMessage())); } } @@ -92,22 +86,17 @@ private boolean isParsedImp(Imp imp) { @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); return extractBids(httpCall.getRequest().getPayload(), bidResponse); } catch (DecodeException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private Result> extractBids(BidRequest request, BidResponse bidResponse) { - if (bidResponse == null || bidResponse.getSeatbid() == null) { - return Result.of(Collections.emptyList(), Collections.emptyList()); + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Result.empty(); } final List responseBids = bidResponse.getSeatbid().stream() .filter(Objects::nonNull) @@ -116,26 +105,24 @@ private Result> extractBids(BidRequest request, BidResponse bidR .flatMap(Collection::stream) .collect(Collectors.toList()); final List errors = new ArrayList<>(); - final List result = bidsFromResponse(request.getImp(), responseBids, errors); + final List result = bidsFromResponse(request.getImp(), responseBids, bidResponse.getCur(), errors); return Result.of(result, errors); } - private static List bidsFromResponse(List imps, List responseBids, List errors) { + private static List bidsFromResponse(List imps, List responseBids, String currency, + List errors) { final List bidderBids = new ArrayList<>(); for (Bid bid : responseBids) { try { - final BidType bidType = resolveBidType(bid.getImpid(), imps); - bidderBids.add(BidderBid.of(bid, bidType, DEFAULT_BID_CURRENCY)); + bidderBids.add(BidderBid.of(bid, getBidType(bid.getImpid(), imps), currency)); } catch (PreBidException e) { - errors.add(BidderError.badInput( - String.format("bid id=%s %s", bid.getId(), e.getMessage())) - ); + errors.add(BidderError.badInput(String.format("bid id=%s %s", bid.getId(), e.getMessage()))); } } return bidderBids; } - private static BidType resolveBidType(String impId, List imps) { + private static BidType getBidType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId)) { if (imp.getBanner() != null) { @@ -147,9 +134,4 @@ private static BidType resolveBidType(String impId, List imps) { } throw new PreBidException(String.format("could not find valid impid=%s", impId)); } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidder.java b/src/main/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidder.java index e293b186e09..cf8c6a19e51 100644 --- a/src/main/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidder.java +++ b/src/main/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidder.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -32,7 +33,6 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -41,7 +41,6 @@ public class VerizonmediaBidder implements Bidder { private static final TypeReference> VERIZON_EXT_TYPE_REFERENCE = new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; private final String endpointUrl; private final JacksonMapper mapper; @@ -74,8 +73,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ private ExtImpVerizonmedia parseAndValidateImpExt(ObjectNode impExtNode, int index) { final ExtImpVerizonmedia extImpVerizonmedia; try { - extImpVerizonmedia = mapper.mapper().convertValue(impExtNode, - VERIZON_EXT_TYPE_REFERENCE).getBidder(); + extImpVerizonmedia = mapper.mapper().convertValue(impExtNode, VERIZON_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { throw new PreBidException(String.format("imp #%s: %s", index, e.getMessage())); } @@ -113,12 +111,18 @@ private static BidRequest modifyRequest(BidRequest request, Imp imp, ExtImpVeriz impBuilder.banner(modifyBanner(banner)); } + final BidRequest.BidRequestBuilder requestBuilder = request.toBuilder(); + final Site site = request.getSite(); - final Site.SiteBuilder siteBuilder = site == null ? Site.builder() : site.toBuilder(); + final App app = request.getApp(); + if (site != null) { + requestBuilder.site(site.toBuilder().id(extImpVerizonmedia.getDcn()).build()); + } else if (app != null) { + requestBuilder.app(app.toBuilder().id(extImpVerizonmedia.getDcn()).build()); + } - return request.toBuilder() + return requestBuilder .imp(Collections.singletonList(impBuilder.build())) - .site(siteBuilder.id(extImpVerizonmedia.getDcn()).build()) .build(); } @@ -146,11 +150,11 @@ private HttpRequest makeHttpRequest(BidRequest outgoingRequest) { } private static MultiMap makeHeaders(Device device) { - final String deviceUa = device != null ? device.getUa() : null; - final MultiMap headers = HttpUtil.headers() - .add("x-openrtb-version", "2.5"); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, "User-Agent", deviceUa); + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + + final String deviceUa = device != null ? device.getUa() : null; + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, deviceUa); return headers; } @@ -161,18 +165,18 @@ public Result> makeBids(HttpCall httpCall, BidReques final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); return Result.of(extractBids(bidResponse, httpCall.getRequest().getPayload()), Collections.emptyList()); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private static List extractBids(BidResponse bidResponse, BidRequest bidRequest) { - final List seatbid = bidResponse != null ? bidResponse.getSeatbid() : null; - if (seatbid == null) { + final List seatBids = bidResponse != null ? bidResponse.getSeatbid() : null; + if (seatBids == null) { return Collections.emptyList(); } - if (seatbid.isEmpty()) { - throw new PreBidException(String.format("Invalid SeatBids count: %d", seatbid.size())); + if (seatBids.isEmpty()) { + throw new PreBidException(String.format("Invalid SeatBids count: %d", seatBids.size())); } return bidsFromResponse(bidResponse, bidRequest.getImp()); } @@ -184,7 +188,7 @@ private static List bidsFromResponse(BidResponse bidResponse, List checkBid(bid.getImpid(), imps)) - .map(bid -> BidderBid.of(bid, BidType.banner, DEFAULT_BID_CURRENCY)) + .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) .collect(Collectors.toList()); } @@ -196,9 +200,4 @@ private static boolean checkBid(String bidImpId, List imps) { } throw new PreBidException(String.format("Unknown ad unit code '%s'", bidImpId)); } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java b/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java index 9b552e9e58d..e02f40cf8a5 100644 --- a/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java +++ b/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java @@ -1,6 +1,5 @@ package org.prebid.server.bidder.visx; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.Bid; import io.vertx.core.http.HttpMethod; @@ -23,7 +22,6 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -78,14 +76,14 @@ private void modifyRequest(BidRequest bidRequest, BidRequest.BidRequestBuilder r public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final VisxResponse visxResponse = mapper.decodeValue(httpCall.getResponse().getBody(), VisxResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), visxResponse), Collections.emptyList()); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), visxResponse)); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private List extractBids(BidRequest bidRequest, VisxResponse visxResponse) { - if (visxResponse == null || visxResponse.getSeatbid() == null) { + if (visxResponse == null || CollectionUtils.isEmpty(visxResponse.getSeatbid())) { return Collections.emptyList(); } return bidsFromResponse(bidRequest, visxResponse); @@ -110,9 +108,4 @@ private List bidsFromResponse(BidRequest bidRequest, VisxResponse vis .build(), BidType.banner, DEFAULT_BID_CURRENCY)) .collect(Collectors.toList()); } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java b/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java index 5c436cbb863..5053482f201 100644 --- a/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java +++ b/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java @@ -1,6 +1,8 @@ package org.prebid.server.bidder.vrtcal; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.OpenrtbBidder; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.request.vrtcal.ExtImpVrtcal; @@ -8,6 +10,9 @@ import java.util.List; +/** + * Vrtcal {@link Bidder} implementation. + */ public class VrtcalBidder extends OpenrtbBidder { public VrtcalBidder(String endpointUrl, JacksonMapper mapper) { @@ -15,7 +20,7 @@ public VrtcalBidder(String endpointUrl, JacksonMapper mapper) { } @Override - protected BidType getBidType(String impId, List imps) { + protected BidType getBidType(Bid bid, List imps) { return BidType.banner; } } diff --git a/src/main/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidder.java b/src/main/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidder.java index b08407ba227..11dd62c8519 100644 --- a/src/main/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidder.java +++ b/src/main/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidder.java @@ -1,5 +1,6 @@ package org.prebid.server.bidder.yeahmobi; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -8,9 +9,8 @@ import com.iab.openrtb.request.Native; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.http.HttpMethod; -import lombok.SneakyThrows; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -29,18 +29,18 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +/** + * Yeahmobi {@link Bidder} implementation. + */ public class YeahmobiBidder implements Bidder { private static final TypeReference> YEAHMOBI_EXT_TYPE_REFERENCE = new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; - private final String endpointUrl; private final JacksonMapper mapper; @@ -53,7 +53,6 @@ public YeahmobiBidder(String endpointUrl, JacksonMapper mapper) { public Result>> makeHttpRequests(BidRequest request) { final List errors = new ArrayList<>(); final List validImps = new ArrayList<>(); - ExtImpYeahmobi extImpYeahmobi = null; for (Imp imp : request.getImp()) { try { @@ -66,10 +65,10 @@ public Result>> makeHttpRequests(BidRequest request } if (extImpYeahmobi == null) { - return Result.emptyWithError(BidderError.badInput("Invalid ExtImpYeahmobi value")); + return Result.withError(BidderError.badInput("Invalid ExtImpYeahmobi value")); } - final String host = String.format("gw-%s-bid.yeahtargeter.com", HttpUtil.encodeUrl(extImpYeahmobi.getZoneId())); + final String host = String.format("gw-%s-bid.yeahtargeter.com", extImpYeahmobi.getZoneId()); final String url = endpointUrl.replace("{{Host}}", host); final BidRequest outgoingRequest = request.toBuilder().imp(validImps).build(); @@ -91,49 +90,51 @@ private ExtImpYeahmobi parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), YEAHMOBI_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException(String.format("Impression id=%s, has invalid Ext", imp.getId())); } } - @SneakyThrows private Imp processImp(Imp imp) { final Native xNative = imp.getXNative(); if (xNative != null) { - final JsonNode nativeRequest = xNative.getRequest() != null - ? mapper.mapper().readValue(xNative.getRequest(), JsonNode.class) - : null; - - final String newNativeRequest; - final ObjectNode objectNode = mapper.mapper().createObjectNode().set("native", nativeRequest); - newNativeRequest = nativeRequest == null || nativeRequest.get("native") == null - ? mapper.mapper().writeValueAsString(objectNode) - : null; - - return newNativeRequest != null - ? imp.toBuilder().xNative(Native.builder().request(newNativeRequest).build()).build() + final String resolvedNativeRequest = resolveNativeRequest(xNative.getRequest()); + return resolvedNativeRequest != null + ? imp.toBuilder().xNative(Native.builder().request(resolvedNativeRequest).build()).build() : imp; } return imp; } - @Override - public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); + private String resolveNativeRequest(String xNativeRequest) { + try { + final JsonNode nativeRequest = xNativeRequest != null + ? mapper.mapper().readValue(xNativeRequest, JsonNode.class) + : mapper.mapper().createObjectNode(); + + if (nativeRequest.isEmpty() || nativeRequest.get("native") == null) { + final ObjectNode objectNode = mapper.mapper().createObjectNode().set("native", nativeRequest); + return mapper.mapper().writeValueAsString(objectNode); + } + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); } + return null; + } + + @Override + public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); } catch (DecodeException | PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } private List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - if (bidResponse == null || bidResponse.getSeatbid() == null) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } return bidsFromResponse(bidRequest, bidResponse); @@ -145,7 +146,7 @@ private List bidsFromResponse(BidRequest bidRequest, BidResponse bidR .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), DEFAULT_BID_CURRENCY)) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) .collect(Collectors.toList()); } @@ -163,9 +164,4 @@ protected BidType getBidType(String impId, List imps) { } return BidType.banner; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java index 0ed9a67006c..29aca137843 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -12,7 +11,7 @@ import com.iab.openrtb.request.Site; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; -import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpHeaderValues; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.lang3.ObjectUtils; @@ -37,6 +36,7 @@ import org.prebid.server.util.HttpUtil; import java.math.BigDecimal; +import java.net.URISyntaxException; import java.time.Instant; import java.util.ArrayList; import java.util.Calendar; @@ -52,7 +52,7 @@ public class YieldlabBidder implements Bidder { new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "EUR"; + private static final String BID_CURRENCY = "EUR"; private static final String AD_SLOT_ID_SEPARATOR = ","; private static final String AD_SIZE_SEPARATOR = "x"; private static final String CREATIVE_ID = "%s%s%s"; @@ -69,55 +69,74 @@ public YieldlabBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final List extImps = collectImpExt(request.getImp()); - final ExtImpYieldlab modifiedExtImp = constructExtImp(extImps); - - return Result.of(Collections.singletonList( - HttpRequest.builder() - .method(HttpMethod.GET) - .uri(makeUrl(modifiedExtImp, request)) - .body(null) - .headers(getHeaders(request)) - .payload(null) - .build()), Collections.emptyList()); - } - - private List collectImpExt(List imps) { - return imps.stream() - .map(this::parseImpExt) - .collect(Collectors.toList()); - } + final ExtImpYieldlab modifiedExtImp = constructExtImp(request.getImp()); - private ExtImpYieldlab parseImpExt(Imp imp) { + final String uri; try { - return mapper.mapper().convertValue(imp.getExt(), YIELDLAB_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage(), e); + uri = makeUrl(modifiedExtImp, request); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); } + + return Result.withValue(HttpRequest.builder() + .method(HttpMethod.GET) + .uri(uri) + .headers(resolveHeaders(request.getSite(), request.getDevice(), request.getUser())) + .build()); } - private ExtImpYieldlab constructExtImp(List extImps) { + private ExtImpYieldlab constructExtImp(List imps) { + final List extImps = collectImpExt(imps); + final List adSlotIds = extImps.stream() .map(ExtImpYieldlab::getAdslotId) + .filter(Objects::nonNull) .collect(Collectors.toList()); final Map targeting = extImps.stream() .map(ExtImpYieldlab::getTargeting) + .filter(Objects::nonNull) .flatMap(map -> map.entrySet().stream()) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + .filter(entry -> entry.getKey() != null) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (channel1, channel2) -> channel1)); final String adSlotIdsParams = adSlotIds.stream().sorted().collect(Collectors.joining(AD_SLOT_ID_SEPARATOR)); return ExtImpYieldlab.builder().adslotId(adSlotIdsParams).targeting(targeting).build(); } + private List collectImpExt(List imps) { + final List extImps = new ArrayList<>(); + for (Imp imp : imps) { + final ExtImpYieldlab extImpYieldlab = parseImpExt(imp); + if (extImpYieldlab != null) { + extImps.add(extImpYieldlab); + } + } + return extImps; + } + + private ExtImpYieldlab parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), YIELDLAB_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + return null; + } + } + private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { // for passing validation tests final String timestamp = isDebugEnabled(request) ? "200000" : String.valueOf(Instant.now().getEpochSecond()); final String updatedPath = String.format("%s/%s", endpointUrl, extImpYieldlab.getAdslotId()); - final URIBuilder uriBuilder = new URIBuilder() - .setPath(updatedPath) + final URIBuilder uriBuilder; + try { + uriBuilder = new URIBuilder(updatedPath); + } catch (URISyntaxException e) { + throw new PreBidException(String.format("Invalid url: %s, error: %s", updatedPath, e.getMessage())); + } + + uriBuilder .addParameter("content", "json") .addParameter("pvid", "true") .addParameter("ts", timestamp) @@ -130,18 +149,18 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { final Device device = request.getDevice(); if (device != null) { - uriBuilder.addParameter("yl_rtb_ifa", device.getIfa()) - .addParameter("yl_rtb_devicetype", String.format("%s", device.getDevicetype())); + uriBuilder.addParameter("yl_rtb_ifa", device.getIfa()); - final Integer connectiontype = device.getConnectiontype(); - if (connectiontype != null) { - uriBuilder.addParameter("yl_rtb_connectiontype", String.format("%s", connectiontype)); + uriBuilder.addParameter("yl_rtb_devicetype", resolveNumberParameter(device.getDevicetype())); + final Integer connectionType = device.getConnectiontype(); + if (connectionType != null) { + uriBuilder.addParameter("yl_rtb_connectiontype", device.getConnectiontype().toString()); } final Geo geo = device.getGeo(); if (geo != null) { - uriBuilder.addParameter("lat", String.format("%s", geo.getLat())) - .addParameter("lon", String.format("%s", geo.getLon())); + uriBuilder.addParameter("lat", resolveNumberParameter(geo.getLat())); + uriBuilder.addParameter("lon", resolveNumberParameter(geo.getLon())); } } @@ -164,7 +183,7 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { /** * Determines debug flag from {@link BidRequest} or {@link ExtRequest}. */ - private boolean isDebugEnabled(BidRequest bidRequest) { + private static boolean isDebugEnabled(BidRequest bidRequest) { if (Objects.equals(bidRequest.getTest(), 1)) { return true; } @@ -184,40 +203,37 @@ private String getTargetingValues(ExtImpYieldlab extImpYieldlab) { return uriBuilder.toString().replace("?", ""); } - private String getGdprParameter(Regs regs) { + private static String getGdprParameter(Regs regs) { if (regs != null) { final Integer gdpr = regs.getExt() != null ? regs.getExt().getGdpr() : null; if (gdpr != null && (gdpr == 0 || gdpr == 1)) { - return String.valueOf(gdpr); + return gdpr.toString(); } } return ""; } - private String getConsentParameter(User user) { + private static String getConsentParameter(User user) { final ExtUser extUser = user != null ? user.getExt() : null; final String consent = extUser != null ? extUser.getConsent() : null; return ObjectUtils.defaultIfNull(consent, ""); } - private static MultiMap getHeaders(BidRequest request) { - final MultiMap headers = HttpUtil.headers(); - final Site site = request.getSite(); + private static MultiMap resolveHeaders(Site site, Device device, User user) { + final MultiMap headers = MultiMap.caseInsensitiveMultiMap() + .add(HttpUtil.ACCEPT_HEADER, HttpHeaderValues.APPLICATION_JSON); if (site != null) { HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER.toString(), site.getPage()); } - final Device device = request.getDevice(); if (device != null) { HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER.toString(), device.getUa()); HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER.toString(), device.getIp()); } - final User user = request.getUser(); - if (user != null) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.COOKIE_HEADER.toString(), - String.format("id=%s", user.getBuyeruid())); + if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) { + headers.add(HttpUtil.COOKIE_HEADER.toString(), String.format("id=%s", user.getBuyeruid())); } return headers; @@ -225,92 +241,127 @@ private static MultiMap getHeaders(BidRequest request) { @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - final List yieldlabResponses; try { yieldlabResponses = decodeBodyToBidList(httpCall); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } - final List extImps = collectImpExt(bidRequest.getImp()); final List bidderBids = new ArrayList<>(); for (int i = 0; i < yieldlabResponses.size(); i++) { - final YieldlabResponse yieldlabResponse = yieldlabResponses.get(i); - final String[] sizeParts = yieldlabResponse.getAdSize().split(AD_SIZE_SEPARATOR); - final int width; - final int height; - if (sizeParts.length != 2) { - width = 0; - height = 0; - } else { - width = StringUtils.isNumeric(sizeParts[0]) ? Integer.parseInt(sizeParts[0]) : 0; - height = StringUtils.isNumeric(sizeParts[1]) ? Integer.parseInt(sizeParts[1]) : 0; + final BidderBid bidderBid; + try { + bidderBid = resolveBidderBid(yieldlabResponses, i, bidRequest); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); } - final ExtImpYieldlab filteredExtImp = filterExtImp(yieldlabResponse.getId(), extImps); - if (filteredExtImp == null) { - return Result.emptyWithError(BidderError.badInput("Invalid extension")); + if (bidderBid != null) { + bidderBids.add(bidderBid); } - final Imp currentImp = bidRequest.getImp().get(i); - final Bid.BidBuilder updatedBid = Bid.builder() - .id(String.valueOf(yieldlabResponse.getId())) - .price(BigDecimal.valueOf(yieldlabResponse.getPrice() / 100)) - .impid(currentImp.getId()) - .dealid(String.valueOf(yieldlabResponse.getPid())) - .crid(makeCreativeId(bidRequest, yieldlabResponse, filteredExtImp)) - .w(width) - .h(height); - - BidType bidType; - if (currentImp.getVideo() != null) { - bidType = BidType.video; - updatedBid.nurl(makeNurl(bidRequest, filteredExtImp, yieldlabResponse)); - } else if (currentImp.getBanner() != null) { - bidType = BidType.banner; - updatedBid.adm(makeAdm(bidRequest, filteredExtImp, yieldlabResponse)); - } else { - continue; - } - - final BidderBid bidderBid = BidderBid.of(updatedBid.build(), bidType, DEFAULT_BID_CURRENCY); - bidderBids.add(bidderBid); } return Result.of(bidderBids, Collections.emptyList()); } + private BidderBid resolveBidderBid(List yieldlabResponses, + int currentImpIndex, BidRequest bidRequest) { + final YieldlabResponse yieldlabResponse = yieldlabResponses.get(currentImpIndex); + + final ExtImpYieldlab matchedExtImp = getMatchedExtImp(yieldlabResponse.getId(), bidRequest.getImp()); + if (matchedExtImp == null) { + throw new PreBidException("Invalid extension"); + } + + final Imp currentImp = bidRequest.getImp().get(currentImpIndex); + if (currentImp == null) { + throw new PreBidException(String.format("Imp not present for id %s", currentImpIndex)); + } + final Bid.BidBuilder updatedBid = Bid.builder(); + + BidType bidType; + if (currentImp.getVideo() != null) { + bidType = BidType.video; + updatedBid.nurl(makeNurl(bidRequest, matchedExtImp, yieldlabResponse)); + } else if (currentImp.getBanner() != null) { + bidType = BidType.banner; + updatedBid.adm(makeAdm(bidRequest, matchedExtImp, yieldlabResponse)); + } else { + return null; + } + + addBidParams(yieldlabResponse, bidRequest, updatedBid) + .impid(currentImp.getId()); + + return BidderBid.of(updatedBid.build(), bidType, BID_CURRENCY); + } + private List decodeBodyToBidList(HttpCall httpCall) { try { return mapper.mapper().readValue( httpCall.getResponse().getBody(), mapper.mapper().getTypeFactory().constructCollectionType(List.class, YieldlabResponse.class)); } catch (DecodeException | JsonProcessingException e) { - throw new PreBidException(e.getMessage(), e); + throw new PreBidException(e.getMessage()); } } - private ExtImpYieldlab filterExtImp(Integer responseId, List extImps) { - return extImps.stream() + private ExtImpYieldlab getMatchedExtImp(Integer responseId, List imps) { + return collectImpExt(imps).stream() .filter(ext -> ext.getAdslotId().equals(String.valueOf(responseId))) .findFirst() .orElse(null); } - private String makeCreativeId(BidRequest bidRequest, YieldlabResponse yieldlabResponse, ExtImpYieldlab extImp) { + private Bid.BidBuilder addBidParams(YieldlabResponse yieldlabResponse, BidRequest bidRequest, + Bid.BidBuilder updatedBid) { + final ExtImpYieldlab matchedExtImp = getMatchedExtImp(yieldlabResponse.getId(), bidRequest.getImp()); + + if (matchedExtImp == null) { + throw new PreBidException("Invalid extension"); + } + + updatedBid.id(resolveNumberParameter(yieldlabResponse.getId())) + .price(resolvePrice(yieldlabResponse.getPrice())) + .dealid(resolveNumberParameter(yieldlabResponse.getPid())) + .crid(makeCreativeId(bidRequest, yieldlabResponse, matchedExtImp)) + .w(resolveSizeParameter(yieldlabResponse.getAdSize(), true)) + .h(resolveSizeParameter(yieldlabResponse.getAdSize(), false)); + + return updatedBid; + } + + private static BigDecimal resolvePrice(Double price) { + return price != null ? BigDecimal.valueOf(price / 100) : null; + } + + private static String resolveNumberParameter(Number param) { + return param != null ? String.valueOf(param) : null; + } + + private static String makeCreativeId(BidRequest bidRequest, YieldlabResponse yieldlabResponse, + ExtImpYieldlab extImp) { // for passing validation tests final int weekNumber = isDebugEnabled(bidRequest) ? 35 : Calendar.getInstance().get(Calendar.WEEK_OF_YEAR); return String.format(CREATIVE_ID, extImp.getAdslotId(), yieldlabResponse.getPid(), weekNumber); } + private static Integer resolveSizeParameter(String adSize, boolean isWidth) { + final String[] sizeParts = adSize.split(AD_SIZE_SEPARATOR); + + if (sizeParts.length != 2) { + return 0; + } + final int sizeIndex = isWidth ? 0 : 1; + return StringUtils.isNumeric(sizeParts[sizeIndex]) ? Integer.parseInt(sizeParts[sizeIndex]) : 0; + } + private String makeAdm(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, YieldlabResponse yieldlabResponse) { return String.format(AD_SOURCE_BANNER, makeNurl(bidRequest, extImpYieldlab, yieldlabResponse)); } - private String makeNurl(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, YieldlabResponse yieldlabResponse) { + private static String makeNurl(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, + YieldlabResponse yieldlabResponse) { // for passing validation tests final String timestamp = isDebugEnabled(bidRequest) ? "200000" : String.valueOf(Instant.now().getEpochSecond()); @@ -334,9 +385,4 @@ private String makeNurl(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, Yi return String.format(AD_SOURCE_URL, extImpYieldlab.getAdslotId(), extImpYieldlab.getSupplyId(), yieldlabResponse.getAdSize(), uriBuilder.toString().replace("?", "")); } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java b/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java index 2354ed96b29..09245179540 100644 --- a/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java @@ -1,6 +1,8 @@ package org.prebid.server.bidder.yieldmo; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.OpenrtbBidder; import org.prebid.server.bidder.yieldmo.proto.YieldmoImpExt; import org.prebid.server.exception.PreBidException; @@ -10,6 +12,9 @@ import java.util.List; +/** + * Yieldmo {@link Bidder} implementation. + */ public class YieldmoBidder extends OpenrtbBidder { public YieldmoBidder(String endpointUrl, JacksonMapper mapper) { @@ -30,7 +35,15 @@ protected Imp modifyImp(Imp imp, ExtImpYieldmo impExt) throws PreBidException { } @Override - protected BidType getBidType(String impId, List imps) { - return BidType.banner; + protected BidType getBidType(Bid bid, List imps) { + final String impId = bid.getImpid(); + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getBanner() != null) { + return BidType.banner; + } + } + } + return BidType.video; } } diff --git a/src/main/java/org/prebid/server/bidder/yieldone/YieldoneBidder.java b/src/main/java/org/prebid/server/bidder/yieldone/YieldoneBidder.java index 4ee0a7f97e5..1384988f7fa 100644 --- a/src/main/java/org/prebid/server/bidder/yieldone/YieldoneBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldone/YieldoneBidder.java @@ -1,14 +1,12 @@ package org.prebid.server.bidder.yieldone; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; @@ -29,7 +27,6 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -41,7 +38,7 @@ public class YieldoneBidder implements Bidder { private static final TypeReference> YIELDONE_EXT_TYPE_REFERENCE = new TypeReference>() { }; - private static final String DEFAULT_BID_CURRENCY = "USD"; + private final String endpointUrl; private final JacksonMapper mapper; @@ -57,8 +54,8 @@ public Result>> makeHttpRequests(BidRequest request for (Imp imp : request.getImp()) { try { + validateImpExt(imp); final Imp updatedImp = modifyImp(imp); - validateImpExt(updatedImp); validImps.add(updatedImp); } catch (PreBidException e) { @@ -67,15 +64,14 @@ public Result>> makeHttpRequests(BidRequest request } final BidRequest outgoingRequest = request.toBuilder().imp(validImps).build(); - final String body = mapper.encode(outgoingRequest); return Result.of(Collections.singletonList( HttpRequest.builder() .method(HttpMethod.POST) .uri(endpointUrl) .headers(HttpUtil.headers()) + .body(mapper.encode(outgoingRequest)) .payload(outgoingRequest) - .body(body) .build()), errors); } @@ -97,8 +93,7 @@ private Imp modifyImp(Imp imp) { private void validateImpExt(Imp imp) { try { - final ExtImpYieldone extImpYieldone = mapper.mapper().convertValue(imp.getExt(), - YIELDONE_EXT_TYPE_REFERENCE).getBidder(); + mapper.mapper().convertValue(imp.getExt(), YIELDONE_EXT_TYPE_REFERENCE); } catch (IllegalArgumentException e) { throw new PreBidException(e.getMessage(), e); } @@ -106,24 +101,24 @@ private void validateImpExt(Imp imp) { @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - try { final BidResponse bidResponse = decodeBodyToBidResponse(httpCall); + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Result.empty(); + } + final List bidderBids = bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), - DEFAULT_BID_CURRENCY)) + bidResponse.getCur())) .collect(Collectors.toList()); - return Result.of(bidderBids, Collections.emptyList()); + + return Result.withValues(bidderBids); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badInput(e.getMessage())); + return Result.withError(BidderError.badInput(e.getMessage())); } } @@ -145,12 +140,6 @@ private static BidType getBidType(String impId, List imps) { } } } - throw new PreBidException(String.format("Failed to find impression %s", impId)); - } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); + throw new PreBidException(String.format("Unknown impression type with id %s", impId)); } } - diff --git a/src/main/java/org/prebid/server/bidder/zeroclickfraud/ZeroclickfraudBidder.java b/src/main/java/org/prebid/server/bidder/zeroclickfraud/ZeroclickfraudBidder.java index 31f1893a379..22aa638a381 100644 --- a/src/main/java/org/prebid/server/bidder/zeroclickfraud/ZeroclickfraudBidder.java +++ b/src/main/java/org/prebid/server/bidder/zeroclickfraud/ZeroclickfraudBidder.java @@ -6,7 +6,6 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -61,7 +60,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ final ExtImpZeroclickfraud extImpZeroclickfraud = parseAndValidateImpExt(imp.getExt()); extToImps.computeIfAbsent(extImpZeroclickfraud, ext -> new ArrayList<>()).add(imp); } catch (PreBidException e) { - return Result.emptyWithError(BidderError.badInput(e.getMessage())); + return Result.withError(BidderError.badInput(e.getMessage())); } } @@ -112,16 +111,11 @@ private HttpRequest makeHttpRequest(ExtImpZeroclickfraud extImpZeroc @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - final int statusCode = httpCall.getResponse().getStatusCode(); - if (statusCode == HttpResponseStatus.NO_CONTENT.code()) { - return Result.empty(); - } - try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); return Result.of(extractBids(bidResponse, httpCall.getRequest().getPayload()), Collections.emptyList()); } catch (DecodeException e) { - return Result.emptyWithError(BidderError.badServerResponse(e.getMessage())); + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } @@ -131,17 +125,18 @@ private static List extractBids(BidResponse bidResponse, BidRequest b : bidsFromResponse(bidResponse, bidRequest.getImp()); } - private static List bidsFromResponse(BidResponse bidResponse, List requestImps) { + private static List bidsFromResponse(BidResponse bidResponse, List imps) { return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getMediaType(bid.getImpid(), requestImps), bidResponse.getCur())) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), imps), bidResponse.getCur())) .collect(Collectors.toList()); } - private static BidType getMediaType(String impId, List requestImps) { - for (Imp imp : requestImps) { + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { if (imp.getId().equals(impId)) { if (imp.getVideo() != null) { return BidType.video; @@ -154,9 +149,4 @@ private static BidType getMediaType(String impId, List requestImps) { } return BidType.banner; } - - @Override - public Map extractTargeting(ObjectNode ext) { - return Collections.emptyMap(); - } } diff --git a/src/main/java/org/prebid/server/cache/CacheService.java b/src/main/java/org/prebid/server/cache/CacheService.java index 8d7064ccccd..963552037cb 100644 --- a/src/main/java/org/prebid/server/cache/CacheService.java +++ b/src/main/java/org/prebid/server/cache/CacheService.java @@ -5,22 +5,23 @@ import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.Imp; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import lombok.Value; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidInfo; import org.prebid.server.cache.model.CacheBid; import org.prebid.server.cache.model.CacheContext; import org.prebid.server.cache.model.CacheHttpRequest; import org.prebid.server.cache.model.CacheHttpResponse; -import org.prebid.server.cache.model.CacheIdInfo; +import org.prebid.server.cache.model.CacheInfo; import org.prebid.server.cache.model.CacheServiceResult; import org.prebid.server.cache.model.CacheTtl; import org.prebid.server.cache.model.DebugHttpCall; -import org.prebid.server.cache.proto.BidCacheResult; -import org.prebid.server.cache.proto.request.BannerValue; import org.prebid.server.cache.proto.request.BidCacheRequest; import org.prebid.server.cache.proto.request.PutObject; import org.prebid.server.cache.proto.response.BidCacheResponse; @@ -32,23 +33,22 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.metric.Metrics; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.MediaType; +import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.util.HttpUtil; +import org.prebid.server.vast.VastModifier; import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; import java.net.MalformedURLException; import java.net.URL; import java.time.Clock; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.function.Function; @@ -64,12 +64,15 @@ public class CacheService { private static final Logger logger = LoggerFactory.getLogger(CacheService.class); - public static final String BID_WURL_ATTRIBUTE = "wurl"; + private static final MultiMap CACHE_HEADERS = HttpUtil.headers(); + private static final Map> DEBUG_HEADERS = HttpUtil.toDebugHeaders(CACHE_HEADERS); + private static final String BID_WURL_ATTRIBUTE = "wurl"; private final CacheTtl mediaTypeCacheTtl; private final HttpClient httpClient; private final URL endpointUrl; private final String cachedAssetUrlTemplate; + private final VastModifier vastModifier; private final EventsService eventsService; private final Metrics metrics; private final Clock clock; @@ -79,6 +82,7 @@ public CacheService(CacheTtl mediaTypeCacheTtl, HttpClient httpClient, URL endpointUrl, String cachedAssetUrlTemplate, + VastModifier vastModifier, EventsService eventsService, Metrics metrics, Clock clock, @@ -88,6 +92,7 @@ public CacheService(CacheTtl mediaTypeCacheTtl, this.httpClient = Objects.requireNonNull(httpClient); this.endpointUrl = Objects.requireNonNull(endpointUrl); this.cachedAssetUrlTemplate = Objects.requireNonNull(cachedAssetUrlTemplate); + this.vastModifier = Objects.requireNonNull(vastModifier); this.eventsService = Objects.requireNonNull(eventsService); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); @@ -108,41 +113,6 @@ public String getCachedAssetURLTemplate() { return cachedAssetUrlTemplate; } - /** - * Makes cache for {@link Bid}s (legacy). - *

- * The returned result will always have the same number of elements as the values argument. - */ - public Future> cacheBids(List bids, Timeout timeout, String accountId) { - return doCache(bids, timeout, accountId, this::createPutObject, this::createBidCacheResult); - } - - /** - * Makes cache for {@link Bid}s with video media type only (legacy). - *

- * The returned result will always have the same number of elements as the values argument. - */ - public Future> cacheBidsVideoOnly(List bids, Timeout timeout, String accountId) { - return doCache(bids, timeout, accountId, CacheService::createPutObjectVideoOnly, this::createBidCacheResult); - } - - /** - * Generic method to work with cache service (legacy). - */ - private Future> doCache(List bids, - Timeout timeout, - String accountId, - Function requestItemCreator, - Function responseItemCreator) { - - final List cachedCreatives = bidsToCachedCreatives(bids, requestItemCreator); - - updateCreativeMetrics(accountId, cachedCreatives); - - return makeRequest(toBidCacheRequest(cachedCreatives), bids.size(), timeout, accountId) - .map(bidCacheResponse -> toResponse(bidCacheResponse, responseItemCreator)); - } - /** * Asks external prebid cache service to store the given value. */ @@ -159,7 +129,7 @@ private Future makeRequest( } final long startTime = clock.millis(); - return httpClient.post(endpointUrl.toString(), HttpUtil.headers(), mapper.encode(bidCacheRequest), + return httpClient.post(endpointUrl.toString(), CACHE_HEADERS, mapper.encode(bidCacheRequest), remainingTimeout) .map(response -> toBidCacheResponse( response.getStatusCode(), response.getBody(), bidCount, accountId, startTime)) @@ -186,12 +156,13 @@ private Future failResponse(Throwable exception, String accoun * The returned result will always have the number of elements equals putObjects list size. */ public Future cachePutObjects(List putObjects, + Boolean isEventsEnabled, Set biddersAllowingVastUpdate, String accountId, - String integration, Timeout timeout) { - + String integration, + Timeout timeout) { final List cachedCreatives = - updatePutObjects(putObjects, biddersAllowingVastUpdate, accountId, integration); + updatePutObjects(putObjects, isEventsEnabled, biddersAllowingVastUpdate, accountId, integration); updateCreativeMetrics(accountId, cachedCreatives); @@ -202,143 +173,106 @@ public Future cachePutObjects(List putObjects, * Modify VAST value in putObjects. */ private List updatePutObjects(List putObjects, - Set biddersAllowingVastUpdate, + Boolean isEventsEnabled, + Set allowedBidders, String accountId, String integration) { - - final List result = new ArrayList<>(); - - for (final PutObject putObject : putObjects) { - final PutObject.PutObjectBuilder builder = putObject.toBuilder() - // remove "/vtrack" specific fields - .bidid(null) - .bidder(null) - .timestamp(null); - - final JsonNode value = putObject.getValue(); - if (biddersAllowingVastUpdate.contains(putObject.getBidder()) && value != null) { - final String eventUrl = eventsService.vastUrlTracking( - putObject.getBidid(), - putObject.getBidder(), - accountId, - putObject.getTimestamp(), - integration); - final String updatedVastXml = appendTrackingUrlToVastXml(value.asText(), eventUrl); - builder.value(new TextNode(updatedVastXml)).build(); - } - - final PutObject payload = builder.build(); - - result.add(CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue()))); - } - - return result; + return putObjects.stream() + .map(putObject -> putObject.toBuilder() + // remove "/vtrack" specific fields + .bidid(null) + .bidder(null) + .timestamp(null) + .value(vastModifier.modifyVastXml(isEventsEnabled, + allowedBidders, + putObject, + accountId, + integration)) + .build()) + .map(payload -> CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue()))) + .collect(Collectors.toList()); } - /** - * Makes cache for OpenRTB {@link com.iab.openrtb.response.Bid}s. - */ - public Future cacheBidsOpenrtb(List bids, + public Future cacheBidsOpenrtb(List bidsToCache, AuctionContext auctionContext, CacheContext cacheContext, EventsContext eventsContext) { - - if (CollectionUtils.isEmpty(bids)) { + if (CollectionUtils.isEmpty(bidsToCache)) { return Future.succeededFuture(CacheServiceResult.empty()); } final List imps = auctionContext.getBidRequest().getImp(); - - final Map impIdToTtl = new HashMap<>(imps.size()); - boolean impWithNoExpExists = false; // indicates at least one impression without expire presents - final List videoImpIds = new ArrayList<>(); - final boolean shouldCacheVideoBids = cacheContext.isShouldCacheVideoBids(); - for (final Imp imp : imps) { - final String impId = imp.getId(); - impIdToTtl.put(impId, imp.getExp()); - impWithNoExpExists |= imp.getExp() == null; - if (shouldCacheVideoBids && impId != null && imp.getVideo() != null) { - videoImpIds.add(impId); - } - } + final boolean isAnyEmptyExpImp = imps.stream() + .map(Imp::getExp) + .anyMatch(Objects::isNull); final Account account = auctionContext.getAccount(); + final CacheTtl accountCacheTtl = accountCacheTtl(isAnyEmptyExpImp, account); - final List cacheBids = getCacheBids(cacheContext.isShouldCacheBids(), bids, impIdToTtl, - impWithNoExpExists, cacheContext.getCacheBidsTtl(), account); - - final List videoCacheBids = getVideoCacheBids(shouldCacheVideoBids, bids, - impIdToTtl, videoImpIds, impWithNoExpExists, cacheContext.getCacheVideoBidsTtl(), account); - - return doCacheOpenrtb( - cacheBids, - videoCacheBids, - auctionContext, - cacheContext.getBidderToVideoBidIdsToModify(), - cacheContext.getBidderToBidIds(), - eventsContext); - } - - /** - * Creates list of {@link CacheBid}s from the list of {@link com.iab.openrtb.response.Bid}s. - */ - private List getCacheBids(boolean shouldCacheBids, - List bids, - Map impIdToTtl, - boolean impWithNoExpExists, - Integer cacheBidsTtl, - Account account) { - - return shouldCacheBids - ? bids.stream() - .map(bid -> toCacheBid(bid, impIdToTtl, cacheBidsTtl, - accountCacheTtlFrom(impWithNoExpExists, account), false)) - .collect(Collectors.toList()) + final List cacheBids = cacheContext.isShouldCacheBids() + ? getCacheBids(bidsToCache, cacheContext.getCacheBidsTtl(), accountCacheTtl) : Collections.emptyList(); - } - /** - * Creates list of video {@link CacheBid}s from the list of {@link com.iab.openrtb.response.Bid}s. - */ - private List getVideoCacheBids( - boolean shouldCacheVideoBids, List bids, Map impIdToTtl, - List videoImpIds, boolean impWithNoExpExists, Integer cacheVideoBidsTtl, Account account) { - - return shouldCacheVideoBids - ? bids.stream() - .filter(bid -> videoImpIds.contains(bid.getImpid())) // bid is video - .map(bid -> toCacheBid(bid, impIdToTtl, cacheVideoBidsTtl, - accountCacheTtlFrom(impWithNoExpExists, account), true)) - .collect(Collectors.toList()) + final List videoCacheBids = cacheContext.isShouldCacheVideoBids() + ? getVideoCacheBids(bidsToCache, cacheContext.getCacheVideoBidsTtl(), accountCacheTtl) : Collections.emptyList(); + + return doCacheOpenrtb(cacheBids, videoCacheBids, auctionContext, eventsContext); } /** * Fetches {@link CacheTtl} from {@link Account}. *

* Returns empty {@link CacheTtl} when there are no impressions without expiration or - * if{@link Account} has neither of banner or video cache ttl. + * if {@link Account} has neither of banner or video cache ttl. */ - private CacheTtl accountCacheTtlFrom(boolean impWithNoExpExists, Account account) { - return impWithNoExpExists && (account.getBannerCacheTtl() != null || account.getVideoCacheTtl() != null) - ? CacheTtl.of(account.getBannerCacheTtl(), account.getVideoCacheTtl()) + private CacheTtl accountCacheTtl(boolean impWithNoExpExists, Account account) { + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + final Integer bannerCacheTtl = accountAuctionConfig != null ? accountAuctionConfig.getBannerCacheTtl() : null; + final Integer videoCacheTtl = accountAuctionConfig != null ? accountAuctionConfig.getVideoCacheTtl() : null; + + return impWithNoExpExists && (bannerCacheTtl != null || videoCacheTtl != null) + ? CacheTtl.of(bannerCacheTtl, videoCacheTtl) : CacheTtl.empty(); } + private List getCacheBids(List bidInfos, + Integer cacheBidsTtl, + CacheTtl accountCacheTtl) { + return bidInfos.stream() + .map(bidInfo -> toCacheBid(bidInfo, cacheBidsTtl, accountCacheTtl, false)) + .collect(Collectors.toList()); + } + + private List getVideoCacheBids(List bidInfos, + Integer cacheBidsTtl, + CacheTtl accountCacheTtl) { + return bidInfos.stream() + .filter(bidInfo -> bidInfo.getBidType().equals(BidType.video)) + .map(bidInfo -> toCacheBid(bidInfo, cacheBidsTtl, accountCacheTtl, true)) + .collect(Collectors.toList()); + } + /** - * Creates {@link CacheBid} from given {@link com.iab.openrtb.response.Bid} and determined cache ttl. + * Creates {@link CacheBid} from given {@link BidInfo} and determined cache ttl. */ - private CacheBid toCacheBid(com.iab.openrtb.response.Bid bid, Map impIdToTtl, Integer requestTtl, - CacheTtl accountCacheTtl, boolean isVideoBid) { + private CacheBid toCacheBid(BidInfo bidInfo, + Integer requestTtl, + CacheTtl accountCacheTtl, + boolean isVideoBid) { + final com.iab.openrtb.response.Bid bid = bidInfo.getBid(); final Integer bidTtl = bid.getExp(); - final Integer impTtl = impIdToTtl.get(bid.getImpid()); + final Imp correspondingImp = bidInfo.getCorrespondingImp(); + final Integer impTtl = correspondingImp != null ? correspondingImp.getExp() : null; final Integer accountMediaTypeTtl = isVideoBid - ? accountCacheTtl.getVideoCacheTtl() : accountCacheTtl.getBannerCacheTtl(); + ? accountCacheTtl.getVideoCacheTtl() + : accountCacheTtl.getBannerCacheTtl(); final Integer mediaTypeTtl = isVideoBid - ? mediaTypeCacheTtl.getVideoCacheTtl() : mediaTypeCacheTtl.getBannerCacheTtl(); + ? mediaTypeCacheTtl.getVideoCacheTtl() + : mediaTypeCacheTtl.getBannerCacheTtl(); final Integer ttl = ObjectUtils.firstNonNull(bidTtl, impTtl, requestTtl, accountMediaTypeTtl, mediaTypeTtl); - return CacheBid.of(bid, ttl); + return CacheBid.of(bidInfo, ttl); } /** @@ -352,17 +286,15 @@ private CacheBid toCacheBid(com.iab.openrtb.response.Bid bid, Map doCacheOpenrtb(List bids, List videoBids, AuctionContext auctionContext, - Map> bidderToVideoBidIdsToModify, - Map> biddersToCacheBidIds, EventsContext eventsContext) { final Account account = auctionContext.getAccount(); - + final String accountId = account.getId(); + final String requestId = auctionContext.getBidRequest().getId(); final List cachedCreatives = Stream.concat( - bids.stream().map(cacheBid -> createJsonPutObjectOpenrtb( - cacheBid, biddersToCacheBidIds, account, eventsContext)), - videoBids.stream().map(cacheBid -> createXmlPutObjectOpenrtb( - cacheBid, bidderToVideoBidIdsToModify, account, eventsContext))) + bids.stream().map(cacheBid -> + createJsonPutObjectOpenrtb(cacheBid, accountId, eventsContext)), + videoBids.stream().map(videoBid -> createXmlPutObjectOpenrtb(videoBid, requestId))) .collect(Collectors.toList()); if (cachedCreatives.isEmpty()) { @@ -377,17 +309,17 @@ private Future doCacheOpenrtb(List bids, final BidCacheRequest bidCacheRequest = toBidCacheRequest(cachedCreatives); - updateCreativeMetrics(account.getId(), cachedCreatives); + updateCreativeMetrics(accountId, cachedCreatives); final String url = endpointUrl.toString(); final String body = mapper.encode(bidCacheRequest); final CacheHttpRequest httpRequest = CacheHttpRequest.of(url, body); final long startTime = clock.millis(); - return httpClient.post(url, HttpUtil.headers(), body, remainingTimeout) + return httpClient.post(url, CACHE_HEADERS, body, remainingTimeout) .map(response -> processResponseOpenrtb( - response, httpRequest, cachedCreatives.size(), bids, videoBids, account.getId(), startTime)) - .otherwise(exception -> failResponseOpenrtb(exception, httpRequest, startTime)); + response, httpRequest, cachedCreatives.size(), bids, videoBids, accountId, startTime)) + .otherwise(exception -> failResponseOpenrtb(exception, accountId, httpRequest, startTime)); } /** @@ -419,10 +351,15 @@ private CacheServiceResult processResponseOpenrtb(HttpClientResponse response, /** * Handles errors occurred while HTTP request or response processing. */ - private CacheServiceResult failResponseOpenrtb(Throwable exception, CacheHttpRequest request, long startTime) { + private CacheServiceResult failResponseOpenrtb(Throwable exception, + String accountId, + CacheHttpRequest request, + long startTime) { logger.warn("Error occurred while interacting with cache service: {0}", exception.getMessage()); logger.debug("Error occurred while interacting with cache service", exception); + metrics.updateCacheRequestFailedTime(accountId, clock.millis() - startTime); + final DebugHttpCall httpCall = makeDebugHttpCall(endpointUrl.toString(), request, null, startTime); return CacheServiceResult.of(httpCall, exception, Collections.emptyMap()); } @@ -440,6 +377,7 @@ private DebugHttpCall makeDebugHttpCall(String endpoint, CacheHttpRequest httpRe .responseStatus(httpResponse != null ? httpResponse.getStatusCode() : null) .responseBody(httpResponse != null ? httpResponse.getBody() : null) .responseTimeMillis(responseTime(startTime)) + .requestHeaders(DEBUG_HEADERS) .build(); } @@ -450,174 +388,77 @@ private int responseTime(long startTime) { return Math.toIntExact(clock.millis() - startTime); } - /** - * Makes put object from {@link Bid}. Used for legacy auction request. - */ - private CachedCreative createPutObject(Bid bid) { - final PutObject payload = MediaType.video.equals(bid.getMediaType()) - ? videoPutObject(bid) - : bannerPutObject(bid); - - return CachedCreative.of(payload, creativeSizeFromAdm(bid)); - } - - /** - * Makes put object from {@link Bid} with video media type only. Used for legacy auction request. - */ - private static CachedCreative createPutObjectVideoOnly(Bid bid) { - if (!MediaType.video.equals(bid.getMediaType())) { - return null; - } - - return CachedCreative.of(videoPutObject(bid), creativeSizeFromAdm(bid)); - } - /** * Makes JSON type {@link PutObject} from {@link com.iab.openrtb.response.Bid}. * Used for OpenRTB auction request. Also, adds win url to result object if events are enabled. */ private CachedCreative createJsonPutObjectOpenrtb(CacheBid cacheBid, - Map> biddersToCacheBidIds, - Account account, + String accountId, EventsContext eventsContext) { - - final com.iab.openrtb.response.Bid bid = cacheBid.getBid(); + final BidInfo bidInfo = cacheBid.getBidInfo(); + final com.iab.openrtb.response.Bid bid = bidInfo.getBid(); final ObjectNode bidObjectNode = mapper.mapper().valueToTree(bid); - final String eventUrl = generateWinUrl(biddersToCacheBidIds, bid, account, eventsContext); + final String eventUrl = + generateWinUrl(bidInfo.getBidId(), + bidInfo.getBidder(), + accountId, + eventsContext, + bidInfo.getLineItemId()); if (eventUrl != null) { bidObjectNode.put(BID_WURL_ATTRIBUTE, eventUrl); } final PutObject payload = PutObject.builder() + .aid(eventsContext.getAuctionId()) .type("json") .value(bidObjectNode) - .expiry(cacheBid.getTtl()) + .ttlseconds(cacheBid.getTtl()) .build(); - return CachedCreative.of(payload, creativeSizeFromAdm(bid)); + return CachedCreative.of(payload, creativeSizeFromAdm(bid.getAdm())); } /** * Makes XML type {@link PutObject} from {@link com.iab.openrtb.response.Bid}. Used for OpenRTB auction request. */ - private CachedCreative createXmlPutObjectOpenrtb(CacheBid cacheBid, - Map> bidderToVideoBidIdsToModify, - Account account, - EventsContext eventsContext) { - - final com.iab.openrtb.response.Bid bid = cacheBid.getBid(); - final String vastXml = resolveVastXmlFrom(bid); - - final String eventUrl = generateVastUrlTracking(bidderToVideoBidIdsToModify, bid, account, eventsContext); - final String effectiveVastXml = eventUrl != null ? appendTrackingUrlToVastXml(vastXml, eventUrl) : vastXml; + private CachedCreative createXmlPutObjectOpenrtb(CacheBid cacheBid, String requestId) { + final BidInfo bidInfo = cacheBid.getBidInfo(); + final com.iab.openrtb.response.Bid bid = bidInfo.getBid(); + final String vastXml = bid.getAdm(); final PutObject payload = PutObject.builder() + .aid(requestId) .type("xml") - .value(new TextNode(effectiveVastXml)) - .expiry(cacheBid.getTtl()) + .value(vastXml != null ? new TextNode(vastXml) : null) + .ttlseconds(cacheBid.getTtl()) .build(); return CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue())); } - private static String resolveVastXmlFrom(com.iab.openrtb.response.Bid bid) { - if (bid.getAdm() == null) { - return "" - + "prebid.org wrapper" - + "" - + "" - + ""; + private String generateWinUrl(String bidId, + String bidder, + String accountId, + EventsContext eventsContext, + String lineItemId) { + if (!eventsContext.isEnabledForAccount()) { + return null; } - return bid.getAdm(); - } - - private String generateWinUrl(Map> biddersToCacheBidIds, - com.iab.openrtb.response.Bid bid, - Account account, - EventsContext eventsContext) { - - if (eventsContext.isEnabledForAccount() && eventsContext.isEnabledForRequest()) { - final String bidId = bid.getId(); - return findBidderForBidId(biddersToCacheBidIds, bidId) - .map(bidder -> eventsService.winUrl( - bidId, - bidder, - account.getId(), - eventsContext.getAuctionTimestamp(), - eventsContext.getIntegration())) - .orElse(null); + if (eventsContext.isEnabledForRequest() || StringUtils.isNotBlank(lineItemId)) { + return eventsService.winUrl( + bidId, + bidder, + accountId, + lineItemId, + eventsContext.isEnabledForRequest(), + eventsContext); } return null; } - private String generateVastUrlTracking(Map> bidderToVideoBidIdsToModify, - com.iab.openrtb.response.Bid bid, - Account account, - EventsContext eventsContext) { - - if (eventsContext.isEnabledForAccount()) { - final String bidId = bid.getId(); - return findBidderForBidId(bidderToVideoBidIdsToModify, bidId) - .map(bidder -> eventsService.vastUrlTracking( - bidId, - bidder, - account.getId(), - eventsContext.getAuctionTimestamp(), - eventsContext.getIntegration())) - .orElse(null); - } - - return null; - } - - private static Optional findBidderForBidId(Map> biddersToCacheBidIds, String bidId) { - return biddersToCacheBidIds.entrySet().stream() - .filter(biddersAndBidIds -> biddersAndBidIds.getValue().contains(bidId)) - .findFirst() - .map(Map.Entry::getKey); - } - - private String appendTrackingUrlToVastXml(String vastXml, String vastUrlTracking) { - final String closeTag = ""; - final int closeTagIndex = vastXml.indexOf(closeTag); - - // no impression tag - pass it as it is - if (closeTagIndex == -1) { - return vastXml; - } - - final String impressionUrl = ""; - final String openTag = ""; - - // empty impression tag - just insert the link - if (closeTagIndex - vastXml.indexOf(openTag) == openTag.length()) { - return vastXml.replaceFirst(openTag, openTag + impressionUrl); - } - - return vastXml.replaceFirst(closeTag, closeTag + openTag + impressionUrl + closeTag); - } - - private static List bidsToCachedCreatives( - List bids, Function requestItemCreator) { - - return bids.stream() - .filter(Objects::nonNull) - .map(requestItemCreator) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - /** - * Transforms {@link CacheObject} into {@link BidCacheResult}. Used for legacy auction request. - */ - private BidCacheResult createBidCacheResult(CacheObject cacheObject) { - final String uuid = cacheObject.getUuid(); - return BidCacheResult.of(uuid, cachedAssetUrlTemplate.concat(uuid)); - } - /** * Handles http response, analyzes response status and creates {@link BidCacheResponse} from response body * or throws {@link PreBidException} in case of errors. @@ -657,32 +498,40 @@ private List toResponse(BidCacheResponse bidCacheResponse, Function toResultMap( - List cacheBids, List cacheVideoBids, List uuids) { - final Map result = new HashMap<>(uuids.size()); - - final List bids = cacheBids.stream() - .map(CacheBid::getBid).collect(Collectors.toList()); - final List videoBids = cacheVideoBids.stream() - .map(CacheBid::getBid).collect(Collectors.toList()); + private static Map toResultMap(List cacheBids, + List cacheVideoBids, + List uuids) { + final Map result = new HashMap<>(uuids.size()); // here we assume "videoBids" is a sublist of "bids" // so, no need for a separate loop on "videoBids" if "bids" is not empty - if (!bids.isEmpty()) { - for (int i = 0; i < bids.size(); i++) { - final com.iab.openrtb.response.Bid bid = bids.get(i); + if (!cacheBids.isEmpty()) { + final List videoBids = cacheVideoBids.stream() + .map(CacheBid::getBidInfo) + .map(BidInfo::getBid) + .collect(Collectors.toList()); + + final int bidsSize = cacheBids.size(); + for (int i = 0; i < bidsSize; i++) { + final CacheBid cacheBid = cacheBids.get(i); + final BidInfo bidInfo = cacheBid.getBidInfo(); + final com.iab.openrtb.response.Bid bid = bidInfo.getBid(); + final Integer ttl = cacheBid.getTtl(); // determine uuid for video bid final int indexOfVideoBid = videoBids.indexOf(bid); - final String videoBidUuid = indexOfVideoBid != -1 ? uuids.get(bids.size() + indexOfVideoBid) : null; + final String videoBidUuid = indexOfVideoBid != -1 ? uuids.get(bidsSize + indexOfVideoBid) : null; + final Integer videoTtl = indexOfVideoBid != -1 ? cacheVideoBids.get(indexOfVideoBid).getTtl() : null; - result.put(bid, CacheIdInfo.of(uuids.get(i), videoBidUuid)); + result.put(bid, CacheInfo.of(uuids.get(i), videoBidUuid, ttl, videoTtl)); } } else { - for (int i = 0; i < videoBids.size(); i++) { - result.put(videoBids.get(i), CacheIdInfo.of(null, uuids.get(i))); + for (int i = 0; i < cacheVideoBids.size(); i++) { + final CacheBid cacheBid = cacheVideoBids.get(i); + final BidInfo bidInfo = cacheBid.getBidInfo(); + result.put(bidInfo.getBid(), CacheInfo.of(null, uuids.get(i), null, cacheBid.getTtl())); } } @@ -721,39 +570,14 @@ private static URL getCacheBaseUrl(String cacheSchema, String cacheHost) throws return new URL(cacheSchema + "://" + cacheHost); } - /** - * Creates video {@link PutObject} from the given {@link Bid}. Used for legacy auction request. - */ - private static PutObject videoPutObject(Bid bid) { - return PutObject.builder() - .type("xml") - .value(new TextNode(bid.getAdm())) - .build(); - } - - /** - * Creates banner {@link PutObject} from the given {@link Bid}. Used for legacy auction request. - */ - private PutObject bannerPutObject(Bid bid) { - return PutObject.builder() - .type("json") - .value(mapper.mapper().valueToTree(BannerValue.of(bid.getAdm(), bid.getNurl(), bid.getWidth(), - bid.getHeight()))) - .build(); - } - private void updateCreativeMetrics(String accountId, List cachedCreatives) { for (final CachedCreative cachedCreative : cachedCreatives) { metrics.updateCacheCreativeSize(accountId, cachedCreative.getSize()); } } - private static int creativeSizeFromAdm(com.iab.openrtb.response.Bid bid) { - return lengthOrZero(bid.getAdm()); - } - - private static int creativeSizeFromAdm(Bid bid) { - return lengthOrZero(bid.getAdm()); + private static int creativeSizeFromAdm(String adm) { + return lengthOrZero(adm); } private static int lengthOrZero(String adm) { diff --git a/src/main/java/org/prebid/server/cache/model/CacheBid.java b/src/main/java/org/prebid/server/cache/model/CacheBid.java index 1c63f23a454..a10ab9a7a3b 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheBid.java +++ b/src/main/java/org/prebid/server/cache/model/CacheBid.java @@ -3,6 +3,7 @@ import com.iab.openrtb.response.Bid; import lombok.AllArgsConstructor; import lombok.Value; +import org.prebid.server.auction.model.BidInfo; import org.prebid.server.cache.CacheService; /** @@ -12,7 +13,7 @@ @Value public class CacheBid { - Bid bid; + BidInfo bidInfo; Integer ttl; } diff --git a/src/main/java/org/prebid/server/cache/model/CacheContext.java b/src/main/java/org/prebid/server/cache/model/CacheContext.java index f26c975c212..4d64bf6e3ad 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheContext.java +++ b/src/main/java/org/prebid/server/cache/model/CacheContext.java @@ -3,9 +3,6 @@ import lombok.Builder; import lombok.Value; -import java.util.List; -import java.util.Map; - /** * Holds the state needed to perform caching response bids. */ @@ -20,8 +17,4 @@ public class CacheContext { boolean shouldCacheVideoBids; Integer cacheVideoBidsTtl; - - Map> bidderToVideoBidIdsToModify; - - Map> bidderToBidIds; } diff --git a/src/main/java/org/prebid/server/cache/model/CacheIdInfo.java b/src/main/java/org/prebid/server/cache/model/CacheIdInfo.java deleted file mode 100644 index 52618025055..00000000000 --- a/src/main/java/org/prebid/server/cache/model/CacheIdInfo.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.prebid.server.cache.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -/** - * Used to determine cache IDs targeting keywords should be in response - */ -@AllArgsConstructor(staticName = "of") -@Value -public class CacheIdInfo { - - private static final CacheIdInfo EMPTY = CacheIdInfo.of(null, null); - - /** - * Cache ID for whole bid - */ - String cacheId; - - /** - * Cache ID for VAST - */ - String videoCacheId; - - public static CacheIdInfo empty() { - return EMPTY; - } -} diff --git a/src/main/java/org/prebid/server/cache/model/CacheInfo.java b/src/main/java/org/prebid/server/cache/model/CacheInfo.java new file mode 100644 index 00000000000..5bfefcc1f97 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/model/CacheInfo.java @@ -0,0 +1,38 @@ +package org.prebid.server.cache.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Used to determine cache IDs targeting keywords should be in response + */ +@AllArgsConstructor(staticName = "of") +@Value +public class CacheInfo { + + private static final CacheInfo EMPTY = CacheInfo.of(null, null, null, null); + + /** + * Cache ID for whole bid + */ + String cacheId; + + /** + * Cache ID for VAST + */ + String videoCacheId; + + /** + * Cache TTL + */ + Integer ttl; + + /** + * Cache TTL for video + */ + Integer videoTtl; + + public static CacheInfo empty() { + return EMPTY; + } +} diff --git a/src/main/java/org/prebid/server/cache/model/CacheServiceResult.java b/src/main/java/org/prebid/server/cache/model/CacheServiceResult.java index 0a987f99115..e40d088faa0 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheServiceResult.java +++ b/src/main/java/org/prebid/server/cache/model/CacheServiceResult.java @@ -20,7 +20,7 @@ public class CacheServiceResult { Throwable error; - Map cacheBids; + Map cacheBids; public static CacheServiceResult empty() { return EMPTY; diff --git a/src/main/java/org/prebid/server/cache/model/DebugHttpCall.java b/src/main/java/org/prebid/server/cache/model/DebugHttpCall.java index ef30c0c6027..f995fa19ca9 100644 --- a/src/main/java/org/prebid/server/cache/model/DebugHttpCall.java +++ b/src/main/java/org/prebid/server/cache/model/DebugHttpCall.java @@ -3,6 +3,9 @@ import lombok.Builder; import lombok.Value; +import java.util.List; +import java.util.Map; + /** * Holds HTTP interaction related data. */ @@ -10,6 +13,8 @@ @Builder public class DebugHttpCall { + private static final DebugHttpCall EMPTY = DebugHttpCall.builder().build(); + String endpoint; String requestUri; @@ -20,5 +25,11 @@ public class DebugHttpCall { String responseBody; + Map> requestHeaders; + Integer responseTimeMillis; + + public static DebugHttpCall empty() { + return EMPTY; + } } diff --git a/src/main/java/org/prebid/server/cache/proto/request/PutObject.java b/src/main/java/org/prebid/server/cache/proto/request/PutObject.java index a68a689687b..4bcc44708e0 100644 --- a/src/main/java/org/prebid/server/cache/proto/request/PutObject.java +++ b/src/main/java/org/prebid/server/cache/proto/request/PutObject.java @@ -16,6 +16,8 @@ public class PutObject { Integer ttlseconds; + String aid; + String bidid; // this is "/vtrack" specific String bidder; // this is "/vtrack" specific diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieService.java b/src/main/java/org/prebid/server/cookie/UidsCookieService.java index 1fa3f1d0364..673eff707be 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookieService.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookieService.java @@ -11,6 +11,7 @@ import org.prebid.server.cookie.proto.Uids; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.model.HttpRequestContext; import org.prebid.server.util.HttpUtil; import java.time.Clock; @@ -81,8 +82,12 @@ public UidsCookieService(String optOutCookieName, *

* Note: UIDs will be excluded from resulting {@link UidsCookie} if their value are 'null'. */ - public UidsCookie parseFromRequest(RoutingContext context) { - return parseFromCookies(HttpUtil.cookiesAsMap(context)); + public UidsCookie parseFromRequest(RoutingContext routingContext) { + return parseFromCookies(HttpUtil.cookiesAsMap(routingContext)); + } + + public UidsCookie parseFromRequest(HttpRequestContext httpRequest) { + return parseFromCookies(HttpUtil.cookiesAsMap(httpRequest)); } /** diff --git a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java index 34912e03d19..640383281ac 100644 --- a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java +++ b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java @@ -1,5 +1,6 @@ package org.prebid.server.currency; +import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.logging.Logger; @@ -10,6 +11,9 @@ import org.prebid.server.currency.proto.CurrencyConversionRates; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.Initializable; @@ -19,7 +23,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; -import java.time.Clock; +import java.time.Duration; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; @@ -64,14 +68,16 @@ public CurrencyConversionService(ExternalConversionProperties externalConversion @Override public void initialize() { if (externalConversionProperties != null) { - final Long refreshPeriod = externalConversionProperties.getRefreshPeriod(); - final Long defaultTimeout = externalConversionProperties.getDefaultTimeout(); + final Long refreshPeriod = externalConversionProperties.getRefreshPeriodMs(); + final Long defaultTimeout = externalConversionProperties.getDefaultTimeoutMs(); final HttpClient httpClient = externalConversionProperties.getHttpClient(); final Vertx vertx = externalConversionProperties.getVertx(); vertx.setPeriodic(refreshPeriod, ignored -> populatesLatestCurrencyRates(currencyServerUrl, defaultTimeout, httpClient)); populatesLatestCurrencyRates(currencyServerUrl, defaultTimeout, httpClient); + + externalConversionProperties.getMetrics().createCurrencyRatesGauge(this::isRatesStale); } } @@ -82,7 +88,7 @@ private void populatesLatestCurrencyRates(String currencyServerUrl, Long default httpClient.get(currencyServerUrl, defaultTimeout) .map(this::processResponse) .map(this::updateCurrencyRates) - .recover(CurrencyConversionService::failResponse); + .otherwise(this::handleErrorResponse); } /** @@ -104,21 +110,37 @@ private CurrencyConversionRates processResponse(HttpClientResponse response) { } } - private CurrencyConversionRates updateCurrencyRates(CurrencyConversionRates currencyConversionRates) { + private Void updateCurrencyRates(CurrencyConversionRates currencyConversionRates) { final Map> receivedCurrencyRates = currencyConversionRates.getConversions(); if (receivedCurrencyRates != null) { externalCurrencyRates = receivedCurrencyRates; - lastUpdated = ZonedDateTime.now(Clock.systemUTC()); + lastUpdated = now(); } - return currencyConversionRates; + + return null; } /** * Handles errors occurred while HTTP request or response processing. */ - private static Future failResponse(Throwable exception) { + private Void handleErrorResponse(Throwable exception) { logger.warn("Error occurred while request to currency service", exception); - return Future.failedFuture(exception); + + if (externalRatesAreStale()) { + externalCurrencyRates = null; + } + + return null; + } + + private boolean externalRatesAreStale() { + final Long stalePeriodMs = externalConversionProperties.getStalePeriodMs(); + + return stalePeriodMs != null && Duration.between(lastUpdated, now()).toMillis() > stalePeriodMs; + } + + private ZonedDateTime now() { + return ZonedDateTime.now(externalConversionProperties.getClock()); } public boolean isExternalRatesActive() { @@ -130,7 +152,7 @@ public String getCurrencyServerUrl() { } public Long getRefreshPeriod() { - return externalConversionProperties != null ? externalConversionProperties.getRefreshPeriod() : null; + return externalConversionProperties != null ? externalConversionProperties.getRefreshPeriodMs() : null; } public ZonedDateTime getLastUpdated() { @@ -141,6 +163,16 @@ public Map> getExternalCurrencyRates() { return externalCurrencyRates; } + /** + * Converts price from fromCurrency to toCurrency using rates from {@link BidRequest} or external currency service. + * If bidrequest.prebid.currecy.usepbsrates is true it takes rates from prebid server, if false from request. + * Default value of usepbsrates is true. + * Throws {@link PreBidException} in case conversion is not possible. + */ + public BigDecimal convertCurrency(BigDecimal price, BidRequest bidRequest, String fromCurrency, String toCurrency) { + return convertCurrency(price, currencyRates(bidRequest), fromCurrency, toCurrency, usepbsrates(bidRequest)); + } + /** * Converts price from bidCurrency to adServerCurrency using rates and usepbsrates flag defined in request. * If usepbsrates is true it takes rates from prebid server, if false from request. Default value of usepbsrates @@ -172,12 +204,31 @@ public BigDecimal convertCurrency(BigDecimal price, Map> currencyRates(BidRequest bidRequest) { + final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestCurrency currency = prebid != null ? prebid.getCurrency() : null; + return currency != null ? currency.getRates() : null; + } + + private static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { + final ExtRequest requestExt = bidRequest.getExt(); + return requestExt != null ? requestExt.getPrebid() : null; + } + + private static Boolean usepbsrates(BidRequest bidRequest) { + final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestCurrency currency = prebid != null ? prebid.getCurrency() : null; + return currency != null ? currency.getUsepbsrates() : null; + } + /** * Returns conversion rate from the given currency rates according to priority. */ @@ -261,4 +312,15 @@ private static BigDecimal findIntermediateConversionRate(Map } return conversionRate; } + + private boolean isRatesStale() { + if (lastUpdated == null) { + return false; + } + + final ZonedDateTime stalenessBoundary = ZonedDateTime.now(externalConversionProperties.getClock()) + .minus(Duration.ofMillis(externalConversionProperties.getStaleAfterMs())); + + return lastUpdated.isBefore(stalenessBoundary); + } } diff --git a/src/main/java/org/prebid/server/deals/AdminCentralService.java b/src/main/java/org/prebid/server/deals/AdminCentralService.java new file mode 100644 index 00000000000..0ec7490ab6a --- /dev/null +++ b/src/main/java/org/prebid/server/deals/AdminCentralService.java @@ -0,0 +1,249 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.deals.events.AdminEventProcessor; +import org.prebid.server.deals.model.AdminAccounts; +import org.prebid.server.deals.model.AdminCentralResponse; +import org.prebid.server.deals.model.AdminLineItems; +import org.prebid.server.deals.model.Command; +import org.prebid.server.deals.model.LogTracer; +import org.prebid.server.deals.model.ServicesCommand; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.CriteriaManager; +import org.prebid.server.settings.CachingApplicationSettings; +import org.prebid.server.settings.SettingsCache; +import org.prebid.server.settings.proto.request.InvalidateSettingsCacheRequest; +import org.prebid.server.settings.proto.request.UpdateSettingsCacheRequest; +import org.prebid.server.util.ObjectUtils; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class AdminCentralService implements AdminEventProcessor { + + private static final Logger logger = LoggerFactory.getLogger(AdminCentralService.class); + + private static final String START = "start"; + private static final String STOP = "stop"; + private static final String INVALIDATE = "invalidate"; + private static final String SAVE = "save"; + private static final String STORED_REQUEST_CACHE = "stored request cache"; + private static final String AMP_STORED_REQUEST_CACHE = "amp stored request cache"; + + private final CriteriaManager criteriaManager; + private final LineItemService lineItemService; + private final DeliveryProgressService deliveryProgressService; + private final SettingsCache settingsCache; + private final SettingsCache ampSettingsCache; + private final CachingApplicationSettings cachingApplicationSettings; + private final JacksonMapper mapper; + private final List suspendableServices; + + public AdminCentralService(CriteriaManager criteriaManager, + LineItemService lineItemService, + DeliveryProgressService deliveryProgressService, + SettingsCache settingsCache, + SettingsCache ampSettingsCache, + CachingApplicationSettings cachingApplicationSettings, + JacksonMapper mapper, + List suspendableServices) { + this.criteriaManager = Objects.requireNonNull(criteriaManager); + this.lineItemService = Objects.requireNonNull(lineItemService); + this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); + this.settingsCache = settingsCache; + this.ampSettingsCache = ampSettingsCache; + this.cachingApplicationSettings = cachingApplicationSettings; + this.mapper = Objects.requireNonNull(mapper); + this.suspendableServices = Objects.requireNonNull(suspendableServices); + } + + @Override + public void processAdminCentralEvent(AdminCentralResponse centralAdminResponse) { + final LogTracer logTracer = centralAdminResponse.getTracer(); + if (logTracer != null) { + handleLogTracer(centralAdminResponse.getTracer()); + } + + final Command lineItemsCommand = centralAdminResponse.getLineItems(); + if (lineItemsCommand != null) { + handleLineItems(lineItemsCommand); + } + + final Command storedRequestCommand = centralAdminResponse.getStoredRequest(); + if (storedRequestCommand != null && settingsCache != null) { + handleStoredRequest(settingsCache, storedRequestCommand, STORED_REQUEST_CACHE); + } + + final Command storedRequestAmpCommand = centralAdminResponse.getStoredRequestAmp(); + if (storedRequestAmpCommand != null && ampSettingsCache != null) { + handleStoredRequest(ampSettingsCache, storedRequestAmpCommand, AMP_STORED_REQUEST_CACHE); + } + + final Command accountCommand = centralAdminResponse.getAccount(); + if (accountCommand != null && cachingApplicationSettings != null) { + handleAccountCommand(accountCommand); + } + + final ServicesCommand servicesCommand = centralAdminResponse.getServices(); + if (servicesCommand != null) { + handleServiceCommand(servicesCommand); + } + } + + private void handleAccountCommand(Command accountCommand) { + final String cmd = accountCommand.getCmd(); + if (StringUtils.isBlank(cmd)) { + logger.warn("Command for account action was not defined in register response"); + return; + } + + if (!Objects.equals(cmd, INVALIDATE)) { + logger.warn("Account commands supports only `invalidate` command, but received {0}", cmd); + return; + } + + final ObjectNode body = accountCommand.getBody(); + final AdminAccounts adminAccounts; + try { + adminAccounts = body != null + ? mapper.mapper().convertValue(body, AdminAccounts.class) + : null; + } catch (IllegalArgumentException e) { + logger.warn("Can't parse admin accounts body, failed with exception message : {0}", e.getMessage()); + return; + } + + final List accounts = ObjectUtils.getIfNotNull(adminAccounts, AdminAccounts::getAccounts); + if (CollectionUtils.isNotEmpty(accounts)) { + accounts.forEach(cachingApplicationSettings::invalidateAccountCache); + } else { + cachingApplicationSettings.invalidateAllAccountCache(); + } + } + + private void handleLineItems(Command lineItemsCommand) { + final String cmd = lineItemsCommand.getCmd(); + if (StringUtils.isBlank(cmd)) { + logger.warn("Command for line-items action was not defined in register response."); + return; + } + + if (!Objects.equals(cmd, INVALIDATE)) { + logger.warn("Line Items section supports only `invalidate` command, but received {0}", cmd); + return; + } + + final ObjectNode body = lineItemsCommand.getBody(); + final AdminLineItems adminLineItems; + try { + adminLineItems = body != null + ? mapper.mapper().convertValue(body, AdminLineItems.class) + : null; + } catch (IllegalArgumentException e) { + logger.warn("Can't parse admin line items body, failed with exception message : {0}", e.getMessage()); + return; + } + + final List lineItemIds = ObjectUtils.getIfNotNull(adminLineItems, AdminLineItems::getIds); + + if (CollectionUtils.isNotEmpty(lineItemIds)) { + lineItemService.invalidateLineItemsByIds(lineItemIds); + deliveryProgressService.invalidateLineItemsByIds(lineItemIds); + } else { + lineItemService.invalidateLineItems(); + deliveryProgressService.invalidateLineItems(); + } + } + + private void handleStoredRequest(SettingsCache settingsCache, Command storedRequestCommand, String serviceName) { + final String cmd = storedRequestCommand.getCmd(); + if (StringUtils.isBlank(cmd)) { + logger.warn("Command for {0} was not defined.", serviceName); + return; + } + + final ObjectNode body = storedRequestCommand.getBody(); + if (body == null) { + logger.warn("Command body for {0} was not defined.", serviceName); + return; + } + + switch (cmd) { + case INVALIDATE: + invalidateStoredRequests(settingsCache, serviceName, body); + break; + case SAVE: + saveStoredRequests(settingsCache, serviceName, body); + break; + default: + logger.warn("Command for {0} should has value 'save' or 'invalidate' but was {1}.", + serviceName, cmd); + } + } + + private void saveStoredRequests(SettingsCache settingsCache, String serviceName, ObjectNode body) { + final UpdateSettingsCacheRequest saveRequest; + try { + saveRequest = mapper.mapper().convertValue(body, UpdateSettingsCacheRequest.class); + } catch (IllegalArgumentException e) { + logger.warn("Can't parse save settings cache request object for {0}," + + " failed with exception message : {1}", serviceName, e.getMessage()); + return; + } + final Map storedRequests = MapUtils.emptyIfNull(saveRequest.getRequests()); + final Map storedImps = MapUtils.emptyIfNull(saveRequest.getImps()); + settingsCache.save(storedRequests, storedImps); + logger.info("Stored request with ids {0} and stored impressions with ids {1} were successfully saved", + String.join(", ", storedRequests.keySet()), String.join(", ", storedImps.keySet())); + } + + private void invalidateStoredRequests(SettingsCache settingsCache, String serviceName, ObjectNode body) { + final InvalidateSettingsCacheRequest invalidateRequest; + try { + invalidateRequest = mapper.mapper().convertValue(body, InvalidateSettingsCacheRequest.class); + } catch (IllegalArgumentException e) { + logger.warn("Can't parse invalidate settings cache request object for {0}," + + " failed with exception message : {1}", serviceName, e.getMessage()); + return; + } + final List requestIds = ListUtils.emptyIfNull(invalidateRequest.getRequests()); + final List impIds = ListUtils.emptyIfNull(invalidateRequest.getImps()); + settingsCache.invalidate(requestIds, impIds); + logger.info("Stored requests with ids {0} and impression with ids {1} were successfully invalidated", + String.join(", ", requestIds), String.join(", ", impIds)); + } + + private void handleLogTracer(LogTracer logTracer) { + final String command = logTracer.getCmd(); + if (StringUtils.isBlank(command)) { + logger.warn("Command for traceLogger was not defined"); + return; + } + + switch (command) { + case START: + criteriaManager.addCriteria(logTracer.getFilters(), logTracer.getDurationInSeconds()); + break; + case STOP: + criteriaManager.stop(); + break; + default: + logger.warn("Command for trace logger should has value 'start' or 'stop' but was {0}.", command); + } + } + + private void handleServiceCommand(ServicesCommand servicesCommand) { + final String command = servicesCommand.getCmd(); + if (command != null && command.equalsIgnoreCase(STOP)) { + suspendableServices.forEach(Suspendable::suspend); + } + logger.info("PBS services were successfully suspended"); + } +} diff --git a/src/main/java/org/prebid/server/deals/AlertHttpService.java b/src/main/java/org/prebid/server/deals/AlertHttpService.java new file mode 100644 index 00000000000..e19a040dcc8 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/AlertHttpService.java @@ -0,0 +1,144 @@ +package org.prebid.server.deals; + +import io.netty.handler.codec.http.HttpHeaderValues; +import io.vertx.core.AsyncResult; +import io.vertx.core.MultiMap; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.deals.model.AlertEvent; +import org.prebid.server.deals.model.AlertPriority; +import org.prebid.server.deals.model.AlertProxyProperties; +import org.prebid.server.deals.model.AlertSource; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.json.EncodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class AlertHttpService { + + private static final Logger logger = LoggerFactory.getLogger(AlertHttpService.class); + private static final String RAISE = "RAISE"; + private static final Long DEFAULT_HIGH_ALERT_PERIOD = 15L; + + private final JacksonMapper mapper; + private final HttpClient httpClient; + private final Clock clock; + private final AlertProxyProperties alertProxyProperties; + private final AlertSource alertSource; + private final boolean enabled; + private final String url; + private final long timeoutMillis; + private final String authHeaderValue; + private final Map alertTypes; + private final Map alertTypesCounters; + + public AlertHttpService(JacksonMapper mapper, HttpClient httpClient, Clock clock, + DeploymentProperties deploymentProperties, + AlertProxyProperties alertProxyProperties) { + this.mapper = Objects.requireNonNull(mapper); + this.httpClient = Objects.requireNonNull(httpClient); + this.clock = Objects.requireNonNull(clock); + this.alertProxyProperties = Objects.requireNonNull(alertProxyProperties); + this.alertSource = makeSource(Objects.requireNonNull(deploymentProperties)); + this.enabled = alertProxyProperties.isEnabled(); + this.timeoutMillis = TimeUnit.SECONDS.toMillis(alertProxyProperties.getTimeoutSec()); + this.url = HttpUtil.validateUrl(Objects.requireNonNull(alertProxyProperties.getUrl())); + this.authHeaderValue = HttpUtil.makeBasicAuthHeaderValue(alertProxyProperties.getUsername(), + alertProxyProperties.getPassword()); + this.alertTypes = new ConcurrentHashMap<>(alertProxyProperties.getAlertTypes()); + this.alertTypesCounters = new ConcurrentHashMap<>(alertTypes.keySet().stream() + .collect(Collectors.toMap(Function.identity(), s -> 0L))); + } + + private static AlertSource makeSource(DeploymentProperties deploymentProperties) { + return AlertSource.builder() + .env(deploymentProperties.getProfile()) + .region(deploymentProperties.getPbsRegion()) + .dataCenter(deploymentProperties.getDataCenter()) + .subSystem(deploymentProperties.getSubSystem()) + .system(deploymentProperties.getSystem()) + .hostId(deploymentProperties.getPbsHostId()) + .build(); + } + + public void alertWithPeriod(String serviceName, String alertType, AlertPriority alertPriority, String message) { + if (alertTypes.get(alertType) == null) { + alertTypes.put(alertType, DEFAULT_HIGH_ALERT_PERIOD); + alertTypesCounters.put(alertType, 0L); + } + + long count = alertTypesCounters.get(alertType); + final long period = alertTypes.get(alertType); + + alertTypesCounters.put(alertType, ++count); + final String formattedMessage = + String.format("Service %s failed to send request %s time(s) with error message : %s", + serviceName, count, message); + if (count == 1) { + alert(alertType, alertPriority, formattedMessage); + } else if (count % period == 0) { + alert(alertType, AlertPriority.HIGH, formattedMessage); + } + } + + public void resetAlertCount(String alertType) { + alertTypesCounters.put(alertType, 0L); + } + + public void alert(String name, AlertPriority alertPriority, String message) { + if (!enabled) { + logger.warn("Alert to proxy is not enabled in pbs configuration"); + return; + } + + final AlertEvent alertEvent = makeEvent(RAISE, alertPriority, name, message, alertSource); + + try { + httpClient.post(alertProxyProperties.getUrl(), headers(), + mapper.encode(Collections.singletonList(alertEvent)), timeoutMillis) + .setHandler(this::handleResponse); + } catch (EncodeException e) { + logger.warn("Can't parse alert proxy payload: {0}", e.getMessage()); + } + } + + private AlertEvent makeEvent(String action, AlertPriority priority, String name, String details, + AlertSource alertSource) { + return AlertEvent.builder() + .id(UUID.randomUUID().toString()) + .action(action.toUpperCase()) + .priority(priority) + .name(name) + .details(details) + .updatedAt(ZonedDateTime.now(clock)) + .source(alertSource) + .build(); + } + + private MultiMap headers() { + return MultiMap.caseInsensitiveMultiMap() + .add(HttpUtil.PG_TRX_ID, UUID.randomUUID().toString()) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .add(HttpUtil.AUTHORIZATION_HEADER, authHeaderValue); + } + + private void handleResponse(AsyncResult httpClientResponseResult) { + if (httpClientResponseResult.failed()) { + logger.error("Error occurred during sending alert to proxy at {0}::{1} ", url, + httpClientResponseResult.cause().getMessage()); + } + } +} diff --git a/src/main/java/org/prebid/server/deals/DealsProcessor.java b/src/main/java/org/prebid/server/deals/DealsProcessor.java new file mode 100644 index 00000000000..9178c6d532f --- /dev/null +++ b/src/main/java/org/prebid/server/deals/DealsProcessor.java @@ -0,0 +1,557 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Pmp; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.User; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.Tuple3; +import org.prebid.server.deals.deviceinfo.DeviceInfoService; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.model.DeviceInfo; +import org.prebid.server.deals.model.MatchLineItemsResult; +import org.prebid.server.deals.model.UserData; +import org.prebid.server.deals.model.UserDetails; +import org.prebid.server.deals.proto.LineItemSize; +import org.prebid.server.execution.Timeout; +import org.prebid.server.geolocation.GeoLocationService; +import org.prebid.server.geolocation.model.GeoInfo; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.CriteriaLogManager; +import org.prebid.server.proto.openrtb.ext.request.ExtDeal; +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtDeviceVendor; +import org.prebid.server.proto.openrtb.ext.request.ExtGeo; +import org.prebid.server.proto.openrtb.ext.request.ExtGeoVendor; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.ExtUserTime; +import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal.Category; +import org.prebid.server.util.StreamUtil; + +import java.time.Clock; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Role as a dispatcher between request factory and all other PG components. + */ +public class DealsProcessor { + + private static final Logger logger = LoggerFactory.getLogger(DealsProcessor.class); + + private static final String PREBID_EXT = "prebid"; + private static final String BIDDER_EXT = "bidder"; + private static final String DEALS_ONLY = "dealsonly"; + + private final LineItemService lineItemService; + private final DeviceInfoService deviceInfoService; + private final GeoLocationService geoLocationService; + private final UserService userService; + private final Clock clock; + private final JacksonMapper mapper; + private final CriteriaLogManager criteriaLogManager; + + public DealsProcessor(LineItemService lineItemService, + DeviceInfoService deviceInfoService, + GeoLocationService geoLocationService, + UserService userService, + Clock clock, + JacksonMapper mapper, + CriteriaLogManager criteriaLogManager) { + + this.lineItemService = Objects.requireNonNull(lineItemService); + this.deviceInfoService = deviceInfoService; + this.geoLocationService = geoLocationService; + this.userService = Objects.requireNonNull(userService); + this.clock = Objects.requireNonNull(clock); + this.mapper = Objects.requireNonNull(mapper); + this.criteriaLogManager = Objects.requireNonNull(criteriaLogManager); + } + + /** + * Returns new {@link AuctionContext} with populated deals information like device info, + * geo-location data, user frequency capping info and deal IDs for corresponding impressions. + *

+ * If account doesn't need additional deals processing, will return the same given {@link AuctionContext}. + */ + public Future populateDealsInfo(AuctionContext context) { + final boolean accountHasDeals = lineItemService.accountHasDeals(context); + final String accountId = context.getAccount().getId(); + if (!accountHasDeals) { + criteriaLogManager.log(logger, accountId, String.format("Account %s does not have deals", accountId), + logger::debug); + return Future.succeededFuture(context); + } + + final Device device = context.getBidRequest().getDevice(); + final Timeout timeout = context.getTimeout(); + final GeoInfo geoInfo = context.getGeoInfo(); + + final CompositeFuture compositeFuture = CompositeFuture.join( + lookupDeviceInfo(device), + geoInfo != null ? Future.succeededFuture(geoInfo) : lookupGeoInfo(device, timeout), + userService.getUserDetails(context, timeout)); + + // AsyncResult has atomic nature: its result() method returns null when at least one future fails. + // So, in handler it is ignored and original CompositeFuture used to process obtained results + // to avoid explicit casting to CompositeFuture implementation. + final Promise> promise = Promise.promise(); + compositeFuture.setHandler(ignored -> handleDealsInfo(compositeFuture, promise, context.getAccount().getId())); + return promise.future() + .map((Tuple3 tuple) -> + enrichAuctionContext(context, tuple.getLeft(), tuple.getMiddle(), tuple.getRight())) + .map(this::matchAndPopulateDeals); + } + + /** + * Finds deals only bidders without matched deals and removes them from {@link Imp}. + * If {@link Imp} has only deals only bidders without deals, null will be returned. + */ + public static Imp removeDealsOnlyBiddersWithoutDeals(Imp imp, AuctionContext auctionContext) { + final Set dealsOnlyBiddersToRemove = getDealsOnlyBiddersToRemove(imp); + Imp resolvedImp = imp; + if (CollectionUtils.isNotEmpty(dealsOnlyBiddersToRemove)) { + final ObjectNode extImp = removeBidders(imp, dealsOnlyBiddersToRemove); + resolvedImp = imp.toBuilder().ext(extImp).build(); + if (hasBidder(resolvedImp)) { + logBidderOrImpExclusion(auctionContext, imp, dealsOnlyBiddersToRemove, "No Line Items from " + + "bidders %s matching imp with id %s and ready to serve. Removing impression from request" + + " to these bidders because dealsonly flag is on for them."); + } else { + logBidderOrImpExclusion(auctionContext, imp, dealsOnlyBiddersToRemove, "No Line Items from " + + "bidders %s matching imp with id %s and ready to serve. Removing imp from requests to all" + + " bidders because dealsonly flag is on for these bidders and no other valid bidders in imp."); + resolvedImp = null; + } + } + + return resolvedImp; + } + + private Future lookupDeviceInfo(Device device) { + return deviceInfoService != null + ? deviceInfoService.getDeviceInfo(device.getUa()) + : Future.failedFuture("Device info is disabled by configuration"); + } + + private Future lookupGeoInfo(Device device, Timeout timeout) { + return geoLocationService != null + ? geoLocationService.lookup(ObjectUtils.defaultIfNull(device.getIp(), device.getIpv6()), timeout) + : Future.failedFuture("Geo location is disabled by configuration"); + } + + /** + * Handles obtained {@link DeviceInfo}, {@link GeoInfo}, {@link UserDetails} results from {@link CompositeFuture}. + */ + private void handleDealsInfo(CompositeFuture compositeFuture, + Promise> resultPromise, String account) { + DeviceInfo deviceInfo = null; + GeoInfo geoInfo = null; + UserDetails userDetails = null; + + for (int i = 0; i < compositeFuture.list().size(); i++) { + final Object o = compositeFuture.resultAt(i); + if (o == null) { + criteriaLogManager.log(logger, account, String.format("Deals processing error: %s", + compositeFuture.cause(i)), logger::warn); + continue; + } + + if (o instanceof DeviceInfo) { + deviceInfo = (DeviceInfo) o; + } else if (o instanceof GeoInfo) { + geoInfo = (GeoInfo) o; + } else if (o instanceof UserDetails) { + userDetails = (UserDetails) o; + } + } + + resultPromise.complete(Tuple3.of(deviceInfo, geoInfo, userDetails)); + } + + /** + * Stores information from {@link DeviceInfoService}, {@link GeoLocationService} and {@link UserService} + * to {@link BidRequest} to make it available during targeting evaluation. + */ + private AuctionContext enrichAuctionContext( + AuctionContext auctionContext, DeviceInfo deviceInfo, GeoInfo geoInfo, UserDetails userDetails) { + + final BidRequest bidRequest = auctionContext.getBidRequest(); + + final BidRequest.BidRequestBuilder requestBuilder = bidRequest.toBuilder(); + + final User updatedUser = updateUser(bidRequest.getUser(), userDetails, geoInfo); + requestBuilder.user(updatedUser); + + if (deviceInfo != null || geoInfo != null) { + final Device updatedDevice = updateDevice(bidRequest.getDevice(), deviceInfo, geoInfo); + requestBuilder.device(updatedDevice); + } + + return auctionContext.toBuilder() + .bidRequest(requestBuilder.build()) + .geoInfo(geoInfo) + .build(); + } + + /** + * Returns {@link Device} populated with {@link DeviceInfo} and {@link GeoInfo} data. + */ + private Device updateDevice(Device device, DeviceInfo deviceInfo, GeoInfo geoInfo) { + final Device.DeviceBuilder deviceBuilder = device != null ? device.toBuilder() : Device.builder(); + + ExtDevice updatedExtDevice = getIfNotNull(device, Device::getExt); + if (deviceInfo != null) { + final ExtDeviceVendor extDeviceVendor = ExtDeviceVendor.builder() + .type(deviceInfo.getDeviceTypeRaw()) + .osfamily(null) + .os(deviceInfo.getOs()) + .osver(deviceInfo.getOsVersion()) + .browser(deviceInfo.getBrowser()) + .browserver(deviceInfo.getBrowserVersion()) + .make(deviceInfo.getManufacturer()) + .model(deviceInfo.getModel()) + .language(deviceInfo.getLanguage()) + .carrier(deviceInfo.getCarrier()) + .build(); + + if (!extDeviceVendor.equals(ExtDeviceVendor.EMPTY)) { + if (updatedExtDevice == null) { + updatedExtDevice = ExtDevice.empty(); + deviceBuilder.ext(updatedExtDevice); + } + updatedExtDevice.addProperty(deviceInfo.getVendor(), mapper.mapper().valueToTree(extDeviceVendor)); + } + } + + if (geoInfo != null) { + final Geo updatedDeviceGeo = updateDeviceGeo(device, geoInfo); + deviceBuilder.geo(updatedDeviceGeo); + + if (geoInfo.getConnectionSpeed() != null) { + final ExtDeviceVendor extDeviceVendor = ExtDeviceVendor.builder() + .connspeed(geoInfo.getConnectionSpeed()) + .build(); + if (updatedExtDevice == null) { + updatedExtDevice = ExtDevice.empty(); + deviceBuilder.ext(updatedExtDevice); + } + updatedExtDevice.addProperty(geoInfo.getVendor(), mapper.mapper().valueToTree(extDeviceVendor)); + } + } + + return deviceBuilder.build(); + } + + /** + * Returns {@link Geo} populated with {@link GeoInfo} data. + */ + private Geo updateDeviceGeo(Device device, GeoInfo geoInfo) { + final String geoInfoVendor = geoInfo.getVendor(); + + final Geo geo = getIfNotNull(device, Device::getGeo); + final ExtGeo extGeo = getIfNotNull(geo, Geo::getExt); + final JsonNode extGeoVendorNode = getIfNotNull(extGeo, node -> node.getProperty(geoInfoVendor)); + final ExtGeoVendor extGeoVendor = parseExt(extGeoVendorNode, ExtGeoVendor.class); + + final ExtGeo updatedExtGeoNode = extGeo != null ? extGeo : ExtGeo.of(); + if (StringUtils.isNotBlank(geoInfo.getContinent()) + || StringUtils.isNotBlank(geoInfo.getCountry()) + || geoInfo.getRegionCode() != null + || geoInfo.getMetroNielsen() != null + || StringUtils.isNotBlank(geoInfo.getCity()) + || StringUtils.isNotBlank(geoInfo.getZip())) { + + final ExtGeoVendor.ExtGeoVendorBuilder extGeoVendorBuilder = extGeoVendor != null + ? extGeoVendor.toBuilder() + : ExtGeoVendor.builder(); + + final ExtGeoVendor updatedExtGeoVendor = extGeoVendorBuilder + .continent(geoInfo.getContinent()) + .country(geoInfo.getCountry()) + .region(geoInfo.getRegionCode()) + .metro(geoInfo.getMetroNielsen()) + .city(geoInfo.getCity()) + .zip(geoInfo.getZip()) + .build(); + updatedExtGeoNode.addProperty(geoInfoVendor, mapper.mapper().valueToTree(updatedExtGeoVendor)); + } + + final Geo.GeoBuilder geoBuilder = geo != null ? geo.toBuilder() : Geo.builder(); + return geoBuilder + .region(geoInfo.getRegion()) + .metro(geoInfo.getMetroGoogle()) + .lat(geoInfo.getLat()) + .lon(geoInfo.getLon()) + .ext(updatedExtGeoNode) + .build(); + } + + /** + * Returns {@link User} populated with {@link UserDetails} data. + */ + private User updateUser(User user, UserDetails userDetails, GeoInfo geoInfo) { + final ExtUser extUser = getIfNotNull(user, User::getExt); + final ExtUser.ExtUserBuilder extUserBuilder = extUser != null ? extUser.toBuilder() : ExtUser.builder(); + + updateUserExtWithUserDetails(extUserBuilder, userDetails); + updateUserExtWithGeoInfo(extUserBuilder, geoInfo); + + final User.UserBuilder userBuilder = user != null ? user.toBuilder() : User.builder(); + return userBuilder + .data(userDetails != null ? makeUserData(userDetails) : null) + .ext(extUserBuilder.build()) + .build(); + } + + private void updateUserExtWithUserDetails(ExtUser.ExtUserBuilder extUserBuilder, UserDetails userDetails) { + if (userDetails != null) { + // Indicate that the call to User Data Store has been made successfully even if the user is not frequency + // capped + extUserBuilder.fcapIds(ListUtils.emptyIfNull(userDetails.getFcapIds())); + } // otherwise leave cappedIds null to indicate that call to User Data Store failed + } + + private void updateUserExtWithGeoInfo(ExtUser.ExtUserBuilder extUserBuilder, GeoInfo geoInfo) { + final ZoneId timeZone = ObjectUtils.firstNonNull(getIfNotNull(geoInfo, GeoInfo::getTimeZone), clock.getZone()); + + final ZonedDateTime dateTime = ZonedDateTime.now(clock).withZoneSameInstant(timeZone); + + extUserBuilder.time(ExtUserTime.of( + dateTime.getDayOfWeek().get(WeekFields.SUNDAY_START.dayOfWeek()), + dateTime.getHour())); + } + + /** + * Makes {@link List} from {@link UserDetails}. + */ + private List makeUserData(UserDetails userDetails) { + final List userData = userDetails.getUserData(); + return userData != null + ? userData.stream() + .map(userDataElement -> Data.builder() + .id(userDataElement.getName()) + .segment(makeSegments(userDataElement.getSegment())).build()) + .collect(Collectors.toList()) + : null; + } + + /** + * Makes {@link List} from {@link List}. + */ + private List makeSegments(List segments) { + return segments != null + ? segments.stream() + .map(segment -> Segment.builder().id(segment.getId()).build()) + .collect(Collectors.toList()) + : null; + } + + /** + * Transforms {@link ObjectNode} to the object of the given {@link Class} type or returns null if error occurred. + */ + private T parseExt(JsonNode node, Class clazz) { + if (node == null) { + return null; + } + try { + return mapper.mapper().treeToValue(node, clazz); + } catch (JsonProcessingException e) { + return null; + } + } + + /** + * Fetches {@link MatchLineItemsResult} for each {@link Imp} and enriches it with {@link Deal}s. + */ + private AuctionContext matchAndPopulateDeals(AuctionContext auctionContext) { + final BidRequest bidRequest = auctionContext.getBidRequest(); + final List updatedImps = new ArrayList<>(); + boolean isImpsUpdated = false; + + for (Imp imp : bidRequest.getImp()) { + final MatchLineItemsResult matchResult = lineItemService.findMatchingLineItems(auctionContext, imp); + final List lineItems = matchResult.getLineItems(); + + lineItems.forEach(lineItem -> criteriaLogManager.log(logger, lineItem.getAccountId(), lineItem.getSource(), + lineItem.getLineItemId(), + String.format("LineItem %s is ready to be served", lineItem.getLineItemId()), logger::debug)); + + final Imp updatedImp = lineItems.isEmpty() ? imp : enrichImpWithDeals(imp, lineItems); + isImpsUpdated |= imp != updatedImp; + + updatedImps.add(updatedImp); + } + + final AuctionContext result; + if (!isImpsUpdated) { + result = auctionContext; + } else { + final BidRequest updatedBidRequest = bidRequest.toBuilder().imp(updatedImps).build(); + result = auctionContext.toBuilder().bidRequest(updatedBidRequest).build(); + } + return result; + } + + /** + * Populates request.imp[].pmp object: + *

+ * - injects dealIds from selected {@link LineItem}s to corresponding request.imp[].pmp.deals[].id. + *

+ * - stores {@link LineItem} information in request.imp[].pmp.deals[].ext.line object. + */ + private Imp enrichImpWithDeals(Imp imp, List lineItems) { + final List deals = lineItems.stream() + .map(lineItem -> toDeal(imp, lineItem)) + .collect(Collectors.toList()); + + return impWithPopulatedDeals(imp, deals); + } + + /** + * Creates {@link Deal} from the given {@link LineItem}. + */ + private Deal toDeal(Imp imp, LineItem lineItem) { + return Deal.builder() + .id(lineItem.getDealId()) + .ext(mapper.mapper().valueToTree(ExtDeal.of(toExtDealLine(imp, lineItem)))) + .build(); + } + + private static ExtDealLine toExtDealLine(Imp imp, LineItem lineItem) { + final List formats = getIfNotNull(imp.getBanner(), Banner::getFormat); + final List lineItemSizes = lineItem.getSizes(); + + final List lineSizes; + if (CollectionUtils.isNotEmpty(formats) && CollectionUtils.isNotEmpty(lineItemSizes)) { + final List matchedSizes = lineItemSizes.stream() + .filter(size -> formatsContainLineItemSize(formats, size)) + .map(size -> Format.builder().w(size.getW()).h(size.getH()).build()) + .collect(Collectors.toList()); + lineSizes = CollectionUtils.isNotEmpty(matchedSizes) ? matchedSizes : null; + } else { + lineSizes = null; + } + + return ExtDealLine.of(lineItem.getLineItemId(), lineItem.getExtLineItemId(), lineSizes, lineItem.getSource()); + } + + /** + * Returns true if the given {@link LineItemSize} is found in a list of imp.banner {@link Format}s. + */ + private static boolean formatsContainLineItemSize(List formats, LineItemSize lineItemSize) { + return formats.stream() + .anyMatch(format -> Objects.equals(format.getW(), lineItemSize.getW()) + && Objects.equals(format.getH(), lineItemSize.getH())); + } + + /** + * Returns {@link Imp} with populated {@link Deal}s. + */ + private static Imp impWithPopulatedDeals(Imp imp, List deals) { + final Pmp pmp = imp.getPmp(); + final List existingDeals = ListUtils.emptyIfNull(pmp != null ? pmp.getDeals() : null); + + final List combinedDeals = Stream.concat(existingDeals.stream(), deals.stream()) + .collect(Collectors.toList()); + + final Pmp.PmpBuilder pmpBuilder = pmp != null ? pmp.toBuilder() : Pmp.builder(); + final Pmp updatedPmp = pmpBuilder.deals(combinedDeals).build(); + return imp.toBuilder().pmp(updatedPmp).build(); + } + + /** + * Returns {@link Set} of bidder names, which should be removed from {@link Imp}. + */ + private static Set getDealsOnlyBiddersToRemove(Imp imp) { + final Pmp pmp = imp.getPmp(); + final List deals = pmp == null ? null : pmp.getDeals(); + return CollectionUtils.isEmpty(deals) + ? findDealsOnlyBidders(imp) + : Collections.emptySet(); + } + + /** + * Returns {@link Set} of valid bidder names, which has dealsOnly parameter with true value. + */ + private static Set findDealsOnlyBidders(Imp imp) { + return StreamUtil.asStream(bidderNodesFromImp(imp)) + .filter(bidderNode -> isDealsOnlyBidder(bidderNode.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + private static Iterator> bidderNodesFromImp(Imp imp) { + final JsonNode extPrebidBidder = imp.getExt().get(PREBID_EXT).get(BIDDER_EXT); + + return extPrebidBidder != null ? extPrebidBidder.fields() : Collections.emptyIterator(); + } + + /** + * Checks if {@link Imp} has at least one valid bidder. + */ + private static boolean hasBidder(Imp imp) { + return bidderNodesFromImp(imp).hasNext(); + } + + /** + * Checks if bidder is deals only. + */ + private static boolean isDealsOnlyBidder(JsonNode bidder) { + final JsonNode dealsOnlyNode = bidder.get(DEALS_ONLY); + return dealsOnlyNode != null && dealsOnlyNode.isBoolean() && dealsOnlyNode.asBoolean(); + } + + /** + * Removes bidders from {@link Imp}. + */ + private static ObjectNode removeBidders(Imp imp, Set bidders) { + final ObjectNode modifiedExt = imp.getExt().deepCopy(); + final ObjectNode extPrebidBidder = (ObjectNode) modifiedExt.get(PREBID_EXT).get(BIDDER_EXT); + + bidders.forEach(extPrebidBidder::remove); + + return modifiedExt; + } + + private static void logBidderOrImpExclusion(AuctionContext auctionContext, Imp imp, + Set dealsOnlyBiddersToRemove, String messageTemplate) { + auctionContext.getDeepDebugLog().add(null, Category.cleanup, () -> + String.format(messageTemplate, String.join(", ", dealsOnlyBiddersToRemove), imp.getId())); + } + + private static R getIfNotNull(T target, Function getter) { + return target != null ? getter.apply(target) : null; + } +} diff --git a/src/main/java/org/prebid/server/deals/DeliveryProgressReportFactory.java b/src/main/java/org/prebid/server/deals/DeliveryProgressReportFactory.java new file mode 100644 index 00000000000..c4d9d1bf19d --- /dev/null +++ b/src/main/java/org/prebid/server/deals/DeliveryProgressReportFactory.java @@ -0,0 +1,313 @@ +package org.prebid.server.deals; + +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.deals.lineitem.DeliveryPlan; +import org.prebid.server.deals.lineitem.DeliveryProgress; +import org.prebid.server.deals.lineitem.DeliveryToken; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; +import org.prebid.server.deals.proto.report.DeliveryProgressReportBatch; +import org.prebid.server.deals.proto.report.DeliverySchedule; +import org.prebid.server.deals.proto.report.LineItemStatus; +import org.prebid.server.deals.proto.report.LostToLineItem; +import org.prebid.server.deals.proto.report.Token; +import org.prebid.server.util.ObjectUtils; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class DeliveryProgressReportFactory { + + private static final Logger logger = LoggerFactory.getLogger(DeliveryProgressReportFactory.class); + + private static final LostToLineItemComparator LOST_TO_LINE_ITEM_COMPARATOR = new LostToLineItemComparator(); + + private final DeploymentProperties deploymentProperties; + private final int competitorsNumber; + private final LineItemService lineItemService; + private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .toFormatter(); + + public DeliveryProgressReportFactory( + DeploymentProperties deploymentProperties, int competitorsNumber, LineItemService lineItemService) { + this.deploymentProperties = Objects.requireNonNull(deploymentProperties); + this.competitorsNumber = competitorsNumber; + this.lineItemService = Objects.requireNonNull(lineItemService); + } + + public DeliveryProgressReport fromDeliveryProgress( + DeliveryProgress deliveryProgress, + ZonedDateTime now, + boolean isOverall) { + final List lineItemStatuses = + new ArrayList<>(deliveryProgress.getLineItemStatuses().values()); + return DeliveryProgressReport.builder() + .reportId(UUID.randomUUID().toString()) + .reportTimeStamp(now != null ? formatTimeStamp(now) : null) + .dataWindowStartTimeStamp(isOverall ? null : formatTimeStamp(deliveryProgress.getStartTimeStamp())) + .dataWindowEndTimeStamp(isOverall ? null : formatTimeStamp(deliveryProgress.getEndTimeStamp())) + .instanceId(deploymentProperties.getPbsHostId()) + .region(deploymentProperties.getPbsRegion()) + .vendor(deploymentProperties.getPbsVendor()) + .clientAuctions(deliveryProgress.getRequests().sum()) + .lineItemStatus(makeLineItemStatusReports(deliveryProgress, lineItemStatuses, + deliveryProgress.getLineItemStatuses(), isOverall)) + .build(); + } + + public DeliveryProgressReportBatch batchFromDeliveryProgress( + DeliveryProgress deliveryProgress, + Map overallLineItemStatuses, + ZonedDateTime now, + int batchSize, + boolean isOverall) { + final List lineItemStatuses + = new ArrayList<>(deliveryProgress.getLineItemStatuses().values()); + final String reportId = UUID.randomUUID().toString(); + final String reportTimeStamp = now != null ? formatTimeStamp(now) : null; + final String dataWindowStartTimeStamp = isOverall + ? null + : formatTimeStamp(deliveryProgress.getStartTimeStamp()); + final String dataWindowEndTimeStamp = isOverall ? null : formatTimeStamp(deliveryProgress.getEndTimeStamp()); + final long clientAuctions = deliveryProgress.getRequests().sum(); + + final int lineItemsCount = lineItemStatuses.size(); + final int batchesNumber = lineItemsCount / batchSize + (lineItemsCount % batchSize > 0 ? 1 : 0); + final Set reportsBatch = IntStream.range(0, batchesNumber) + .mapToObj(batchNumber -> updateReportWithLineItems(deliveryProgress, lineItemStatuses, + overallLineItemStatuses, lineItemsCount, batchNumber, batchSize, isOverall)) + .map(deliveryProgressReport -> deliveryProgressReport + .reportId(reportId) + .reportTimeStamp(reportTimeStamp) + .dataWindowStartTimeStamp(dataWindowStartTimeStamp) + .dataWindowEndTimeStamp(dataWindowEndTimeStamp) + .clientAuctions(clientAuctions) + .instanceId(deploymentProperties.getPbsHostId()) + .region(deploymentProperties.getPbsRegion()) + .vendor(deploymentProperties.getPbsVendor()) + .build()) + .collect(Collectors.toSet()); + + logNotDeliveredLineItems(deliveryProgress, reportsBatch); + return DeliveryProgressReportBatch.of(reportsBatch, reportId, dataWindowEndTimeStamp); + } + + private DeliveryProgressReport.DeliveryProgressReportBuilder updateReportWithLineItems( + DeliveryProgress deliveryProgress, + List lineItemStatuses, + Map overallLineItemStatuses, + int lineItemsCount, + int batchNumber, + int batchSize, + boolean isOverall) { + final int startBatchIndex = batchNumber * batchSize; + final int endBatchIndex = (batchNumber + 1) * batchSize; + final List batchList = + lineItemStatuses.subList(startBatchIndex, Math.min(endBatchIndex, lineItemsCount)); + return DeliveryProgressReport.builder() + .lineItemStatus(makeLineItemStatusReports(deliveryProgress, batchList, + overallLineItemStatuses, isOverall)); + } + + private Set makeLineItemStatusReports( + DeliveryProgress deliveryProgress, + List lineItemStatuses, + Map overallLineItemStatuses, + boolean isOverall) { + + return lineItemStatuses.stream() + .map(lineItemStatus -> toLineItemStatusReport(lineItemStatus, + overallLineItemStatuses != null + ? overallLineItemStatuses.get(lineItemStatus.getLineItemId()) + : null, + deliveryProgress, isOverall)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + private static void logNotDeliveredLineItems(DeliveryProgress deliveryProgress, + Set reportsBatch) { + final Set reportedLineItems = reportsBatch.stream() + .map(DeliveryProgressReport::getLineItemStatus) + .flatMap(Collection::stream) + .map(LineItemStatus::getLineItemId) + .collect(Collectors.toSet()); + + final String notDeliveredLineItems = deliveryProgress.getLineItemStatuses().keySet().stream() + .filter(id -> !reportedLineItems.contains(id)) + .collect(Collectors.joining(", ")); + if (StringUtils.isNotBlank(notDeliveredLineItems)) { + logger.info("Line item with id {0} will not be reported," + + " as it does not have active delivery schedules during report window.", notDeliveredLineItems); + } + } + + DeliveryProgressReport updateReportTimeStamp(DeliveryProgressReport deliveryProgressReport, ZonedDateTime now) { + return deliveryProgressReport.toBuilder().reportTimeStamp(formatTimeStamp(now)).build(); + } + + private LineItemStatus toLineItemStatusReport(org.prebid.server.deals.lineitem.LineItemStatus lineItemStatus, + org.prebid.server.deals.lineitem.LineItemStatus overallLineItemStatus, + DeliveryProgress deliveryProgress, boolean isOverall) { + final String lineItemId = lineItemStatus.getLineItemId(); + final LineItem lineItem = lineItemService.getLineItemById(lineItemId); + if (isOverall && lineItem == null) { + return null; + } + final DeliveryPlan activeDeliveryPlan = ObjectUtils.getIfNotNull(lineItem, LineItem::getActiveDeliveryPlan); + final Set deliverySchedules = deliverySchedule(lineItemStatus, overallLineItemStatus, + activeDeliveryPlan); + if (CollectionUtils.isEmpty(deliverySchedules) && !isOverall) { + return null; + } + + return LineItemStatus.builder() + .lineItemSource(ObjectUtils.firstNonNull(lineItemStatus::getSource, + () -> ObjectUtils.getIfNotNull(lineItem, LineItem::getSource))) + .lineItemId(lineItemId) + .dealId(ObjectUtils.firstNonNull(lineItemStatus::getDealId, + () -> ObjectUtils.getIfNotNull(lineItem, LineItem::getDealId))) + .extLineItemId(ObjectUtils.firstNonNull(lineItemStatus::getExtLineItemId, + () -> ObjectUtils.getIfNotNull(lineItem, LineItem::getExtLineItemId))) + .accountAuctions(accountRequests(ObjectUtils.firstNonNull(lineItemStatus::getAccountId, + () -> ObjectUtils.getIfNotNull(lineItem, LineItem::getAccountId)), deliveryProgress)) + .domainMatched(lineItemStatus.getDomainMatched().sum()) + .targetMatched(lineItemStatus.getTargetMatched().sum()) + .targetMatchedButFcapped(lineItemStatus.getTargetMatchedButFcapped().sum()) + .targetMatchedButFcapLookupFailed(lineItemStatus.getTargetMatchedButFcapLookupFailed().sum()) + .pacingDeferred(lineItemStatus.getPacingDeferred().sum()) + .sentToBidder(lineItemStatus.getSentToBidder().sum()) + .sentToBidderAsTopMatch(lineItemStatus.getSentToBidderAsTopMatch().sum()) + .receivedFromBidder(lineItemStatus.getReceivedFromBidder().sum()) + .receivedFromBidderInvalidated(lineItemStatus.getReceivedFromBidderInvalidated().sum()) + .sentToClient(lineItemStatus.getSentToClient().sum()) + .sentToClientAsTopMatch(lineItemStatus.getSentToClientAsTopMatch().sum()) + .lostToLineItems(lostToLineItems(lineItemStatus, deliveryProgress)) + .events(lineItemStatus.getEvents()) + .deliverySchedule(deliverySchedules) + .readyAt(isOverall ? toReadyAt(lineItem) : null) + .spentTokens(isOverall && activeDeliveryPlan != null ? activeDeliveryPlan.getSpentTokens() : null) + .pacingFrequency(isOverall && activeDeliveryPlan != null + ? activeDeliveryPlan.getDeliveryRateInMilliseconds() + : null) + .build(); + } + + private String toReadyAt(LineItem lineItem) { + final ZonedDateTime readyAt = ObjectUtils.getIfNotNull(lineItem, LineItem::getReadyAt); + return readyAt != null ? UTC_MILLIS_FORMATTER.format(readyAt) : null; + } + + private Long accountRequests(String accountId, DeliveryProgress deliveryProgress) { + final LongAdder accountRequests = accountId != null + ? deliveryProgress.getRequestsPerAccount().get(accountId) + : null; + return accountRequests != null ? accountRequests.sum() : null; + } + + private Set lostToLineItems(org.prebid.server.deals.lineitem.LineItemStatus lineItemStatus, + DeliveryProgress deliveryProgress) { + final Map lostTo = + deliveryProgress.getLineItemIdToLost().get(lineItemStatus.getLineItemId()); + + if (lostTo != null) { + return lostTo.values().stream() + .sorted(LOST_TO_LINE_ITEM_COMPARATOR.reversed()) + .map(this::toLostToLineItems) + .limit(competitorsNumber) + .collect(Collectors.toSet()); + } + + return null; + } + + private LostToLineItem toLostToLineItems(org.prebid.server.deals.lineitem.LostToLineItem lostToLineItem) { + final String lineItemId = lostToLineItem.getLineItemId(); + return LostToLineItem.of( + ObjectUtils.getIfNotNull(lineItemService.getLineItemById(lineItemId), LineItem::getSource), lineItemId, + lostToLineItem.getCount().sum()); + } + + private static Set deliverySchedule( + org.prebid.server.deals.lineitem.LineItemStatus lineItemStatus, + org.prebid.server.deals.lineitem.LineItemStatus overallLineItemStatus, + DeliveryPlan activeDeliveryPlan) { + + final Map idToDeliveryPlan = overallLineItemStatus != null + ? overallLineItemStatus.getDeliveryPlans().stream() + .collect(Collectors.toMap(DeliveryPlan::getPlanId, Function.identity())) + : Collections.emptyMap(); + + final Set deliverySchedules = lineItemStatus.getDeliveryPlans().stream() + .map(deliveryPlan -> toDeliverySchedule(deliveryPlan, idToDeliveryPlan.get(deliveryPlan.getPlanId()))) + .collect(Collectors.toSet()); + + if (CollectionUtils.isEmpty(deliverySchedules)) { + if (activeDeliveryPlan != null) { + deliverySchedules.add(DeliveryProgressReportFactory + .toDeliverySchedule(activeDeliveryPlan.withoutSpentTokens())); + } + } + return deliverySchedules; + } + + static DeliverySchedule toDeliverySchedule(DeliveryPlan deliveryPlan) { + return toDeliverySchedule(deliveryPlan, null); + } + + private static DeliverySchedule toDeliverySchedule(DeliveryPlan plan, DeliveryPlan overallPlan) { + final Map priorityClassToTotalSpent = overallPlan != null + ? overallPlan.getDeliveryTokens().stream() + .collect(Collectors.toMap(DeliveryToken::getPriorityClass, deliveryToken -> deliveryToken.getSpent() + .sum())) + : Collections.emptyMap(); + + final Set tokens = plan.getDeliveryTokens().stream() + .map(token -> Token.of(token.getPriorityClass(), token.getTotal(), + token.getSpent().sum(), priorityClassToTotalSpent.get(token.getPriorityClass()))) + .collect(Collectors.toSet()); + + return DeliverySchedule.builder() + .planId(plan.getPlanId()) + .planStartTimeStamp(formatTimeStamp(plan.getStartTimeStamp())) + .planExpirationTimeStamp(formatTimeStamp(plan.getEndTimeStamp())) + .planUpdatedTimeStamp(formatTimeStamp(plan.getUpdatedTimeStamp())) + .tokens(tokens) + .build(); + } + + private static String formatTimeStamp(ZonedDateTime zonedDateTime) { + return zonedDateTime != null + ? UTC_MILLIS_FORMATTER.format(zonedDateTime) + : null; + } + + private static class LostToLineItemComparator implements + Comparator { + + @Override + public int compare(org.prebid.server.deals.lineitem.LostToLineItem lostToLineItem1, + org.prebid.server.deals.lineitem.LostToLineItem lostToLineItem2) { + return Long.compare(lostToLineItem1.getCount().sum(), lostToLineItem2.getCount().sum()); + } + } +} diff --git a/src/main/java/org/prebid/server/deals/DeliveryProgressService.java b/src/main/java/org/prebid/server/deals/DeliveryProgressService.java new file mode 100644 index 00000000000..76dfae0c7fd --- /dev/null +++ b/src/main/java/org/prebid/server/deals/DeliveryProgressService.java @@ -0,0 +1,206 @@ +package org.prebid.server.deals; + +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.deals.events.ApplicationEventProcessor; +import org.prebid.server.deals.lineitem.DeliveryPlan; +import org.prebid.server.deals.lineitem.DeliveryProgress; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.lineitem.LineItemStatus; +import org.prebid.server.deals.model.DeliveryProgressProperties; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; +import org.prebid.server.deals.proto.report.DeliverySchedule; +import org.prebid.server.deals.proto.report.LineItemStatusReport; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.log.CriteriaLogManager; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * Tracks {@link LineItem}s' progress. + */ +public class DeliveryProgressService implements ApplicationEventProcessor { + + private static final Logger logger = LoggerFactory.getLogger(DeliveryProgressService.class); + + private final DeliveryProgressProperties deliveryProgressProperties; + private final LineItemService lineItemService; + private final DeliveryStatsService deliveryStatsService; + private final DeliveryProgressReportFactory deliveryProgressReportFactory; + private final Clock clock; + private final CriteriaLogManager criteriaLogManager; + + private final long lineItemStatusTtl; + + protected DeliveryProgress overallDeliveryProgress; + protected DeliveryProgress currentDeliveryProgress; + + public DeliveryProgressService(DeliveryProgressProperties deliveryProgressProperties, + LineItemService lineItemService, + DeliveryStatsService deliveryStatsService, + DeliveryProgressReportFactory deliveryProgressReportFactory, + Clock clock, + CriteriaLogManager criteriaLogManager) { + this.deliveryProgressProperties = Objects.requireNonNull(deliveryProgressProperties); + this.lineItemService = Objects.requireNonNull(lineItemService); + this.deliveryStatsService = Objects.requireNonNull(deliveryStatsService); + this.deliveryProgressReportFactory = Objects.requireNonNull(deliveryProgressReportFactory); + this.clock = Objects.requireNonNull(clock); + this.criteriaLogManager = Objects.requireNonNull(criteriaLogManager); + + this.lineItemStatusTtl = TimeUnit.SECONDS.toMillis(deliveryProgressProperties.getLineItemStatusTtlSeconds()); + + final ZonedDateTime now = ZonedDateTime.now(clock); + overallDeliveryProgress = DeliveryProgress.of(now, lineItemService); + currentDeliveryProgress = DeliveryProgress.of(now, lineItemService); + } + + public void shutdown() { + createDeliveryProgressReports(ZonedDateTime.now(clock)); + deliveryStatsService.sendDeliveryProgressReports(); + } + + /** + * Updates copy of overall {@link DeliveryProgress} with current delivery progress and + * creates {@link DeliveryProgressReport}. + */ + public DeliveryProgressReport getOverallDeliveryProgressReport() { + final DeliveryProgress overallDeliveryProgressCopy = + overallDeliveryProgress.copyWithOriginalPlans(); + + lineItemService.getLineItems() + .forEach(lineItem -> overallDeliveryProgressCopy.getLineItemStatuses() + .putIfAbsent(lineItem.getLineItemId(), LineItemStatus.of(lineItem.getLineItemId(), + lineItem.getSource(), lineItem.getDealId(), lineItem.getExtLineItemId(), + lineItem.getAccountId()))); + + overallDeliveryProgressCopy.mergeFrom(currentDeliveryProgress); + return deliveryProgressReportFactory.fromDeliveryProgress(overallDeliveryProgressCopy, ZonedDateTime.now(clock), + true); + } + + /** + * Updates delivery progress from {@link AuctionContext} statistics. + */ + @Override + public void processAuctionEvent(AuctionContext auctionContext) { + processAuctionEvent(auctionContext.getTxnLog(), auctionContext.getAccount().getId(), ZonedDateTime.now(clock)); + } + + /** + * Updates delivery progress from {@link AuctionContext} statistics for defined date. + */ + protected void processAuctionEvent(TxnLog txnLog, String accountId, ZonedDateTime now) { + final Map planIdToTokenPriority = new HashMap<>(); + + txnLog.lineItemSentToClientAsTopMatch().stream() + .map(lineItemService::getLineItemById) + .filter(Objects::nonNull) + .filter(lineItem -> lineItem.getActiveDeliveryPlan() != null) + .forEach(lineItem -> incrementTokens(lineItem, now, planIdToTokenPriority)); + + currentDeliveryProgress.recordTransactionLog(txnLog, planIdToTokenPriority, accountId); + } + + /** + * Updates delivery progress with win event. + */ + @Override + public void processLineItemWinEvent(String lineItemId) { + final LineItem lineItem = lineItemService.getLineItemById(lineItemId); + if (lineItem != null) { + currentDeliveryProgress.recordWinEvent(lineItemId); + criteriaLogManager.log(logger, lineItem.getAccountId(), lineItem.getSource(), lineItemId, + String.format("Win event for LineItem with id %s was recorded", lineItemId), logger::debug); + } + } + + @Override + public void processDeliveryProgressUpdateEvent() { + lineItemService.getLineItems() + .stream() + .filter(lineItem -> lineItem.getActiveDeliveryPlan() != null) + .forEach(this::mergePlanFromLineItem); + } + + private void mergePlanFromLineItem(LineItem lineItem) { + overallDeliveryProgress.upsertPlanReferenceFromLineItem(lineItem); + currentDeliveryProgress.mergePlanFromLineItem(lineItem); + } + + /** + * Prepare report from statuses to send it to delivery stats. + */ + public void createDeliveryProgressReports(ZonedDateTime now) { + final DeliveryProgress deliveryProgressToReport = currentDeliveryProgress; + + currentDeliveryProgress = DeliveryProgress.of(now, lineItemService); + + deliveryProgressToReport.setEndTimeStamp(now); + deliveryProgressToReport.updateWithActiveLineItems(lineItemService.getLineItems()); + + overallDeliveryProgress.mergeFrom(deliveryProgressToReport); + + deliveryStatsService.addDeliveryProgress(deliveryProgressToReport, + overallDeliveryProgress.getLineItemStatuses()); + + overallDeliveryProgress.cleanLineItemStatuses( + now, lineItemStatusTtl, deliveryProgressProperties.getCachedPlansNumber()); + } + + public void invalidateLineItemsByIds(List lineItemIds) { + overallDeliveryProgress.getLineItemStatuses().entrySet() + .removeIf(stringLineItemEntry -> lineItemIds.contains(stringLineItemEntry.getKey())); + currentDeliveryProgress.getLineItemStatuses().entrySet() + .removeIf(stringLineItemEntry -> lineItemIds.contains(stringLineItemEntry.getKey())); + } + + public void invalidateLineItems() { + overallDeliveryProgress.getLineItemStatuses().clear(); + currentDeliveryProgress.getLineItemStatuses().clear(); + } + + /** + * Increments tokens for specified in parameters lineItem, plan and class priority. + */ + protected void incrementTokens(LineItem lineItem, ZonedDateTime now, Map planIdToTokenPriority) { + final Integer classPriority = lineItem.incSpentToken(now); + if (classPriority != null) { + planIdToTokenPriority.put(lineItem.getActiveDeliveryPlan().getPlanId(), classPriority); + } + } + + /** + * Returns {@link LineItemStatusReport} for the given {@link LineItem}'s ID. + */ + public LineItemStatusReport getLineItemStatusReport(String lineItemId, ZonedDateTime now) { + final LineItem lineItem = lineItemService.getLineItemById(lineItemId); + if (lineItem == null) { + throw new PreBidException(String.format("LineItem not found: %s", lineItemId)); + } + + final DeliveryPlan activeDeliveryPlan = lineItem.getActiveDeliveryPlan(); + if (activeDeliveryPlan == null) { + return LineItemStatusReport.builder() + .lineItemId(lineItemId) + .build(); + } + + final DeliverySchedule deliverySchedule = DeliveryProgressReportFactory.toDeliverySchedule(activeDeliveryPlan); + return LineItemStatusReport.builder() + .lineItemId(lineItemId) + .deliverySchedule(deliverySchedule) + .readyToServeTimestamp(lineItem.getReadyAt()) + .spentTokens(activeDeliveryPlan.getSpentTokens()) + .pacingFrequency(activeDeliveryPlan.getDeliveryRateInMilliseconds()) + .build(); + } +} diff --git a/src/main/java/org/prebid/server/deals/DeliveryStatsService.java b/src/main/java/org/prebid/server/deals/DeliveryStatsService.java new file mode 100644 index 00000000000..5763e73095b --- /dev/null +++ b/src/main/java/org/prebid/server/deals/DeliveryStatsService.java @@ -0,0 +1,291 @@ +package org.prebid.server.deals; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.http.HttpHeaders; +import org.prebid.server.deals.lineitem.DeliveryProgress; +import org.prebid.server.deals.lineitem.LineItemStatus; +import org.prebid.server.deals.model.AlertPriority; +import org.prebid.server.deals.model.DeliveryStatsProperties; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; +import org.prebid.server.deals.proto.report.DeliveryProgressReportBatch; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.Base64; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.zip.GZIPOutputStream; + +public class DeliveryStatsService implements Suspendable { + + private static final Logger logger = LoggerFactory.getLogger(DeliveryStatsService.class); + + private static final String BASIC_AUTH_PATTERN = "Basic %s"; + private static final String PG_TRX_ID = "pg-trx-id"; + private static final String PBS_DELIVERY_CLIENT_ERROR = "pbs-delivery-stats-client-error"; + private static final String SERVICE_NAME = "deliveryStats"; + public static final String GZIP = "gzip"; + + private final DeliveryStatsProperties deliveryStatsProperties; + private final DeliveryProgressReportFactory deliveryProgressReportFactory; + private final AlertHttpService alertHttpService; + private final HttpClient httpClient; + private final Metrics metrics; + private final Clock clock; + private final Vertx vertx; + private final JacksonMapper mapper; + + private final String basicAuthHeader; + private final NavigableSet requiredBatches; + private volatile boolean isSuspended; + + public DeliveryStatsService(DeliveryStatsProperties deliveryStatsProperties, + DeliveryProgressReportFactory deliveryProgressReportFactory, + AlertHttpService alertHttpService, + HttpClient httpClient, + Metrics metrics, + Clock clock, + Vertx vertx, + JacksonMapper mapper) { + + this.deliveryStatsProperties = Objects.requireNonNull(deliveryStatsProperties); + this.deliveryProgressReportFactory = Objects.requireNonNull(deliveryProgressReportFactory); + this.alertHttpService = Objects.requireNonNull(alertHttpService); + this.httpClient = Objects.requireNonNull(httpClient); + this.clock = Objects.requireNonNull(clock); + this.vertx = Objects.requireNonNull(vertx); + this.metrics = Objects.requireNonNull(metrics); + this.mapper = Objects.requireNonNull(mapper); + this.basicAuthHeader = authHeader(deliveryStatsProperties.getUsername(), deliveryStatsProperties.getPassword()); + + requiredBatches = new ConcurrentSkipListSet<>(Comparator + .comparing(DeliveryProgressReportBatch::getDataWindowEndTimeStamp) + .thenComparing(DeliveryProgressReportBatch::hashCode)); + } + + @Override + public void suspend() { + isSuspended = true; + } + + public void addDeliveryProgress(DeliveryProgress deliveryProgress, + Map overallLineItemStatuses) { + requiredBatches.add(deliveryProgressReportFactory.batchFromDeliveryProgress(deliveryProgress, + overallLineItemStatuses, null, deliveryStatsProperties.getLineItemsPerReport(), false)); + } + + public void sendDeliveryProgressReports() { + sendDeliveryProgressReports(ZonedDateTime.now(clock)); + } + + public void sendDeliveryProgressReports(ZonedDateTime now) { + if (isSuspended) { + logger.warn("Report will not be sent, as service was suspended from register response"); + return; + } + final long batchesIntervalMs = deliveryStatsProperties.getBatchesIntervalMs(); + final int batchesCount = requiredBatches.size(); + final Set sentBatches = new HashSet<>(); + requiredBatches.stream() + .reduce(Future.succeededFuture(), + (future, batch) -> future.compose(v -> sendBatch(batch, now) + .map(aVoid -> sentBatches.add(batch)) + .compose(aVoid -> batchesIntervalMs > 0 && batchesCount > sentBatches.size() + ? setInterval(batchesIntervalMs) + : Future.succeededFuture())), + // combiner does not do any useful operations, just required for this type of reduce operation + (a, b) -> Promise.promise().future()) + .setHandler(result -> handleDeliveryResult(result, batchesCount, sentBatches)); + } + + protected Future sendBatch(DeliveryProgressReportBatch deliveryProgressReportBatch, ZonedDateTime now) { + final Promise promise = Promise.promise(); + final MultiMap headers = headers(); + final Set sentReports = new HashSet<>(); + final long reportIntervalMs = deliveryStatsProperties.getReportsIntervalMs(); + final Set reports = deliveryProgressReportBatch.getReports(); + final int reportsCount = reports.size(); + reports.stream() + .reduce(Future.succeededFuture(), + (future, report) -> future.compose(v -> sendReport(report, headers, now) + .map(aVoid -> sentReports.add(report))) + .compose(aVoid -> reportIntervalMs > 0 && reportsCount > sentReports.size() + ? setInterval(reportIntervalMs) + : Future.succeededFuture()), + (a, b) -> Promise.promise().future()) + .setHandler(result -> handleBatchDelivery(result, deliveryProgressReportBatch, sentReports, promise)); + return promise.future(); + } + + protected Future sendReport(DeliveryProgressReport deliveryProgressReport, MultiMap headers, + ZonedDateTime now) { + final Promise promise = Promise.promise(); + final long startTime = clock.millis(); + if (isSuspended) { + logger.warn("Report will not be sent, as service was suspended from register response"); + promise.complete(); + return promise.future(); + } + + final String body = mapper.encode(deliveryProgressReportFactory + .updateReportTimeStamp(deliveryProgressReport, now)); + + logger.info("Sending delivery progress report to Delivery Stats, {0} is {1}", PG_TRX_ID, + headers.get(PG_TRX_ID)); + logger.debug("Delivery progress report is: {0}", body); + if (deliveryStatsProperties.isRequestCompressionEnabled()) { + headers.add(HttpHeaders.CONTENT_ENCODING, GZIP); + httpClient.request(HttpMethod.POST, deliveryStatsProperties.getEndpoint(), headers, gzipBody(body), + deliveryStatsProperties.getTimeoutMs()) + .setHandler(result -> handleDeliveryProgressReport(result, deliveryProgressReport, promise, + startTime)); + } else { + httpClient.post(deliveryStatsProperties.getEndpoint(), headers, body, + deliveryStatsProperties.getTimeoutMs()) + .setHandler(result -> handleDeliveryProgressReport(result, deliveryProgressReport, promise, + startTime)); + } + + return promise.future(); + } + + /** + * Handles delivery report response from Planner. + */ + private void handleDeliveryProgressReport(AsyncResult result, + DeliveryProgressReport deliveryProgressReport, + Promise promise, + long startTime) { + metrics.updateRequestTimeMetric(MetricName.delivery_request_time, clock.millis() - startTime); + if (result.failed()) { + logger.warn("Cannot send delivery progress report to delivery stats service", result.cause()); + promise.fail(new PreBidException(String.format("Sending report with id = %s failed in a reason: %s", + deliveryProgressReport.getReportId(), result.cause().getMessage()))); + } else { + final int statusCode = result.result().getStatusCode(); + final String reportId = deliveryProgressReport.getReportId(); + if (statusCode == 200 || statusCode == 409) { + handleSuccessfulResponse(deliveryProgressReport, promise, statusCode, reportId); + } else { + logger.warn("HTTP status code {0}", statusCode); + promise.fail(new PreBidException(String.format("Delivery stats service responded with status" + + " code = %s for report with id = %s", statusCode, deliveryProgressReport.getReportId()))); + } + } + } + + private void handleSuccessfulResponse(DeliveryProgressReport deliveryProgressReport, Promise promise, + int statusCode, String reportId) { + metrics.updateDeliveryRequestMetric(true); + promise.complete(); + if (statusCode == 409) { + logger.info("Delivery stats service respond with 409 duplicated, report with {0} line items and id = {1}" + + " was already delivered before and will be removed from from delivery queue", + deliveryProgressReport.getLineItemStatus().size(), reportId); + } else { + logger.info("Delivery progress report with {0} line items and id = {1} was successfully sent to" + + " delivery stats service", deliveryProgressReport.getLineItemStatus().size(), reportId); + } + } + + private Future setInterval(long interval) { + Promise promise = Promise.promise(); + vertx.setTimer(interval, event -> promise.complete()); + return promise.future(); + } + + private void handleDeliveryResult(AsyncResult result, int reportBatchesNumber, + Set sentBatches) { + if (result.failed()) { + logger.warn("Failed to send {0} report batches, {1} report batches left to send." + + " Reason is: {2}", reportBatchesNumber, reportBatchesNumber - sentBatches.size(), + result.cause().getMessage()); + alertHttpService.alertWithPeriod(SERVICE_NAME, PBS_DELIVERY_CLIENT_ERROR, AlertPriority.MEDIUM, + String.format("Report was not send to delivery stats service with a reason: %s", + result.cause().getMessage())); + requiredBatches.removeAll(sentBatches); + handleFailedReportDelivery(); + } else { + requiredBatches.clear(); + alertHttpService.resetAlertCount(PBS_DELIVERY_CLIENT_ERROR); + logger.info("{0} report batches were successfully sent.", reportBatchesNumber); + } + } + + private void handleBatchDelivery(AsyncResult result, + DeliveryProgressReportBatch deliveryProgressReportBatch, + Set sentReports, + Promise promise) { + final String reportId = deliveryProgressReportBatch.getReportId(); + final String endTimeWindow = deliveryProgressReportBatch.getDataWindowEndTimeStamp(); + final int batchSize = deliveryProgressReportBatch.getReports().size(); + final int sentSize = sentReports.size(); + if (result.succeeded()) { + logger.info("Batch of reports with reports id = {0}, end time window = {1} and size {2} was successfully" + + " sent", reportId, endTimeWindow, batchSize); + promise.complete(); + } else { + logger.warn("Failed to sent batch of reports with reports id = {0} end time windows = {1}." + + " {2} out of {3} were sent.", reportId, endTimeWindow, sentSize, batchSize); + deliveryProgressReportBatch.removeReports(sentReports); + promise.fail(result.cause().getMessage()); + } + } + + protected MultiMap headers() { + return MultiMap.caseInsensitiveMultiMap() + .add(HttpUtil.AUTHORIZATION_HEADER, basicAuthHeader) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpUtil.APPLICATION_JSON_CONTENT_TYPE) + .add(PG_TRX_ID, UUID.randomUUID().toString()); + } + + /** + * Creates Authorization header value from username and password. + */ + private static String authHeader(String username, String password) { + return String.format( + BASIC_AUTH_PATTERN, + Base64.getEncoder().encodeToString((username + ':' + password).getBytes())); + } + + private static byte[] gzipBody(String body) { + try (ByteArrayOutputStream obj = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(obj)) { + gzip.write(body.getBytes(StandardCharsets.UTF_8)); + gzip.finish(); + return obj.toByteArray(); + } catch (IOException e) { + throw new PreBidException(String.format("Failed to gzip request with a reason : %s", e.getMessage())); + } + } + + private void handleFailedReportDelivery() { + metrics.updateDeliveryRequestMetric(false); + while (requiredBatches.size() > deliveryStatsProperties.getCachedReportsNumber()) { + requiredBatches.pollFirst(); + } + } +} diff --git a/src/main/java/org/prebid/server/deals/LineItemService.java b/src/main/java/org/prebid/server/deals/LineItemService.java new file mode 100644 index 00000000000..04d284bdc69 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/LineItemService.java @@ -0,0 +1,554 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.User; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.deals.events.ApplicationEventService; +import org.prebid.server.deals.lineitem.DeliveryPlan; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.model.MatchLineItemsResult; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.deals.proto.DeliverySchedule; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.deals.proto.Price; +import org.prebid.server.deals.targeting.TargetingDefinition; +import org.prebid.server.exception.TargetingSyntaxException; +import org.prebid.server.log.CriteriaLogManager; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal.Category; +import org.prebid.server.util.StreamUtil; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Works with {@link LineItem} related information. + */ +public class LineItemService { + + private static final Logger logger = LoggerFactory.getLogger(LineItemService.class); + + private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .toFormatter(); + + private static final String PREBID_EXT = "prebid"; + private static final String BIDDER_EXT = "bidder"; + private static final String ACTIVE = "active"; + + private final Comparator lineItemComparator = Comparator + .comparing(LineItem::getHighestUnspentTokensClass) + .thenComparing(LineItem::getRelativePriority) + .thenComparing(LineItem::getCpm, Comparator.reverseOrder()); + + private final int maxDealsPerBidder; + private final TargetingService targetingService; + private final BidderCatalog bidderCatalog; + private final CurrencyConversionService conversionService; + protected final ApplicationEventService applicationEventService; + private final String adServerCurrency; + private final Clock clock; + private final CriteriaLogManager criteriaLogManager; + + protected final Map idToLineItems; + protected volatile boolean isPlannerResponsive; + + public LineItemService(int maxDealsPerBidder, + TargetingService targetingService, + BidderCatalog bidderCatalog, + CurrencyConversionService conversionService, + ApplicationEventService applicationEventService, + String adServerCurrency, + Clock clock, + CriteriaLogManager criteriaLogManager) { + + this.maxDealsPerBidder = maxDealsPerBidder; + this.targetingService = Objects.requireNonNull(targetingService); + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + this.conversionService = Objects.requireNonNull(conversionService); + this.applicationEventService = Objects.requireNonNull(applicationEventService); + this.adServerCurrency = Objects.requireNonNull(adServerCurrency); + this.clock = Objects.requireNonNull(clock); + this.criteriaLogManager = Objects.requireNonNull(criteriaLogManager); + + idToLineItems = new ConcurrentHashMap<>(); + } + + /** + * Returns {@link LineItem} by its id. + */ + public LineItem getLineItemById(String lineItemId) { + return idToLineItems.get(lineItemId); + } + + /** + * Returns true when account has at least one active {@link LineItem}. + */ + public boolean accountHasDeals(AuctionContext auctionContext) { + return accountHasDeals(auctionContext.getAccount().getId(), ZonedDateTime.now(clock)); + } + + /** + * Returns true when account has at least one active {@link LineItem} in the given time. + */ + public boolean accountHasDeals(String account, ZonedDateTime now) { + return StringUtils.isNotEmpty(account) + && idToLineItems.values().stream().anyMatch(lineItem -> Objects.equals(lineItem.getAccountId(), account) + && lineItem.isActive(now)); + } + + /** + * Finds among active Line Items those matching Imp of the OpenRTB2 request + * taking into account Line Items’ targeting and delivery progress. + */ + public MatchLineItemsResult findMatchingLineItems(AuctionContext auctionContext, Imp imp) { + final ZonedDateTime now = ZonedDateTime.now(clock); + return findMatchingLineItems(auctionContext, imp, now); + } + + /** + * Finds among active Line Items those matching Imp of the OpenRTB2 request + * taking into account Line Items’ targeting and delivery progress by the given time. + */ + protected MatchLineItemsResult findMatchingLineItems(AuctionContext auctionContext, Imp imp, ZonedDateTime now) { + final List matchedLineItems = getPreMatchedLineItems(auctionContext.getAccount().getId(), + imp, extractAliases(auctionContext.getBidRequest())) + .stream() + .filter(lineItem -> isTargetingMatched(lineItem, imp, auctionContext)) + .collect(Collectors.toList()); + return MatchLineItemsResult.of(postProcessMatchedLineItems(matchedLineItems, auctionContext, imp, now)); + } + + public void updateIsPlannerResponsive(boolean isPlannerResponsive) { + this.isPlannerResponsive = isPlannerResponsive; + } + + /** + * Updates metadata, starts tracking new {@link LineItem}s and {@link DeliverySchedule}s + * and remove from tracking expired. + */ + public void updateLineItems(List planResponse, boolean isPlannerResponsive) { + updateLineItems(planResponse, isPlannerResponsive, ZonedDateTime.now(clock)); + } + + public void updateLineItems(List planResponse, boolean isPlannerResponsive, ZonedDateTime now) { + this.isPlannerResponsive = isPlannerResponsive; + if (isPlannerResponsive) { + final List lineItemsMetaData = ListUtils.emptyIfNull(planResponse).stream() + .filter(lineItemMetaData -> !isExpired(now, lineItemMetaData.getEndTimeStamp())) + .filter(lineItemMetaData -> Objects.equals(lineItemMetaData.getStatus(), ACTIVE)) + .collect(Collectors.toList()); + + removeInactiveLineItems(planResponse, now); + lineItemsMetaData.forEach(lineItemMetaData -> updateLineItem(lineItemMetaData, now)); + } + } + + public void invalidateLineItemsByIds(List lineItemIds) { + idToLineItems.entrySet().removeIf(stringLineItemEntry -> lineItemIds.contains(stringLineItemEntry.getKey())); + logger.info("Line Items with ids {0} were removed", String.join(", ", lineItemIds)); + } + + public void invalidateLineItems() { + final String lineItemsToRemove = String.join(", ", idToLineItems.keySet()); + idToLineItems.clear(); + logger.info("Line Items with ids {0} were removed", lineItemsToRemove); + } + + private boolean isExpired(ZonedDateTime now, ZonedDateTime endTime) { + return now.isAfter(endTime); + } + + private void removeInactiveLineItems(List planResponse, ZonedDateTime now) { + final Set lineItemsToRemove = ListUtils.emptyIfNull(planResponse).stream() + .filter(lineItemMetaData -> !Objects.equals(lineItemMetaData.getStatus(), ACTIVE) + || isExpired(now, lineItemMetaData.getEndTimeStamp())) + .map(LineItemMetaData::getLineItemId) + .collect(Collectors.toSet()); + + idToLineItems.entrySet().stream() + .filter(entry -> isExpired(now, entry.getValue().getEndTimeStamp())) + .map(Map.Entry::getKey) + .collect(Collectors.toCollection(() -> lineItemsToRemove)); + + if (CollectionUtils.isNotEmpty(lineItemsToRemove)) { + logger.info("Line Items {0} were dropped as expired or inactive", String.join(", ", lineItemsToRemove)); + } + idToLineItems.entrySet().removeIf(entry -> lineItemsToRemove.contains(entry.getKey())); + } + + protected Collection getLineItems() { + return idToLineItems.values(); + } + + protected Set getLineItemIds() { + return idToLineItems.keySet(); + } + + protected void updateLineItem(LineItemMetaData lineItemMetaData, ZonedDateTime now) { + final TargetingDefinition targetingDefinition = makeTargeting(lineItemMetaData); + final Price normalizedPrice = normalizedPrice(lineItemMetaData); + + idToLineItems.compute(lineItemMetaData.getLineItemId(), (id, li) -> li != null + ? li.withUpdatedMetadata(lineItemMetaData, normalizedPrice, targetingDefinition, li.getReadyAt(), now) + : LineItem.of(lineItemMetaData, normalizedPrice, targetingDefinition, now)); + } + + public void advanceToNextPlan(ZonedDateTime now) { + final Collection lineItems = idToLineItems.values(); + for (LineItem lineItem : lineItems) { + lineItem.advanceToNextPlan(now, isPlannerResponsive); + } + applicationEventService.publishDeliveryUpdateEvent(); + } + + /** + * Creates {@link TargetingDefinition} from {@link LineItemMetaData} targeting json node. + */ + private TargetingDefinition makeTargeting(LineItemMetaData lineItemMetaData) { + TargetingDefinition targetingDefinition; + try { + targetingDefinition = targetingService.parseTargetingDefinition(lineItemMetaData.getTargeting(), + lineItemMetaData.getLineItemId()); + } catch (TargetingSyntaxException e) { + criteriaLogManager.log(logger, lineItemMetaData.getAccountId(), lineItemMetaData.getSource(), + lineItemMetaData.getLineItemId(), + String.format("Line item targeting parsing failed with a reason: %s", e.getMessage()), + logger::warn); + targetingDefinition = null; + } + return targetingDefinition; + } + + /** + * Returns {@link Price} with converted lineItem cpm to adServerCurrency. + */ + private Price normalizedPrice(LineItemMetaData lineItemMetaData) { + final Price price = lineItemMetaData.getPrice(); + if (price == null) { + return null; + } + + final String receivedCur = price.getCurrency(); + if (StringUtils.equals(adServerCurrency, receivedCur)) { + return price; + } + final BigDecimal updatedCpm = conversionService + .convertCurrency(price.getCpm(), Collections.emptyMap(), adServerCurrency, receivedCur, null); + + return Price.of(updatedCpm, adServerCurrency); + } + + /** + * Checks if bidder is valid against configured bidders in {@link BidderCatalog} or aliases. + */ + private boolean isValidActiveBidder(String bidder, Map aliases) { + return !bidderCatalog.isDeprecatedName(bidder) + && (bidderCatalog.isValidName(bidder) || aliases.containsKey(bidder)); + } + + /** + * Returns true if collection of bidder codes contains bidder or it's alias value. + */ + private boolean containBidderCodeConsideringAliases(List bidders, String bidder, + Map aliases) { + return bidders.contains(bidder) || bidders.contains(aliases.get(bidder)) + || bidders.stream().anyMatch(bidderCode -> bidder.equals(aliases.get(bidderCode))); + } + + /** + * Return {@link List} matched to {@link Imp} bidders considering aliases. + */ + private List getPreMatchedLineItems(String accountId, Imp imp, Map aliases) { + if (StringUtils.isBlank(accountId)) { + return Collections.emptyList(); + } + + final List accountsLineItems = idToLineItems.values().stream() + .filter(lineItem -> lineItem.getAccountId().equals(accountId)) + .collect(Collectors.toList()); + + if (accountsLineItems.isEmpty()) { + criteriaLogManager.log(logger, accountId, + String.format("There are no line items for account %s", accountId), logger::debug); + return Collections.emptyList(); + } + + final List bidders = StreamUtil.asStream(bidderParamsFromImp(imp).fieldNames()) + .filter(bidder -> isValidActiveBidder(bidder, aliases)) + .distinct() + .collect(Collectors.toList()); + + return accountsLineItems.stream() + .filter(lineItem -> containBidderCodeConsideringAliases(bidders, lineItem.getSource(), aliases)) + .collect(Collectors.toList()); + } + + private static JsonNode bidderParamsFromImp(Imp imp) { + return imp.getExt().get(PREBID_EXT).get(BIDDER_EXT); + } + + /** + * Extracts aliases from {@link BidRequest}. + */ + private Map extractAliases(BidRequest bidRequest) { + final ExtRequest extRequest = bidRequest.getExt(); + final ExtRequestPrebid prebid = extRequest != null ? extRequest.getPrebid() : null; + final Map aliases = prebid != null ? prebid.getAliases() : null; + return aliases != null ? aliases : Collections.emptyMap(); + } + + /** + * Returns true if {@link LineItem}s {@link TargetingDefinition} matches to {@link Imp}. + *

+ * Updates deep debug log with matching information. + */ + private boolean isTargetingMatched(LineItem lineItem, Imp imp, AuctionContext auctionContext) { + final TargetingDefinition targetingDefinition = lineItem.getTargetingDefinition(); + final String accountId = auctionContext.getAccount().getId(); + final String source = lineItem.getSource(); + final String lineItemId = lineItem.getLineItemId(); + if (targetingDefinition == null) { + deepDebug(auctionContext, Category.targeting, + String.format("Line Item %s targeting was not defined or has incorrect format", + lineItemId), accountId, source, lineItemId); + return false; + } + + final boolean matched = targetingService.matchesTargeting(auctionContext, imp, + lineItem.getTargetingDefinition()); + if (matched) { + deepDebug(auctionContext, Category.targeting, + String.format("Line Item %s targeting matched imp with id %s", lineItemId, imp.getId()), + accountId, source, lineItemId); + } else { + deepDebug(auctionContext, Category.targeting, + String.format("Line Item %s targeting did not match imp with id %s", lineItemId, imp.getId()), + accountId, source, lineItemId); + } + return matched; + } + + /** + * Filters {@link LineItem}s by next parameters: fcaps, readyAt, limit per bidder, same deal line items. + */ + private List postProcessMatchedLineItems(List lineItems, AuctionContext auctionContext, Imp imp, + ZonedDateTime now) { + final TxnLog txnLog = auctionContext.getTxnLog(); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final User user = bidRequest.getUser(); + final ExtUser extUser = user.getExt(); + + return lineItems.stream() + .peek(lineItem -> txnLog.lineItemsMatchedWholeTargeting().add(lineItem.getLineItemId())) + .filter(lineItem -> isNotFrequencyCapped(extUser.getFcapIds(), lineItem, auctionContext, txnLog)) + .filter(lineItem -> planHasTokensIfPresent(lineItem, auctionContext)) + .filter(lineItem -> isReadyAtInPast(now, lineItem, auctionContext, txnLog)) + .peek(lineItem -> txnLog.lineItemsReadyToServe().add(lineItem.getLineItemId())) + .collect(Collectors.groupingBy(LineItem::getSource)) + .values().stream() + .map(valueAsLineItems -> filterLineItemPerBidder(valueAsLineItems, auctionContext, imp)) + .filter(CollectionUtils::isNotEmpty) + .peek(lineItemsForBidder -> recordInTxnSentToBidderAsTopMatch(txnLog, lineItemsForBidder)) + .flatMap(Collection::stream) + .peek(lineItem -> txnLog.lineItemsSentToBidder().get(lineItem.getSource()) + .add(lineItem.getLineItemId())) + .collect(Collectors.toList()); + } + + private boolean planHasTokensIfPresent(LineItem lineItem, AuctionContext auctionContext) { + final DeliveryPlan deliveryPlan = lineItem.getActiveDeliveryPlan(); + if (deliveryPlan == null) { + return true; + } + boolean hasUnspentTokens = deliveryPlan.getDeliveryTokens().stream() + .anyMatch(deliveryToken -> deliveryToken.getTotal() - deliveryToken.getSpent().sum() > 0); + if (!hasUnspentTokens) { + final String lineItemId = lineItem.getLineItemId(); + final String lineItemSource = lineItem.getSource(); + auctionContext.getTxnLog().lineItemsPacingDeferred().add(lineItemId); + deepDebug(auctionContext, Category.pacing, String.format("Matched Line Item %s for bidder %s does not" + + " have unspent tokens to be served", lineItemId, lineItemSource), + auctionContext.getAccount().getId(), lineItemSource, lineItemId); + } + return hasUnspentTokens; + } + + private boolean isReadyAtInPast(ZonedDateTime now, LineItem lineItem, AuctionContext auctionContext, + TxnLog txnLog) { + final ZonedDateTime readyAt = lineItem.getReadyAt(); + final boolean ready = readyAt != null && (readyAt.isEqual(now) || readyAt.isBefore(now)); + final String accountId = auctionContext.getAccount().getId(); + final String lineItemSource = lineItem.getSource(); + final String lineItemId = lineItem.getLineItemId(); + + if (ready) { + deepDebug(auctionContext, Category.pacing, String.format("Matched Line Item %s for bidder %s ready to " + + "serve. relPriority %d", lineItemId, lineItemSource, lineItem.getRelativePriority()), + accountId, lineItemSource, lineItemId); + } else { + txnLog.lineItemsPacingDeferred().add(lineItemId); + deepDebug(auctionContext, Category.pacing, String.format("Matched Line Item %s for bidder %s not ready to" + + " serve. Will be ready at %s, current time is %s", lineItemId, lineItemSource, + readyAt != null ? UTC_MILLIS_FORMATTER.format(readyAt) : "never", UTC_MILLIS_FORMATTER.format(now)), + accountId, lineItemSource, lineItemId); + } + + return ready; + } + + /** + * Returns false if {@link LineItem} has fcaps defined and either + * - one of them present in the list of fcaps reached + * - list of fcaps reached is null which means that calling User Data Store failed + *

+ * Otherwise returns true + *

+ * Has side effect - records discarded line item id in the transaction log + */ + private boolean isNotFrequencyCapped(List frequencyCappedByIds, LineItem lineItem, + AuctionContext auctionContext, TxnLog txnLog) { + if (CollectionUtils.isEmpty(lineItem.getFcapIds())) { + return true; + } + + final String lineItemId = lineItem.getLineItemId(); + final String accountId = auctionContext.getAccount().getId(); + final String lineItemSource = lineItem.getSource(); + + if (frequencyCappedByIds == null) { + txnLog.lineItemsMatchedTargetingFcapLookupFailed().add(lineItemId); + final String message = String.format("Failed to match fcap for Line Item %s bidder %s in a reason of bad " + + "response from user data service", lineItemId, lineItemSource); + deepDebug(auctionContext, Category.pacing, message, accountId, lineItemSource, lineItemId); + criteriaLogManager.log(logger, lineItem.getAccountId(), lineItem.getSource(), lineItemId, + String.format("Failed to match fcap for lineItem %s in a reason of bad response from user" + + " data service", lineItemId), logger::debug); + + return false; + } else if (!frequencyCappedByIds.isEmpty()) { + final Optional fcapIdOptional = lineItem.getFcapIds().stream() + .filter(frequencyCappedByIds::contains).findFirst(); + if (fcapIdOptional.isPresent()) { + final String fcapId = fcapIdOptional.get(); + txnLog.lineItemsMatchedTargetingFcapped().add(lineItemId); + final String message = String.format("Matched Line Item %s for bidder %s is " + + "frequency capped by fcap id %s.", lineItemId, lineItemSource, fcapId); + deepDebug(auctionContext, Category.pacing, message, accountId, lineItemSource, lineItemId); + criteriaLogManager.log(logger, lineItem.getAccountId(), lineItem.getSource(), lineItemId, + message, logger::debug); + return false; + } + } + + return true; + } + + /** + * Filters {@link LineItem} with the same deal id and cuts {@link List} by maxDealsPerBidder value. + */ + private List filterLineItemPerBidder(List lineItems, AuctionContext auctionContext, Imp imp) { + final List sortedLineItems = new ArrayList<>(lineItems); + Collections.shuffle(sortedLineItems); + sortedLineItems.sort(lineItemComparator); + + final List filteredLineItems = uniqueBySentToBidderAsTopMatch(sortedLineItems, auctionContext, imp); + updateLostToLineItems(filteredLineItems, auctionContext.getTxnLog()); + + final Set dealIds = new HashSet<>(); + final List resolvedLineItems = new ArrayList<>(); + for (final LineItem lineItem : filteredLineItems) { + final String dealId = lineItem.getDealId(); + if (!dealIds.contains(dealId)) { + dealIds.add(dealId); + resolvedLineItems.add(lineItem); + } + } + return resolvedLineItems.size() > maxDealsPerBidder + ? cutLineItemsToDealMaxNumber(resolvedLineItems) + : resolvedLineItems; + } + + /** + * Removes from consideration any line items that have already been sent to bidder as the TopMatch + * in a previous impression for auction. + */ + private List uniqueBySentToBidderAsTopMatch(List lineItems, AuctionContext auctionContext, + Imp imp) { + final TxnLog txnLog = auctionContext.getTxnLog(); + final Set topMatchedLineItems = txnLog.lineItemsSentToBidderAsTopMatch().values().stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + + final List result = new ArrayList<>(lineItems); + for (LineItem lineItem : lineItems) { + final String lineItemId = lineItem.getLineItemId(); + if (!topMatchedLineItems.contains(lineItemId)) { + return result; + } + result.remove(lineItem); + deepDebug(auctionContext, Category.cleanup, String.format( + "LineItem %s was dropped from imp with id %s because it was top match in another imp", + lineItemId, imp.getId()), auctionContext.getAccount().getId(), lineItem.getSource(), lineItemId); + } + return result; + } + + private List cutLineItemsToDealMaxNumber(List resolvedLineItems) { + resolvedLineItems.subList(maxDealsPerBidder, resolvedLineItems.size()) + .forEach(lineItem -> criteriaLogManager.log(logger, lineItem.getAccountId(), lineItem.getSource(), + lineItem.getLineItemId(), + String.format("LineItem %s was dropped by max deal per bidder limit %s", + lineItem.getLineItemId(), maxDealsPerBidder), logger::debug)); + return resolvedLineItems.subList(0, maxDealsPerBidder); + } + + private void updateLostToLineItems(List lineItems, TxnLog txnLog) { + for (int i = 1; i < lineItems.size(); i++) { + final LineItem lineItem = lineItems.get(i); + final Set lostTo = lineItems.subList(0, i).stream() + .map(LineItem::getLineItemId) + .collect(Collectors.toSet()); + txnLog.lostMatchingToLineItems().put(lineItem.getLineItemId(), lostTo); + } + } + + private void deepDebug(AuctionContext auctionContext, Category category, String message, String accountId, + String bidder, String lineItemId) { + criteriaLogManager.log(logger, accountId, bidder, lineItemId, message, logger::debug); + auctionContext.getDeepDebugLog().add(lineItemId, category, () -> message); + } + + private static void recordInTxnSentToBidderAsTopMatch(TxnLog txnLog, List lineItemsForBidder) { + final LineItem topLineItem = lineItemsForBidder.get(0); + txnLog.lineItemsSentToBidderAsTopMatch() + .get(topLineItem.getSource()) + .add(topLineItem.getLineItemId()); + } +} diff --git a/src/main/java/org/prebid/server/deals/PlannerService.java b/src/main/java/org/prebid/server/deals/PlannerService.java new file mode 100644 index 00000000000..042989de460 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/PlannerService.java @@ -0,0 +1,235 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.deals.model.AlertPriority; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.deals.model.PlannerProperties; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.time.Clock; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Class manages line item metadata retrieving from planner and reporting. + */ +public class PlannerService implements Suspendable { + + private static final Logger logger = LoggerFactory.getLogger(PlannerService.class); + + protected static final TypeReference> LINE_ITEM_METADATA_TYPE_REFERENCE + = new TypeReference>() { + }; + + private static final String BASIC_AUTH_PATTERN = "Basic %s"; + private static final String PG_TRX_ID = "pg-trx-id"; + private static final String INSTANCE_ID_PARAMETER = "instanceId"; + private static final String REGION_PARAMETER = "region"; + private static final String VENDOR_PARAMETER = "vendor"; + private static final String SERVICE_NAME = "planner"; + private static final String PBS_PLANNER_CLIENT_ERROR = "pbs-planner-client-error"; + private static final String PBS_PLANNER_EMPTY_RESPONSE = "pbs-planner-empty-response-error"; + + private final LineItemService lineItemService; + private final DeliveryProgressService deliveryProgressService; + private final AlertHttpService alertHttpService; + protected final HttpClient httpClient; + private final Metrics metrics; + private final Clock clock; + private final JacksonMapper mapper; + + protected final String planEndpoint; + private final long plannerTimeout; + private final String basicAuthHeader; + + protected final AtomicBoolean isPlannerResponsive; + private volatile boolean isSuspended; + + public PlannerService(PlannerProperties plannerProperties, + DeploymentProperties deploymentProperties, + LineItemService lineItemService, + DeliveryProgressService deliveryProgressService, + AlertHttpService alertHttpService, + HttpClient httpClient, + Metrics metrics, + Clock clock, + JacksonMapper mapper) { + this.lineItemService = Objects.requireNonNull(lineItemService); + this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); + this.alertHttpService = Objects.requireNonNull(alertHttpService); + this.httpClient = Objects.requireNonNull(httpClient); + this.metrics = Objects.requireNonNull(metrics); + this.clock = Objects.requireNonNull(clock); + this.mapper = Objects.requireNonNull(mapper); + + this.planEndpoint = buildPlannerMetaDataUrl(plannerProperties.getPlanEndpoint(), + deploymentProperties.getPbsHostId(), + deploymentProperties.getPbsRegion(), + deploymentProperties.getPbsVendor()); + this.plannerTimeout = plannerProperties.getTimeoutMs(); + this.basicAuthHeader = authHeader(plannerProperties.getUsername(), plannerProperties.getPassword()); + + this.isPlannerResponsive = new AtomicBoolean(true); + } + + @Override + public void suspend() { + isSuspended = true; + } + + /** + * Fetches line items meta data from Planner + */ + protected Future> fetchLineItemMetaData(String plannerUrl, MultiMap headers) { + logger.info("Requesting line items metadata and plans from Planner, {0} is {1}", PG_TRX_ID, + headers.get(PG_TRX_ID)); + final long startTime = clock.millis(); + return httpClient.get(plannerUrl, headers, plannerTimeout) + .map(httpClientResponse -> processLineItemMetaDataResponse(httpClientResponse, startTime)); + } + + protected MultiMap headers() { + return MultiMap.caseInsensitiveMultiMap() + .add(HttpUtil.AUTHORIZATION_HEADER, basicAuthHeader) + .add(PG_TRX_ID, UUID.randomUUID().toString()); + } + + /** + * Processes response from planner. + * If status code == 4xx - stop fetching process. + * If status code =! 2xx - start retry fetching process. + * If status code == 200 - parse response. + */ + protected List processLineItemMetaDataResponse(HttpClientResponse response, long startTime) { + final int statusCode = response.getStatusCode(); + if (statusCode != 200) { + throw new PreBidException(String.format("Failed to fetch data from Planner, HTTP status code %d", + statusCode)); + } + + final String body = response.getBody(); + if (body == null) { + throw new PreBidException("Failed to fetch data from planner, response can't be null"); + } + + metrics.updateRequestTimeMetric(MetricName.planner_request_time, clock.millis() - startTime); + + logger.debug("Received line item metadata and plans from Planner: {0}", body); + + try { + final List lineItemMetaData = mapper.decodeValue(body, + LINE_ITEM_METADATA_TYPE_REFERENCE); + validateForEmptyResponse(lineItemMetaData); + metrics.updateLineItemsNumberMetric(lineItemMetaData.size()); + logger.info("Received line item metadata from Planner, amount: {0}", lineItemMetaData.size()); + + return lineItemMetaData; + } catch (DecodeException e) { + final String errorMessage = String.format("Cannot parse response: %s", body); + throw new PreBidException(errorMessage, e); + } + } + + private void validateForEmptyResponse(List lineItemMetaData) { + if (CollectionUtils.isEmpty(lineItemMetaData)) { + alertHttpService.alertWithPeriod(SERVICE_NAME, PBS_PLANNER_EMPTY_RESPONSE, AlertPriority.LOW, + "Response without line items was received from planner"); + } else { + alertHttpService.resetAlertCount(PBS_PLANNER_EMPTY_RESPONSE); + } + } + + /** + * Creates Authorization header value from username and password. + */ + private static String authHeader(String username, String password) { + return String.format(BASIC_AUTH_PATTERN, Base64.getEncoder().encodeToString((username + ':' + password) + .getBytes())); + } + + /** + * Builds url for fetching metadata from planner + */ + private static String buildPlannerMetaDataUrl(String plannerMetaDataUrl, String pbsHostname, String pbsRegion, + String pbsVendor) { + return String.format("%s?%s=%s&%s=%s&%s=%s", plannerMetaDataUrl, INSTANCE_ID_PARAMETER, pbsHostname, + REGION_PARAMETER, pbsRegion, VENDOR_PARAMETER, pbsVendor); + } + + /** + * Fetches line item metadata from planner during the regular, not retry flow. + */ + public void updateLineItemMetaData() { + if (isSuspended) { + logger.warn("Fetch request was not sent to general planner, as planner service is suspended from" + + " register endpoint."); + return; + } + + final MultiMap headers = headers(); + fetchLineItemMetaData(planEndpoint, headers) + .recover(ignored -> startRecoveryProcess(planEndpoint, headers)) + .setHandler(this::handleInitializationResult); + } + + private Future> startRecoveryProcess(String planEndpoint, MultiMap headers) { + metrics.updatePlannerRequestMetric(false); + logger.info("Retry to fetch line items from general planner by uri = {0}", planEndpoint); + + return fetchLineItemMetaData(planEndpoint, headers); + } + + /** + * Handles result of initialization process. Sets metadata if request was successful. + */ + protected void handleInitializationResult(AsyncResult> plannerResponse) { + if (plannerResponse.succeeded()) { + handleSuccessInitialization(plannerResponse); + } else { + handleFailedInitialization(plannerResponse); + } + } + + private void handleSuccessInitialization(AsyncResult> plannerResponse) { + alertHttpService.resetAlertCount(PBS_PLANNER_CLIENT_ERROR); + metrics.updatePlannerRequestMetric(true); + isPlannerResponsive.set(true); + lineItemService.updateIsPlannerResponsive(true); + updateMetaData(plannerResponse.result()); + } + + private void handleFailedInitialization(AsyncResult> plannerResponse) { + final String message = String.format("Failed to retrieve line items from GP. Reason: %s", + plannerResponse.cause().getMessage()); + alertHttpService.alertWithPeriod(SERVICE_NAME, PBS_PLANNER_CLIENT_ERROR, AlertPriority.MEDIUM, message); + logger.warn(message); + isPlannerResponsive.set(false); + lineItemService.updateIsPlannerResponsive(false); + metrics.updatePlannerRequestMetric(false); + } + + /** + * Overwrites maps with metadata + */ + private void updateMetaData(List metaData) { + lineItemService.updateLineItems(metaData, isPlannerResponsive.get()); + deliveryProgressService.processDeliveryProgressUpdateEvent(); + } +} diff --git a/src/main/java/org/prebid/server/deals/RegisterService.java b/src/main/java/org/prebid/server/deals/RegisterService.java new file mode 100644 index 00000000000..70b6243477b --- /dev/null +++ b/src/main/java/org/prebid/server/deals/RegisterService.java @@ -0,0 +1,183 @@ +package org.prebid.server.deals; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.deals.events.AdminEventService; +import org.prebid.server.deals.model.AdminCentralResponse; +import org.prebid.server.deals.model.AlertPriority; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.deals.model.PlannerProperties; +import org.prebid.server.deals.proto.CurrencyServiceState; +import org.prebid.server.deals.proto.RegisterRequest; +import org.prebid.server.deals.proto.Status; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.health.HealthMonitor; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.Initializable; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.Base64; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class RegisterService implements Initializable, Suspendable { + + private static final Logger logger = LoggerFactory.getLogger(RegisterService.class); + + private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .toFormatter(); + + private static final String BASIC_AUTH_PATTERN = "Basic %s"; + private static final String PG_TRX_ID = "pg-trx-id"; + private static final String PBS_REGISTER_CLIENT_ERROR = "pbs-register-client-error"; + private static final String SERVICE_NAME = "register"; + + private final PlannerProperties plannerProperties; + private final DeploymentProperties deploymentProperties; + private final AdminEventService adminEventService; + private final DeliveryProgressService deliveryProgressService; + private final AlertHttpService alertHttpService; + private final HealthMonitor healthMonitor; + private final CurrencyConversionService currencyConversionService; + private final HttpClient httpClient; + private final Vertx vertx; + private final JacksonMapper mapper; + + private final long registerTimeout; + private final long registerPeriod; + private final String basicAuthHeader; + private volatile long registerTimerId; + private volatile boolean isSuspended; + + public RegisterService(PlannerProperties plannerProperties, + DeploymentProperties deploymentProperties, + AdminEventService adminEventService, + DeliveryProgressService deliveryProgressService, + AlertHttpService alertHttpService, + HealthMonitor healthMonitor, + CurrencyConversionService currencyConversionService, + HttpClient httpClient, + Vertx vertx, + JacksonMapper mapper) { + this.plannerProperties = Objects.requireNonNull(plannerProperties); + this.deploymentProperties = Objects.requireNonNull(deploymentProperties); + this.adminEventService = Objects.requireNonNull(adminEventService); + this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); + this.alertHttpService = Objects.requireNonNull(alertHttpService); + this.healthMonitor = Objects.requireNonNull(healthMonitor); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.httpClient = Objects.requireNonNull(httpClient); + this.vertx = Objects.requireNonNull(vertx); + this.mapper = Objects.requireNonNull(mapper); + + this.registerTimeout = plannerProperties.getTimeoutMs(); + this.registerPeriod = TimeUnit.SECONDS.toMillis(plannerProperties.getRegisterPeriodSeconds()); + this.basicAuthHeader = authHeader(plannerProperties.getUsername(), plannerProperties.getPassword()); + } + + /** + * Creates Authorization header value from username and password. + */ + private static String authHeader(String username, String password) { + return String.format(BASIC_AUTH_PATTERN, Base64.getEncoder().encodeToString((username + ':' + password) + .getBytes())); + } + + @Override + public void suspend() { + isSuspended = true; + vertx.cancelTimer(registerTimerId); + } + + @Override + public void initialize() { + registerTimerId = vertx.setPeriodic(registerPeriod, ignored -> register(headers())); + register(headers()); + } + + protected void register(MultiMap headers) { + if (isSuspended) { + logger.warn("Register request was not sent to general planner, as planner service is suspended from" + + " register endpoint."); + return; + } + + final BigDecimal healthIndex = healthMonitor.calculateHealthIndex(); + final ZonedDateTime currencyLastUpdate = currencyConversionService.getLastUpdated(); + final RegisterRequest request = RegisterRequest.of( + healthIndex, + Status.of(currencyLastUpdate != null + ? CurrencyServiceState.of(UTC_MILLIS_FORMATTER.format(currencyLastUpdate)) + : null, + deliveryProgressService.getOverallDeliveryProgressReport()), + deploymentProperties.getPbsHostId(), + deploymentProperties.getPbsRegion(), + deploymentProperties.getPbsVendor()); + final String body = mapper.encode(request); + + logger.info("Sending register request to Planner, {0} is {1}", PG_TRX_ID, headers.get(PG_TRX_ID)); + logger.debug("Register request payload: {0}", body); + + httpClient.post(plannerProperties.getRegisterEndpoint(), headers, body, registerTimeout) + .setHandler(this::handleRegister); + } + + protected MultiMap headers() { + return MultiMap.caseInsensitiveMultiMap() + .add(HttpUtil.AUTHORIZATION_HEADER, basicAuthHeader) + .add(PG_TRX_ID, UUID.randomUUID().toString()); + } + + private void handleRegister(AsyncResult asyncResult) { + if (asyncResult.failed()) { + final Throwable cause = asyncResult.cause(); + final String errorMessage = String.format("Error occurred while registering with the Planner: %s", cause); + alert(errorMessage, logger::warn); + } else { + final HttpClientResponse response = asyncResult.result(); + final int statusCode = response.getStatusCode(); + final String responseBody = response.getBody(); + if (statusCode == HttpResponseStatus.OK.code()) { + if (StringUtils.isNotBlank(responseBody)) { + adminEventService.publishAdminCentralEvent(parseRegisterResponse(responseBody)); + } + alertHttpService.resetAlertCount(PBS_REGISTER_CLIENT_ERROR); + } else { + final String errorMessage = String.format("Planner responded with non-successful code %s," + + " response: %s", statusCode, responseBody); + alert(errorMessage, logger::warn); + } + } + } + + private AdminCentralResponse parseRegisterResponse(String responseBody) { + try { + return mapper.decodeValue(responseBody, AdminCentralResponse.class); + } catch (DecodeException e) { + String errorMessage = String.format("Cannot parse register response: %s", responseBody); + alert(errorMessage, logger::warn); + throw new PreBidException(errorMessage, e); + } + } + + private void alert(String message, Consumer logger) { + alertHttpService.alertWithPeriod(SERVICE_NAME, PBS_REGISTER_CLIENT_ERROR, AlertPriority.MEDIUM, message); + logger.accept(message); + } +} diff --git a/src/main/java/org/prebid/server/deals/Suspendable.java b/src/main/java/org/prebid/server/deals/Suspendable.java new file mode 100644 index 00000000000..b834b4badfe --- /dev/null +++ b/src/main/java/org/prebid/server/deals/Suspendable.java @@ -0,0 +1,6 @@ +package org.prebid.server.deals; + +public interface Suspendable { + + void suspend(); +} diff --git a/src/main/java/org/prebid/server/deals/TargetingService.java b/src/main/java/org/prebid/server/deals/TargetingService.java new file mode 100644 index 00000000000..a8aecfb2e9f --- /dev/null +++ b/src/main/java/org/prebid/server/deals/TargetingService.java @@ -0,0 +1,353 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.TargetingDefinition; +import org.prebid.server.deals.targeting.interpret.And; +import org.prebid.server.deals.targeting.interpret.DomainMetricAwareExpression; +import org.prebid.server.deals.targeting.interpret.Expression; +import org.prebid.server.deals.targeting.interpret.InIntegers; +import org.prebid.server.deals.targeting.interpret.InStrings; +import org.prebid.server.deals.targeting.interpret.IntersectsIntegers; +import org.prebid.server.deals.targeting.interpret.IntersectsSizes; +import org.prebid.server.deals.targeting.interpret.IntersectsStrings; +import org.prebid.server.deals.targeting.interpret.Matches; +import org.prebid.server.deals.targeting.interpret.Not; +import org.prebid.server.deals.targeting.interpret.Or; +import org.prebid.server.deals.targeting.interpret.Within; +import org.prebid.server.deals.targeting.model.GeoRegion; +import org.prebid.server.deals.targeting.model.Size; +import org.prebid.server.deals.targeting.syntax.BooleanOperator; +import org.prebid.server.deals.targeting.syntax.MatchingFunction; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; +import org.prebid.server.exception.TargetingSyntaxException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.StreamUtil; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Responsible for parsing and interpreting targeting defined in the Line Items’ metadata + * and determining if individual requests match those targeting conditions. + */ +public class TargetingService { + + private final JacksonMapper mapper; + + public TargetingService(JacksonMapper mapper) { + this.mapper = Objects.requireNonNull(mapper); + } + + /** + * Accepts targeting definition expressed in JSON syntax (see below), + * parses it and transforms it into an object supporting efficient evaluation + * of the targeting rules against the OpenRTB2 request. + */ + public TargetingDefinition parseTargetingDefinition(JsonNode targetingDefinition, String lineItemId) { + return TargetingDefinition.of(parseNode(targetingDefinition, lineItemId)); + } + + /** + * Accepts OpenRTB2 request and particular Imp object to evaluate Line Item targeting + * definition against and returns whether it is matched or not. + */ + public boolean matchesTargeting(AuctionContext auctionContext, Imp imp, TargetingDefinition targetingDefinition) { + final RequestContext requestContext = new RequestContext( + auctionContext.getBidRequest(), imp, auctionContext.getTxnLog(), mapper); + + return targetingDefinition.getRootExpression().matches(requestContext); + } + + private Expression parseNode(JsonNode node, String lineItemId) { + final Map.Entry field = validateIsSingleElementObject(node); + final String fieldName = field.getKey(); + + if (BooleanOperator.isBooleanOperator(fieldName)) { + return parseBooleanOperator(fieldName, field.getValue(), lineItemId); + } else if (TargetingCategory.isTargetingCategory(fieldName)) { + return parseTargetingCategory(fieldName, field.getValue(), lineItemId); + } else { + throw new TargetingSyntaxException( + String.format("Expected either boolean operator or targeting category, got %s", fieldName)); + } + } + + private Expression parseBooleanOperator(String fieldName, JsonNode value, String lineItemId) { + final BooleanOperator operator = BooleanOperator.fromString(fieldName); + switch (operator) { + case AND: + return new And(parseArray(value, node -> parseNode(node, lineItemId))); + case OR: + return new Or(parseArray(value, node -> parseNode(node, lineItemId))); + case NOT: + return new Not(parseNode(value, lineItemId)); + default: + throw new IllegalStateException(String.format("Unexpected boolean operator %s", operator)); + } + } + + private Expression parseTargetingCategory(String fieldName, JsonNode value, String lineItemId) { + final TargetingCategory category = TargetingCategory.fromString(fieldName); + switch (category.type()) { + case size: + return new IntersectsSizes(category, + parseArrayFunction(value, MatchingFunction.INTERSECTS, this::parseSize)); + case mediaType: + case userSegment: + return new IntersectsStrings(category, + parseArrayFunction(value, MatchingFunction.INTERSECTS, TargetingService::parseString)); + case domain: + return new DomainMetricAwareExpression(parseStringFunction(category, value), lineItemId); + case referrer: + case appBundle: + case adslot: + return parseStringFunction(category, value); + case pagePosition: + case dow: + case hour: + return new InIntegers(category, + parseArrayFunction(value, MatchingFunction.IN, TargetingService::parseInteger)); + case deviceGeoExt: + case deviceExt: + return new InStrings(category, + parseArrayFunction(value, MatchingFunction.IN, TargetingService::parseString)); + case location: + return new Within(category, parseSingleObjectFunction(value, MatchingFunction.WITHIN, + this::parseGeoRegion)); + case bidderParam: + case userFirstPartyData: + case siteFirstPartyData: + return parseTypedFunction(category, value); + default: + throw new IllegalStateException(String.format("Unexpected targeting category type %s", category)); + } + } + + private static List parseArrayFunction(JsonNode value, MatchingFunction function, + Function mapper) { + + return parseArray(validateIsFunction(value, function), mapper); + } + + private static T parseSingleObjectFunction( + JsonNode value, MatchingFunction function, Function mapper) { + + return mapper.apply(validateIsFunction(value, function)); + } + + private static Expression parseStringFunction(TargetingCategory category, JsonNode value) { + final Map.Entry field = validateIsSingleElementObject(value); + final MatchingFunction function = + validateCompatibleFunction(field, MatchingFunction.MATCHES, MatchingFunction.IN); + + switch (function) { + case MATCHES: + return new Matches(category, parseString(field.getValue())); + case IN: + return createInStringsFunction(category, field.getValue()); + default: + throw new IllegalStateException(String.format("Unexpected string function %s", function.value())); + } + } + + private static Expression parseTypedFunction(TargetingCategory category, JsonNode value) { + final Map.Entry field = validateIsSingleElementObject(value); + final MatchingFunction function = validateCompatibleFunction(field, + MatchingFunction.MATCHES, MatchingFunction.IN, MatchingFunction.INTERSECTS); + + final JsonNode functionValue = field.getValue(); + switch (function) { + case MATCHES: + return new Matches(category, parseString(functionValue)); + case IN: + return parseTypedInFunction(category, functionValue); + case INTERSECTS: + return parseTypedIntersectsFunction(category, functionValue); + default: + throw new IllegalStateException(String.format("Unexpected typed function %s", function.value())); + } + } + + private Size parseSize(JsonNode node) { + validateIsObject(node); + + final Size size; + try { + size = mapper.mapper().treeToValue(node, Size.class); + } catch (JsonProcessingException e) { + throw new TargetingSyntaxException( + String.format("Exception occurred while parsing size: %s", e.getMessage()), e); + } + + if (size.getH() == null || size.getW() == null) { + throw new TargetingSyntaxException("Height and width in size definition could not be null or missing"); + } + + return size; + } + + private static String parseString(JsonNode node) { + validateIsString(node); + + final String value = node.textValue(); + if (StringUtils.isEmpty(value)) { + throw new TargetingSyntaxException("String value could not be empty"); + } + return value; + } + + private static Integer parseInteger(JsonNode node) { + validateIsInteger(node); + + return node.intValue(); + } + + private GeoRegion parseGeoRegion(JsonNode node) { + validateIsObject(node); + + final GeoRegion region; + try { + region = mapper.mapper().treeToValue(node, GeoRegion.class); + } catch (JsonProcessingException e) { + throw new TargetingSyntaxException( + String.format("Exception occurred while parsing geo region: %s", e.getMessage()), e); + } + + if (region.getLat() == null || region.getLon() == null || region.getRadiusMiles() == null) { + throw new TargetingSyntaxException( + "Lat, lon and radiusMiles in geo region definition could not be null or missing"); + } + + return region; + } + + private static List parseArray(JsonNode node, Function mapper) { + validateIsArray(node); + + return StreamUtil.asStream(node.spliterator()).map(mapper).collect(Collectors.toList()); + } + + private static Expression parseTypedInFunction(TargetingCategory category, JsonNode value) { + return parseTypedArrayFunction(category, value, TargetingService::createInIntegersFunction, + TargetingService::createInStringsFunction); + } + + private static Expression parseTypedIntersectsFunction(TargetingCategory category, JsonNode value) { + return parseTypedArrayFunction(category, value, TargetingService::createIntersectsIntegersFunction, + TargetingService::createIntersectsStringsFunction); + } + + private static Expression parseTypedArrayFunction( + TargetingCategory category, JsonNode value, + BiFunction integerCreator, + BiFunction stringCreator) { + + validateIsArray(value); + + final Iterator iterator = value.iterator(); + + final JsonNodeType dataType = iterator.hasNext() ? iterator.next().getNodeType() : JsonNodeType.STRING; + switch (dataType) { + case NUMBER: + return integerCreator.apply(category, value); + case STRING: + return stringCreator.apply(category, value); + default: + throw new TargetingSyntaxException(String.format("Expected integer or string, got %s", dataType)); + } + } + + private static Expression createInIntegersFunction(TargetingCategory category, JsonNode value) { + return new InIntegers(category, parseArray(value, TargetingService::parseInteger)); + } + + private static InStrings createInStringsFunction(TargetingCategory category, JsonNode value) { + return new InStrings(category, parseArray(value, TargetingService::parseString)); + } + + private static Expression createIntersectsStringsFunction(TargetingCategory category, JsonNode value) { + return new IntersectsStrings(category, parseArray(value, TargetingService::parseString)); + } + + private static Expression createIntersectsIntegersFunction(TargetingCategory category, JsonNode value) { + return new IntersectsIntegers(category, parseArray(value, TargetingService::parseInteger)); + } + + private static void validateIsObject(JsonNode value) { + if (!value.isObject()) { + throw new TargetingSyntaxException(String.format("Expected object, got %s", value.getNodeType())); + } + } + + private static Map.Entry validateIsSingleElementObject(JsonNode value) { + validateIsObject(value); + + if (value.size() != 1) { + throw new TargetingSyntaxException( + String.format("Expected only one element in the object, got %d", value.size())); + } + + return value.fields().next(); + } + + private static void validateIsArray(JsonNode value) { + if (!value.isArray()) { + throw new TargetingSyntaxException(String.format("Expected array, got %s", value.getNodeType())); + } + } + + private static void validateIsString(JsonNode value) { + if (!value.isTextual()) { + throw new TargetingSyntaxException(String.format("Expected string, got %s", value.getNodeType())); + } + } + + private static void validateIsInteger(JsonNode value) { + if (!value.isInt()) { + throw new TargetingSyntaxException(String.format("Expected integer, got %s", value.getNodeType())); + } + } + + private static JsonNode validateIsFunction(JsonNode value, MatchingFunction function) { + final Map.Entry field = validateIsSingleElementObject(value); + final String fieldName = field.getKey(); + + if (!MatchingFunction.isMatchingFunction(fieldName)) { + throw new TargetingSyntaxException(String.format("Expected matching function, got %s", fieldName)); + } else if (MatchingFunction.fromString(fieldName) != function) { + throw new TargetingSyntaxException( + String.format("Expected %s matching function, got %s", function.value(), fieldName)); + } + + return field.getValue(); + } + + private static MatchingFunction validateCompatibleFunction(Map.Entry field, + MatchingFunction... compatibleFunctions) { + final String fieldName = field.getKey(); + + if (!MatchingFunction.isMatchingFunction(fieldName)) { + throw new TargetingSyntaxException(String.format("Expected matching function, got %s", fieldName)); + } + + final MatchingFunction function = MatchingFunction.fromString(fieldName); + if (!Arrays.asList(compatibleFunctions).contains(function)) { + throw new TargetingSyntaxException(String.format("Expected one of %s matching functions, got %s", + Arrays.stream(compatibleFunctions).map(MatchingFunction::value).collect(Collectors.joining(", ")), + fieldName)); + } + return function; + } +} diff --git a/src/main/java/org/prebid/server/deals/UserService.java b/src/main/java/org/prebid/server/deals/UserService.java new file mode 100644 index 00000000000..deda5564547 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/UserService.java @@ -0,0 +1,294 @@ +package org.prebid.server.deals; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.cache.model.DebugHttpCall; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.cookie.model.UidWithExpiry; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.model.User; +import org.prebid.server.deals.model.UserDetails; +import org.prebid.server.deals.model.UserDetailsProperties; +import org.prebid.server.deals.model.UserDetailsRequest; +import org.prebid.server.deals.model.UserDetailsResponse; +import org.prebid.server.deals.model.UserId; +import org.prebid.server.deals.model.UserIdRule; +import org.prebid.server.deals.model.WinEventNotification; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.Timeout; +import org.prebid.server.handler.NotificationEventHandler; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Works with user related information. + */ +public class UserService { + + private static final Logger logger = LoggerFactory.getLogger(UserService.class); + private static final String USER_SERVICE = "userservice"; + + private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .toFormatter(); + + private final LineItemService lineItemService; + private final HttpClient httpClient; + private final Clock clock; + private final Metrics metrics; + private final JacksonMapper mapper; + + private final String userDetailsUrl; + private final String winEventUrl; + private final long timeout; + private final List userIdRules; + private final String dataCenterRegion; + + public UserService(UserDetailsProperties userDetailsProperties, + String dataCenterRegion, + LineItemService lineItemService, + HttpClient httpClient, + Clock clock, + Metrics metrics, + JacksonMapper mapper) { + + this.lineItemService = Objects.requireNonNull(lineItemService); + this.httpClient = Objects.requireNonNull(httpClient); + this.clock = Objects.requireNonNull(clock); + this.metrics = Objects.requireNonNull(metrics); + + this.userDetailsUrl = Objects.requireNonNull( + HttpUtil.validateUrl(userDetailsProperties.getUserDetailsEndpoint())); + this.winEventUrl = Objects.requireNonNull(HttpUtil.validateUrl(userDetailsProperties.getWinEventEndpoint())); + this.timeout = userDetailsProperties.getTimeout(); + this.userIdRules = Objects.requireNonNull(userDetailsProperties.getUserIds()); + this.dataCenterRegion = Objects.requireNonNull(dataCenterRegion); + this.mapper = Objects.requireNonNull(mapper); + } + + /** + * Fetches {@link UserDetails} from the User Data Store. + */ + public Future getUserDetails(AuctionContext context, Timeout timeout) { + final Map uidsMap = context.getUidsCookie().getCookieUids().getUids(); + if (CollectionUtils.isEmpty(uidsMap.values())) { + metrics.updateUserDetailsRequestPreparationFailed(); + context.getDebugHttpCalls().put(USER_SERVICE, Collections.singletonList(DebugHttpCall.empty())); + return Future.succeededFuture(UserDetails.empty()); + } + + final List userIds = getUserIds(uidsMap); + if (CollectionUtils.isEmpty(userIds)) { + metrics.updateUserDetailsRequestPreparationFailed(); + context.getDebugHttpCalls().put(USER_SERVICE, Collections.singletonList(DebugHttpCall.empty())); + return Future.succeededFuture(UserDetails.empty()); + } + + final UserDetailsRequest userDetailsRequest = UserDetailsRequest.of( + UTC_MILLIS_FORMATTER.format(ZonedDateTime.now(clock)), userIds); + final String body = mapper.encode(userDetailsRequest); + + final long requestTimeout = Math.min(this.timeout, timeout.remaining()); + + final long startTime = clock.millis(); + return httpClient.post(userDetailsUrl, body, requestTimeout) + .map(httpClientResponse -> toUserServiceResult(httpClientResponse, context, + userDetailsUrl, body, startTime)) + .recover(throwable -> failGetDetailsResponse(throwable, context, userDetailsUrl, body, startTime)); + } + + /** + * Retrieves the UID from UIDs Map by each {@link UserIdRule#getLocation()} and if UID is present - creates a + * {@link UserId} object that contains {@link UserIdRule#getType()} and UID and adds it to UserId list. + */ + private List getUserIds(Map bidderToUid) { + final List userIds = new ArrayList<>(); + for (UserIdRule rule : userIdRules) { + final UidWithExpiry uid = bidderToUid.get(rule.getLocation()); + if (uid != null) { + userIds.add(UserId.of(rule.getType(), uid.getUid())); + } + } + return userIds; + } + + /** + * Transforms response from User Data Store into {@link Future} of {@link UserDetails}. + *

+ * Throws {@link PreBidException} if an error occurs during response body deserialization. + */ + private UserDetails toUserServiceResult(HttpClientResponse clientResponse, AuctionContext context, + String requestUrl, String requestBody, long startTime) { + final int responseStatusCode = clientResponse.getStatusCode(); + verifyStatusCode(responseStatusCode); + + final String responseBody = clientResponse.getBody(); + final User user; + final int responseTime = responseTime(startTime); + try { + user = parseUserDetailsResponse(responseBody); + } finally { + context.getDebugHttpCalls().put(USER_SERVICE, Collections.singletonList( + DebugHttpCall.builder() + .requestUri(requestUrl) + .requestBody(requestBody) + .responseStatus(responseStatusCode) + .responseBody(responseBody) + .responseTimeMillis(responseTime) + .build())); + } + metrics.updateRequestTimeMetric(MetricName.user_details_request_time, responseTime); + metrics.updateUserDetailsRequestMetric(true); + return UserDetails.of(user.getData(), user.getExt().getFcapIds()); + } + + private User parseUserDetailsResponse(String responseBody) { + final UserDetailsResponse userDetailsResponse; + try { + userDetailsResponse = mapper.decodeValue(responseBody, UserDetailsResponse.class); + } catch (DecodeException e) { + throw new PreBidException(String.format("Cannot parse response: %s", responseBody), e); + } + + final User user = userDetailsResponse.getUser(); + if (user == null) { + throw new PreBidException(String.format("Field 'user' is missing in response: %s", responseBody)); + } + + if (user.getData() == null) { + throw new PreBidException(String.format("Field 'user.data' is missing in response: %s", responseBody)); + } + + if (user.getExt() == null) { + throw new PreBidException(String.format("Field 'user.ext' is missing in response: %s", responseBody)); + } + return user; + } + + /** + * Throw {@link PreBidException} if response status is not 200. + */ + private static void verifyStatusCode(int statusCode) { + if (statusCode != 200) { + throw new PreBidException(String.format("Bad response status code: %s", statusCode)); + } + } + + /** + * Handles errors that occurred during getUserDetails HTTP request or response processing. + */ + private Future failGetDetailsResponse(Throwable exception, AuctionContext context, String requestUrl, + String requestBody, long startTime) { + final int responseTime = responseTime(startTime); + context.getDebugHttpCalls().putIfAbsent(USER_SERVICE, + Collections.singletonList( + DebugHttpCall.builder() + .requestUri(requestUrl) + .requestBody(requestBody) + .responseTimeMillis(responseTime) + .build())); + metrics.updateUserDetailsRequestMetric(false); + metrics.updateRequestTimeMetric(MetricName.user_details_request_time, responseTime); + logger.warn("Error occurred while fetching user details", exception); + return Future.failedFuture(exception); + } + + /** + * Calculates execution time since the given start time. + */ + private int responseTime(long startTime) { + return Math.toIntExact(clock.millis() - startTime); + } + + /** + * Accepts lineItemId and bidId from the {@link NotificationEventHandler}, + * joins event data with corresponding Line Item metadata (provided by LineItemService) + * and passes this information to the User Data Store to facilitate frequency capping. + */ + public void processWinEvent(String lineItemId, String bidId, UidsCookie uids) { + final LineItem lineItem = lineItemService.getLineItemById(lineItemId); + final List userIds = getUserIds(uids.getCookieUids().getUids()); + + if (!hasRequiredData(lineItem, userIds, lineItemId)) { + metrics.updateWinRequestPreparationFailed(); + return; + } + + final String body = mapper.encode(WinEventNotification.builder() + .bidderCode(lineItem.getSource()) + .bidId(bidId) + .lineItemId(lineItemId) + .region(dataCenterRegion) + .userIds(userIds) + .winEventDateTime(ZonedDateTime.now(clock)) + .lineUpdatedDateTime(lineItem.getUpdatedTimeStamp()) + .frequencyCaps(lineItem.getFrequencyCaps()) + .build()); + + metrics.updateWinNotificationMetric(); + final long startTime = clock.millis(); + httpClient.post(winEventUrl, body, timeout) + .setHandler(result -> handleWinResponse(result, startTime)); + } + + /** + * Verify that all necessary data is present and log error if something is missing. + */ + private static boolean hasRequiredData(LineItem lineItem, List userIds, String lineItemId) { + if (lineItem == null) { + logger.error("Meta Data for Line Item Id {0} does not exist", lineItemId); + return false; + } + + if (CollectionUtils.isEmpty(userIds)) { + logger.error("User Ids cannot be empty"); + return false; + } + return true; + } + + /** + * Checks response from User Data Store. + */ + private void handleWinResponse(AsyncResult asyncResult, long startTime) { + metrics.updateWinRequestTime(responseTime(startTime)); + if (asyncResult.succeeded()) { + try { + verifyStatusCode(asyncResult.result().getStatusCode()); + metrics.updateWinEventRequestMetric(true); + } catch (PreBidException e) { + metrics.updateWinEventRequestMetric(false); + logWinEventError(e); + } + } else { + metrics.updateWinEventRequestMetric(false); + logWinEventError(asyncResult.cause()); + } + } + + /** + * Logs errors that occurred during processWinEvent HTTP request or bad response code. + */ + private static void logWinEventError(Throwable exception) { + logger.warn("Error occurred while pushing win event notification", exception); + } +} diff --git a/src/main/java/org/prebid/server/deals/deviceinfo/DeviceInfoService.java b/src/main/java/org/prebid/server/deals/deviceinfo/DeviceInfoService.java new file mode 100644 index 00000000000..1e680ebd51f --- /dev/null +++ b/src/main/java/org/prebid/server/deals/deviceinfo/DeviceInfoService.java @@ -0,0 +1,16 @@ +package org.prebid.server.deals.deviceinfo; + +import io.vertx.core.Future; +import org.prebid.server.deals.model.DeviceInfo; + +/** + * Processes device related information. + */ +@FunctionalInterface +public interface DeviceInfoService { + + /** + * Provides information about device based on User-Agent string and other available attributes. + */ + Future getDeviceInfo(String ua); +} diff --git a/src/main/java/org/prebid/server/deals/events/AdminEventProcessor.java b/src/main/java/org/prebid/server/deals/events/AdminEventProcessor.java new file mode 100644 index 00000000000..dbf9133a902 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/events/AdminEventProcessor.java @@ -0,0 +1,8 @@ +package org.prebid.server.deals.events; + +import org.prebid.server.deals.model.AdminCentralResponse; + +public interface AdminEventProcessor { + + void processAdminCentralEvent(AdminCentralResponse adminCentralResponse); +} diff --git a/src/main/java/org/prebid/server/deals/events/AdminEventService.java b/src/main/java/org/prebid/server/deals/events/AdminEventService.java new file mode 100644 index 00000000000..f8f6130c6b5 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/events/AdminEventService.java @@ -0,0 +1,30 @@ +package org.prebid.server.deals.events; + +import io.vertx.core.eventbus.DeliveryOptions; +import io.vertx.core.eventbus.EventBus; +import org.prebid.server.deals.model.AdminCentralResponse; +import org.prebid.server.vertx.LocalMessageCodec; + +import java.util.Objects; + +public class AdminEventService { + + private static final String ADDRESS_ADMIN_CENTRAL_COMMAND = "event.admin-central"; + + private static final DeliveryOptions DELIVERY_OPTIONS = + new DeliveryOptions() + .setCodecName(LocalMessageCodec.codecName()); + + private final EventBus eventBus; + + public AdminEventService(EventBus eventBus) { + this.eventBus = Objects.requireNonNull(eventBus); + } + + /** + * Publishes admin central event. + */ + public void publishAdminCentralEvent(AdminCentralResponse adminCentralResponse) { + eventBus.publish(ADDRESS_ADMIN_CENTRAL_COMMAND, adminCentralResponse, DELIVERY_OPTIONS); + } +} diff --git a/src/main/java/org/prebid/server/deals/events/ApplicationEventProcessor.java b/src/main/java/org/prebid/server/deals/events/ApplicationEventProcessor.java new file mode 100644 index 00000000000..ed1595d1eda --- /dev/null +++ b/src/main/java/org/prebid/server/deals/events/ApplicationEventProcessor.java @@ -0,0 +1,17 @@ +package org.prebid.server.deals.events; + +import org.prebid.server.auction.model.AuctionContext; + +/** + * Interface for the components able to consume application events. + * + * @see ApplicationEventService + */ +public interface ApplicationEventProcessor { + + void processAuctionEvent(AuctionContext auctionContext); + + void processLineItemWinEvent(String lineItemId); + + void processDeliveryProgressUpdateEvent(); +} diff --git a/src/main/java/org/prebid/server/deals/events/ApplicationEventService.java b/src/main/java/org/prebid/server/deals/events/ApplicationEventService.java new file mode 100644 index 00000000000..78dec0ae622 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/events/ApplicationEventService.java @@ -0,0 +1,57 @@ +package org.prebid.server.deals.events; + +import io.vertx.core.eventbus.DeliveryOptions; +import io.vertx.core.eventbus.EventBus; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.vertx.LocalMessageCodec; + +import java.util.Objects; + +/** + * Main purpose of this service is decoupling of application events delivery from their generators to consumers. + *

+ * This service is essentially a facade for Vert.x {@link EventBus}, it encapsulates addressing and consumers + * configuration concerns and provides type-safe API for publishing different application events which are consumed + * by all {@link ApplicationEventProcessor}s registered in the application. + *

+ * Implementation notes: + * Communication through {@link EventBus} is performed only locally, that's why no serialization/deserialization + * happens for objects passed over the bus and hence no implied performance penalty (see {@link LocalMessageCodec}). + */ +public class ApplicationEventService { + + private static final String ADDRESS_EVENT_OPENRTB2_AUCTION = "event.openrtb2-auction"; + private static final String ADDRESS_EVENT_LINE_ITEM_WIN = "event.line-item-win"; + private static final String ADDRESS_EVENT_DELIVERY_UPDATE = "event.delivery-update"; + + private static final DeliveryOptions DELIVERY_OPTIONS = + new DeliveryOptions() + .setCodecName(LocalMessageCodec.codecName()); + + private final EventBus eventBus; + + public ApplicationEventService(EventBus eventBus) { + this.eventBus = Objects.requireNonNull(eventBus); + } + + /** + * Publishes auction event. + */ + public void publishAuctionEvent(AuctionContext auctionContext) { + eventBus.publish(ADDRESS_EVENT_OPENRTB2_AUCTION, auctionContext, DELIVERY_OPTIONS); + } + + /** + * Publishes line item win event. + */ + public void publishLineItemWinEvent(String lineItemId) { + eventBus.publish(ADDRESS_EVENT_LINE_ITEM_WIN, lineItemId); + } + + /** + * Publishes delivery update event. + */ + public void publishDeliveryUpdateEvent() { + eventBus.publish(ADDRESS_EVENT_DELIVERY_UPDATE, null); + } +} diff --git a/src/main/java/org/prebid/server/deals/events/EventServiceInitializer.java b/src/main/java/org/prebid/server/deals/events/EventServiceInitializer.java new file mode 100644 index 00000000000..93802bbadd0 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/events/EventServiceInitializer.java @@ -0,0 +1,53 @@ +package org.prebid.server.deals.events; + +import io.vertx.core.eventbus.EventBus; +import io.vertx.core.eventbus.Message; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.deals.model.AdminCentralResponse; +import org.prebid.server.vertx.Initializable; + +import java.util.List; +import java.util.Objects; + +public class EventServiceInitializer implements Initializable { + + private static final String ADDRESS_EVENT_OPENRTB2_AUCTION = "event.openrtb2-auction"; + private static final String ADDRESS_EVENT_LINE_ITEM_WIN = "event.line-item-win"; + private static final String ADDRESS_EVENT_DELIVERY_UPDATE = "event.delivery-update"; + private static final String ADDRESS_ADMIN_CENTRAL_COMMAND = "event.admin-central"; + + private final List applicationEventProcessors; + private final List adminEventProcessors; + private final EventBus eventBus; + + public EventServiceInitializer(List applicationEventProcessors, + List adminEventProcessors, + EventBus eventBus) { + this.applicationEventProcessors = Objects.requireNonNull(applicationEventProcessors); + this.adminEventProcessors = Objects.requireNonNull(adminEventProcessors); + this.eventBus = Objects.requireNonNull(eventBus); + } + + @Override + public void initialize() { + eventBus.localConsumer( + ADDRESS_EVENT_OPENRTB2_AUCTION, + (Message message) -> applicationEventProcessors.forEach( + recorder -> recorder.processAuctionEvent(message.body()))); + + eventBus.localConsumer( + ADDRESS_EVENT_LINE_ITEM_WIN, + (Message message) -> applicationEventProcessors.forEach( + recorder -> recorder.processLineItemWinEvent(message.body()))); + + eventBus.localConsumer( + ADDRESS_EVENT_DELIVERY_UPDATE, + (Message message) -> applicationEventProcessors.forEach( + ApplicationEventProcessor::processDeliveryProgressUpdateEvent)); + + eventBus.localConsumer( + ADDRESS_ADMIN_CENTRAL_COMMAND, + (Message message) -> adminEventProcessors.forEach( + recorder -> recorder.processAdminCentralEvent(message.body()))); + } +} diff --git a/src/main/java/org/prebid/server/deals/lineitem/DeliveryPlan.java b/src/main/java/org/prebid/server/deals/lineitem/DeliveryPlan.java new file mode 100644 index 00000000000..a5dd8aa7274 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/lineitem/DeliveryPlan.java @@ -0,0 +1,188 @@ +package org.prebid.server.deals.lineitem; + +import org.apache.commons.collections4.SetUtils; +import org.prebid.server.deals.proto.DeliverySchedule; +import org.prebid.server.deals.proto.Token; +import org.prebid.server.exception.PreBidException; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class DeliveryPlan { + + private final DeliverySchedule deliverySchedule; + + private final Set deliveryTokens; + + private DeliveryPlan(DeliverySchedule deliverySchedule) { + this(Objects.requireNonNull(deliverySchedule), toDeliveryTokens(deliverySchedule.getTokens())); + } + + private DeliveryPlan(DeliverySchedule deliverySchedule, Set deliveryTokens) { + this.deliverySchedule = Objects.requireNonNull(deliverySchedule); + this.deliveryTokens = Objects.requireNonNull(deliveryTokens); + } + + public static DeliveryPlan of(DeliverySchedule deliverySchedule) { + return new DeliveryPlan(deliverySchedule); + } + + /** + * Returns number of not spent tokens in {@link DeliveryPlan}. + */ + public int getUnspentTokens() { + return deliveryTokens.stream().mapToInt(DeliveryToken::getUnspent).sum(); + } + + /** + * Returns number of spent tokens in {@link DeliveryPlan}. + */ + public long getSpentTokens() { + return deliveryTokens.stream().map(DeliveryToken::getSpent).mapToLong(LongAdder::sum).sum(); + } + + public long getTotalTokens() { + return deliveryTokens.stream().mapToLong(DeliveryToken::getTotal).sum(); + } + + /** + * Returns lowest (which means highest priority) token's class value with unspent tokens. + */ + public int getHighestUnspentTokensClass() { + return deliveryTokens.stream() + .filter(token -> token.getUnspent() > 0) + .findFirst() + .map(DeliveryToken::getPriorityClass) + .orElseThrow(() -> new PreBidException(String.format( + "Class with not spent tokens was not found for plan with id %s", + deliverySchedule.getPlanId()))); + } + + /** + * Increments tokens in {@link DeliveryToken} with highest priority within {@link DeliveryPlan} + * + * @return class of the token incremented + */ + public Integer incSpentToken() { + final DeliveryToken unspentToken = deliveryTokens.stream() + .filter(token -> token.getUnspent() > 0) + .findFirst() + .orElse(null); + if (unspentToken != null) { + unspentToken.inc(); + return unspentToken.getPriorityClass(); + } + + return null; + } + + /** + * Merges tokens from expired {@link DeliveryPlan} to the next one. + */ + public DeliveryPlan mergeWithNextDeliverySchedule(DeliverySchedule nextDeliverySchedule, boolean sumTotal) { + + final Map nextTokensByClass = nextDeliverySchedule.getTokens().stream() + .collect(Collectors.toMap(Token::getPriorityClass, Function.identity())); + + final Set mergedTokens = new TreeSet<>(); + + for (final DeliveryToken expiredToken : deliveryTokens) { + final Integer priorityClass = expiredToken.getPriorityClass(); + final Token nextToken = nextTokensByClass.get(priorityClass); + + mergedTokens.add(expiredToken.mergeWithToken(nextToken, sumTotal)); + + nextTokensByClass.remove(priorityClass); + } + + // add remaining (not merged) tokens + nextTokensByClass.values().stream().map(DeliveryToken::of).forEach(mergedTokens::add); + + return new DeliveryPlan(nextDeliverySchedule, mergedTokens); + } + + public DeliveryPlan mergeWithNextDeliveryPlan(DeliveryPlan anotherPlan) { + return mergeWithNextDeliverySchedule(anotherPlan.deliverySchedule, false); + } + + public DeliveryPlan withoutSpentTokens() { + return new DeliveryPlan(deliverySchedule, deliveryTokens.stream() + .map(DeliveryToken::of) + .collect(Collectors.toSet())); + } + + public void incTokenWithPriority(Integer tokenPriority) { + deliveryTokens.stream() + .filter(token -> Objects.equals(token.getPriorityClass(), tokenPriority)) + .findAny() + .ifPresent(DeliveryToken::inc); + } + + /** + * Calculates readyAt from expirationDate and number of unspent tokens. + */ + public ZonedDateTime calculateReadyAt() { + final ZonedDateTime planStartTime = deliverySchedule.getStartTimeStamp(); + final long spentTokens = getSpentTokens(); + final long unspentTokens = getUnspentTokens(); + final long timeShift = spentTokens * ((deliverySchedule.getEndTimeStamp().toInstant().toEpochMilli() + - planStartTime.toInstant().toEpochMilli()) / getTotalTokens()); + return unspentTokens > 0 + ? ZonedDateTime.ofInstant(planStartTime.toInstant().plusMillis(timeShift), ZoneOffset.UTC) + : null; + } + + public Long getDeliveryRateInMilliseconds() { + final int unspentTokens = getUnspentTokens(); + return unspentTokens > 0 + ? (deliverySchedule.getEndTimeStamp().toInstant().toEpochMilli() + - deliverySchedule.getStartTimeStamp().toInstant().toEpochMilli()) + / getTotalTokens() + : null; + } + + public boolean isUpdated(DeliverySchedule deliverySchedule) { + final ZonedDateTime currentPlanUpdatedDate = this.deliverySchedule.getUpdatedTimeStamp(); + final ZonedDateTime newPlanUpdatedDate = deliverySchedule.getUpdatedTimeStamp(); + return !(currentPlanUpdatedDate == null && newPlanUpdatedDate == null) + && (currentPlanUpdatedDate == null || newPlanUpdatedDate == null + || currentPlanUpdatedDate.isBefore(newPlanUpdatedDate)); + } + + public String getPlanId() { + return deliverySchedule.getPlanId(); + } + + public ZonedDateTime getStartTimeStamp() { + return deliverySchedule.getStartTimeStamp(); + } + + public ZonedDateTime getEndTimeStamp() { + return deliverySchedule.getEndTimeStamp(); + } + + public ZonedDateTime getUpdatedTimeStamp() { + return deliverySchedule.getUpdatedTimeStamp(); + } + + public Set getDeliveryTokens() { + return deliveryTokens; + } + + public DeliverySchedule getDeliverySchedule() { + return deliverySchedule; + } + + private static Set toDeliveryTokens(Set tokens) { + return SetUtils.emptyIfNull(tokens).stream() + .map(DeliveryToken::of) + .collect(Collectors.toCollection(TreeSet::new)); + } +} diff --git a/src/main/java/org/prebid/server/deals/lineitem/DeliveryProgress.java b/src/main/java/org/prebid/server/deals/lineitem/DeliveryProgress.java new file mode 100644 index 00000000000..f14dbef4a07 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/lineitem/DeliveryProgress.java @@ -0,0 +1,349 @@ +package org.prebid.server.deals.lineitem; + +import org.prebid.server.deals.LineItemService; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.deals.proto.report.Event; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class DeliveryProgress { + + private static final String WIN_EVENT_TYPE = "win"; + + private final Map lineItemStatuses; + private final Map requestsPerAccount; + private final Map> lineItemIdToLost; + private final LongAdder requests; + private ZonedDateTime startTimeStamp; + private ZonedDateTime endTimeStamp; + private final LineItemService lineItemService; + + private DeliveryProgress(ZonedDateTime startTimeStamp, LineItemService lineItemService) { + this.startTimeStamp = Objects.requireNonNull(startTimeStamp); + this.lineItemStatuses = new ConcurrentHashMap<>(); + this.requests = new LongAdder(); + this.requestsPerAccount = new ConcurrentHashMap<>(); + this.lineItemIdToLost = new ConcurrentHashMap<>(); + this.lineItemService = Objects.requireNonNull(lineItemService); + } + + public static DeliveryProgress of(ZonedDateTime startTimeStamp, LineItemService lineItemService) { + return new DeliveryProgress(startTimeStamp, lineItemService); + } + + public DeliveryProgress copyWithOriginalPlans() { + final DeliveryProgress progress = DeliveryProgress.of(this.getStartTimeStamp(), + this.lineItemService); + + for (final LineItemStatus originalStatus : this.lineItemStatuses.values()) { + progress.lineItemStatuses.put(originalStatus.getLineItemId(), createStatusWithPlans(originalStatus)); + } + + progress.mergeFrom(this); + + return progress; + } + + private LineItemStatus createStatusWithPlans(LineItemStatus originalStatus) { + final LineItemStatus status = createLineItemStatus(originalStatus.getLineItemId()); + status.getDeliveryPlans().addAll(originalStatus.getDeliveryPlans()); + return status; + } + + /** + * Updates delivery progress from {@link TxnLog}. + */ + public void recordTransactionLog(TxnLog txnLog, Map planIdToTokenPriority, String accountId) { + accountRequests(accountId).increment(); + requests.increment(); + + txnLog.lineItemSentToClientAsTopMatch() + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incSentToClientAsTopMatch)); + txnLog.lineItemsSentToClient() + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incSentToClient)); + txnLog.lineItemsMatchedDomainTargeting() + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incDomainMatched)); + txnLog.lineItemsMatchedWholeTargeting() + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incTargetMatched)); + txnLog.lineItemsMatchedTargetingFcapped() + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incTargetMatchedButFcapped)); + txnLog.lineItemsMatchedTargetingFcapLookupFailed() + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incTargetMatchedButFcapLookupFailed)); + txnLog.lineItemsPacingDeferred() + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incPacingDeferred)); + txnLog.lineItemsSentToBidder().values().forEach(idList -> idList + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incSentToBidder))); + txnLog.lineItemsSentToBidderAsTopMatch().values().forEach(bidderList -> bidderList + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incSentToBidderAsTopMatch))); + txnLog.lineItemsReceivedFromBidder().values().forEach(idList -> idList + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incReceivedFromBidder))); + txnLog.lineItemsResponseInvalidated() + .forEach(lineItemId -> increment(lineItemId, LineItemStatus::incReceivedFromBidderInvalidated)); + + txnLog.lineItemSentToClientAsTopMatch() + .forEach(lineItemId -> incToken(lineItemId, planIdToTokenPriority)); + + txnLog.lostMatchingToLineItems().forEach((lineItemId, lostToLineItemsIds) -> + updateLostToEachLineItem(lineItemId, lostToLineItemsIds, lineItemIdToLost)); + txnLog.lostAuctionToLineItems().forEach((lineItemId, lostToLineItemsIds) -> + updateLostToEachLineItem(lineItemId, lostToLineItemsIds, lineItemIdToLost)); + } + + /** + * Increments {@link LineItemStatus} win type {@link Event} counter. Creates new {@link LineItemStatus} if not + * exists. + */ + public void recordWinEvent(String lineItemId) { + final LineItemStatus lineItemStatus = lineItemStatuses.computeIfAbsent(lineItemId, this::createLineItemStatus); + final Event winEvent = lineItemStatus.getEvents().stream() + .filter(event -> event.getType().equals(WIN_EVENT_TYPE)) + .findAny() + .orElseGet(() -> Event.of(WIN_EVENT_TYPE, new LongAdder())); + + winEvent.getCount().increment(); + lineItemStatus.getEvents().add(winEvent); + } + + private LineItemStatus createLineItemStatus(String lineItemId) { + final LineItem lineItem = lineItemService.getLineItemById(lineItemId); + return lineItem != null + ? LineItemStatus.of(lineItem) + : LineItemStatus.of(lineItemId); + } + + /** + * Updates delivery progress from another {@link DeliveryProgress}. + */ + public void mergeFrom(DeliveryProgress another) { + requests.add(another.requests.sum()); + + another.requestsPerAccount.forEach((accountId, requestsCount) -> + mergeRequestsCount(accountId, requestsCount, requestsPerAccount)); + + another.lineItemStatuses.forEach((lineItemId, lineItemStatus) -> + lineItemStatuses.computeIfAbsent(lineItemId, this::createLineItemStatus).merge(lineItemStatus)); + + another.lineItemIdToLost.forEach((lineItemId, currentLineItemLost) -> + mergeCurrentLineItemLostReportToOverall(lineItemId, currentLineItemLost, lineItemIdToLost)); + } + + public void upsertPlanReferenceFromLineItem(LineItem lineItem) { + final String lineItemId = lineItem.getLineItemId(); + final LineItemStatus existingLineItemStatus = lineItemStatuses.get(lineItemId); + final DeliveryPlan activeDeliveryPlan = lineItem.getActiveDeliveryPlan(); + if (existingLineItemStatus == null) { + final LineItemStatus lineItemStatus = createLineItemStatus(lineItem.getLineItemId()); + lineItemStatus.getDeliveryPlans().add(activeDeliveryPlan); + lineItemStatuses.put(lineItemId, lineItemStatus); + } else { + updateLineItemStatusWithActiveDeliveryPlan(existingLineItemStatus, activeDeliveryPlan); + } + } + + /** + * Updates {@link LineItemStatus} with current {@link DeliveryPlan}. + */ + public void mergePlanFromLineItem(LineItem lineItem) { + final LineItemStatus currentLineItemStatus = lineItemStatuses.computeIfAbsent(lineItem.getLineItemId(), + this::createLineItemStatus); + final DeliveryPlan updatedDeliveryPlan = lineItem.getActiveDeliveryPlan(); + + final Set deliveryPlans = currentLineItemStatus.getDeliveryPlans(); + final DeliveryPlan currentPlan = deliveryPlans.stream() + .filter(plan -> Objects.equals(plan.getPlanId(), updatedDeliveryPlan.getPlanId())) + .findFirst() + .orElse(null); + + if (currentPlan == null) { + deliveryPlans.add(updatedDeliveryPlan.withoutSpentTokens()); + } else if (currentPlan.isUpdated(updatedDeliveryPlan.getDeliverySchedule())) { + final DeliveryPlan updatedPlan = currentPlan.mergeWithNextDeliveryPlan(updatedDeliveryPlan); + deliveryPlans.remove(currentPlan); + deliveryPlans.add(updatedPlan); + } + } + + /** + * Remove stale {@link LineItemStatus} from statistic. + */ + public void cleanLineItemStatuses(ZonedDateTime now, long lineItemStatusTtl, int maxPlanNumberInDeliveryProgress) { + lineItemStatuses.entrySet().removeIf(entry -> isLineItemStatusExpired(entry.getKey(), now, lineItemStatusTtl)); + + lineItemStatuses.values().forEach( + lineItemStatus -> cutCachedDeliveryPlans(lineItemStatus, maxPlanNumberInDeliveryProgress)); + } + + /** + * Returns true when lineItem is not in metaData and it is expired for more then defined in configuration time. + */ + private boolean isLineItemStatusExpired(String lineItemId, ZonedDateTime now, long lineItemStatusTtl) { + final LineItem lineItem = lineItemService.getLineItemById(lineItemId); + + return lineItem == null || ChronoUnit.MILLIS.between(lineItem.getEndTimeStamp(), now) > lineItemStatusTtl; + } + + /** + * Cuts number of plans in {@link LineItemStatus} from overall statistic by number defined in configuration. + */ + private void cutCachedDeliveryPlans(LineItemStatus lineItemStatus, int maxPlanNumberInDeliveryProgress) { + final Set deliveryPlans = lineItemStatus.getDeliveryPlans(); + if (deliveryPlans.size() > maxPlanNumberInDeliveryProgress) { + final Set plansToRemove = deliveryPlans.stream() + .sorted(Comparator.comparing(DeliveryPlan::getEndTimeStamp)) + .limit(deliveryPlans.size() - maxPlanNumberInDeliveryProgress) + .collect(Collectors.toSet()); + plansToRemove.forEach(deliveryPlans::remove); + } + } + + /** + * Updates {@link LineItemStatus} with active {@link DeliveryPlan}. + */ + private void updateLineItemStatusWithActiveDeliveryPlan(LineItemStatus lineItemStatus, + DeliveryPlan updatedDeliveryPlan) { + final Set deliveryPlans = lineItemStatus.getDeliveryPlans(); + final DeliveryPlan currentPlan = deliveryPlans.stream() + .filter(plan -> Objects.equals(plan.getPlanId(), updatedDeliveryPlan.getPlanId())) + .filter(plan -> plan.isUpdated(updatedDeliveryPlan.getDeliverySchedule())) + .findAny() + .orElse(null); + if (currentPlan != null) { + if (!Objects.equals(currentPlan.getUpdatedTimeStamp(), updatedDeliveryPlan.getUpdatedTimeStamp())) { + deliveryPlans.add(updatedDeliveryPlan); + deliveryPlans.remove(currentPlan); + } + } else { + deliveryPlans.add(updatedDeliveryPlan); + } + } + + public void updateWithActiveLineItems(Collection lineItems) { + lineItems.forEach(lineItem -> lineItemStatuses.putIfAbsent(lineItem.getLineItemId(), + createLineItemStatus(lineItem.getLineItemId()))); + } + + public Map getLineItemStatuses() { + return lineItemStatuses; + } + + public Map getRequestsPerAccount() { + return requestsPerAccount; + } + + public Map> getLineItemIdToLost() { + return lineItemIdToLost; + } + + public LongAdder getRequests() { + return requests; + } + + public ZonedDateTime getStartTimeStamp() { + return startTimeStamp; + } + + public void setStartTimeStamp(ZonedDateTime startTimeStamp) { + this.startTimeStamp = startTimeStamp; + } + + public void setEndTimeStamp(ZonedDateTime endTimeStamp) { + this.endTimeStamp = endTimeStamp; + } + + public ZonedDateTime getEndTimeStamp() { + return endTimeStamp; + } + + private LongAdder accountRequests(String account) { + return requestsPerAccount.computeIfAbsent(account, ignored -> new LongAdder()); + } + + /** + * Increments {@link LineItemStatus} metric, creates line item status if does not exist. + */ + private void increment(String lineItemId, Consumer inc) { + inc.accept(lineItemStatuses.computeIfAbsent(lineItemId, this::createLineItemStatus)); + } + + /** + * Increment tokens in active delivery report. + */ + private void incToken(String lineItemId, Map planIdToTokenPriority) { + final LineItemStatus lineItemStatus = lineItemStatuses.get(lineItemId); + final LineItem lineItem = lineItemService.getLineItemById(lineItemId); + final DeliveryPlan lineItemActivePlan = lineItem.getActiveDeliveryPlan(); + if (lineItemActivePlan != null) { + DeliveryPlan reportActivePlan = lineItemStatus.getDeliveryPlans().stream() + .filter(plan -> Objects.equals(plan.getPlanId(), lineItemActivePlan.getPlanId())) + .findFirst() + .orElse(null); + if (reportActivePlan == null) { + reportActivePlan = lineItemActivePlan.withoutSpentTokens(); + lineItemStatus.getDeliveryPlans().add(reportActivePlan); + } + + final Integer tokenPriority = planIdToTokenPriority.get(reportActivePlan.getPlanId()); + if (tokenPriority != null) { + reportActivePlan.incTokenWithPriority(tokenPriority); + } + } + } + + /** + * Updates lostToLineItem metric for line item specified by lineItemId parameter against line item ids from + * parameter lostToLineItemIds + */ + private void updateLostToEachLineItem(String lineItemId, Set lostToLineItemsIds, + Map> lostToLineItemTimes) { + final Map lostToLineItemsTimes = lostToLineItemTimes + .computeIfAbsent(lineItemId, key -> new ConcurrentHashMap<>()); + lostToLineItemsIds.forEach(lostToLineItemId -> incLostToLineItemTimes(lostToLineItemId, lostToLineItemsTimes)); + } + + /** + * Updates listToLineItem metric against line item specified in parameter lostToLineItemId + */ + private void incLostToLineItemTimes(String lostToLineItemId, Map lostToLineItemsTimes) { + final LostToLineItem lostToLineItem = lostToLineItemsTimes.computeIfAbsent(lostToLineItemId, + ignored -> LostToLineItem.of(lostToLineItemId, new LongAdder())); + lostToLineItem.getCount().increment(); + } + + /** + * Merges requests per account to overall statistics. + */ + private void mergeRequestsCount(String accountId, LongAdder requestsCount, + Map requestsPerAccount) { + requestsPerAccount.computeIfPresent(accountId, (key, oldValue) -> { + oldValue.add(requestsCount.sum()); + return oldValue; + }); + requestsPerAccount.putIfAbsent(accountId, requestsCount); + } + + private void mergeCurrentLineItemLostReportToOverall( + String lineItemId, + Map currentLineItemLost, + Map> overallLineItemIdToLost) { + final Map overallLineItemLost = overallLineItemIdToLost + .computeIfAbsent(lineItemId, ignored -> new ConcurrentHashMap<>()); + currentLineItemLost.forEach((lineItemIdLostTo, currentLostToLineItem) -> + overallLineItemLost.merge(lineItemIdLostTo, currentLostToLineItem, this::addToCount) + ); + } + + private LostToLineItem addToCount(LostToLineItem mergeTo, LostToLineItem mergeFrom) { + mergeTo.getCount().add(mergeFrom.getCount().sum()); + return mergeTo; + } +} diff --git a/src/main/java/org/prebid/server/deals/lineitem/DeliveryToken.java b/src/main/java/org/prebid/server/deals/lineitem/DeliveryToken.java new file mode 100644 index 00000000000..4016be23e23 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/lineitem/DeliveryToken.java @@ -0,0 +1,76 @@ +package org.prebid.server.deals.lineitem; + +import org.prebid.server.deals.proto.Token; + +import java.util.Comparator; +import java.util.Objects; +import java.util.concurrent.atomic.LongAdder; + +public class DeliveryToken implements Comparable { + + private static final Comparator COMPARATOR = Comparator.comparing(DeliveryToken::getPriorityClass); + + private final Token token; + + private final LongAdder spent; + + private DeliveryToken(DeliveryToken deliveryToken) { + this(deliveryToken.token, new LongAdder()); + } + + private DeliveryToken(Token token) { + this(token, new LongAdder()); + } + + private DeliveryToken(Token token, LongAdder spent) { + this.token = Objects.requireNonNull(token); + this.spent = Objects.requireNonNull(spent); + } + + public static DeliveryToken of(DeliveryToken deliveryToken) { + return new DeliveryToken(deliveryToken); + } + + public static DeliveryToken of(Token token) { + return new DeliveryToken(token); + } + + /** + * Return unspent tokens from {@link DeliveryToken}. + */ + public int getUnspent() { + return (int) (token.getTotal() - spent.sum()); + } + + public void inc() { + spent.increment(); + } + + public DeliveryToken mergeWithToken(Token nextToken, boolean sumTotal) { + if (nextToken == null) { + return this; + } else { + final int total = sumTotal + ? getTotal() + nextToken.getTotal() + : nextToken.getTotal(); + return new DeliveryToken(Token.of(getPriorityClass(), total), spent); + } + } + + public LongAdder getSpent() { + return spent; + } + + public Integer getTotal() { + return token.getTotal(); + } + + public Integer getPriorityClass() { + return token.getPriorityClass(); + } + + @Override + public int compareTo(DeliveryToken another) { + return COMPARATOR.compare(this, another); + } +} diff --git a/src/main/java/org/prebid/server/deals/lineitem/LineItem.java b/src/main/java/org/prebid/server/deals/lineitem/LineItem.java new file mode 100644 index 00000000000..0419454eb71 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/lineitem/LineItem.java @@ -0,0 +1,272 @@ +package org.prebid.server.deals.lineitem; + +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.prebid.server.deals.proto.DeliverySchedule; +import org.prebid.server.deals.proto.FrequencyCap; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.deals.proto.LineItemSize; +import org.prebid.server.deals.proto.Price; +import org.prebid.server.deals.targeting.TargetingDefinition; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +public class LineItem { + + private static final Logger logger = LoggerFactory.getLogger(LineItem.class); + + private final LineItemMetaData metaData; + + private final Price normalizedPrice; + + private final List fcapIds; + + private final TargetingDefinition targetingDefinition; + + private final AtomicReference activeDeliveryPlan; + + private final AtomicReference readyAt; + + private LineItem(LineItemMetaData metaData, Price normalizedPrice, TargetingDefinition targetingDefinition) { + this.metaData = Objects.requireNonNull(metaData); + this.normalizedPrice = normalizedPrice; + this.targetingDefinition = targetingDefinition; + + this.fcapIds = extractFcapIds(metaData); + + activeDeliveryPlan = new AtomicReference<>(); + readyAt = new AtomicReference<>(); + } + + private LineItem(LineItemMetaData metaData, + Price normalizedPrice, + TargetingDefinition targetingDefinition, + ZonedDateTime readyAt, + ZonedDateTime now, + DeliveryPlan currentPlan) { + this(metaData, normalizedPrice, targetingDefinition); + this.readyAt.set(readyAt); + + updateOrAdvanceActivePlan(now, true, currentPlan); + } + + private LineItem(LineItemMetaData metaData, + Price normalizedPrice, + TargetingDefinition targetingDefinition, + ZonedDateTime now) { + this(metaData, normalizedPrice, targetingDefinition, null, now, null); + } + + public static LineItem of(LineItemMetaData metaData, + Price normalizedPrice, + TargetingDefinition targetingDefinition, + ZonedDateTime now) { + return new LineItem(metaData, normalizedPrice, targetingDefinition, now); + } + + public LineItem withUpdatedMetadata(LineItemMetaData metaData, + Price normalizedPrice, + TargetingDefinition targetingDefinition, + ZonedDateTime readyAt, + ZonedDateTime now) { + return new LineItem(metaData, normalizedPrice, targetingDefinition, readyAt, now, getActiveDeliveryPlan()); + } + + public void advanceToNextPlan(ZonedDateTime now, boolean isPlannerResponsive) { + updateOrAdvanceActivePlan(now, isPlannerResponsive, getActiveDeliveryPlan()); + } + + /** + * Increments tokens in {@link DeliveryToken} with highest priority within {@link DeliveryPlan}. + * + * @return class of the token incremented. + */ + public Integer incSpentToken(ZonedDateTime now) { + return incSpentToken(now, 0); + } + + public Integer incSpentToken(ZonedDateTime now, long adjustment) { + final DeliveryPlan deliveryPlan = activeDeliveryPlan.get(); + + if (deliveryPlan != null) { + final Integer tokenClassIncremented = deliveryPlan.incSpentToken(); + ZonedDateTime readyAtNewValue = deliveryPlan.calculateReadyAt(); + readyAtNewValue = readyAtNewValue != null && adjustment != 0 + ? readyAtNewValue.plusNanos(TimeUnit.MILLISECONDS.toNanos(adjustment)) + : readyAtNewValue; + readyAt.set(readyAtNewValue); + if (logger.isDebugEnabled()) { + logger.debug("ReadyAt for lineItem {0} plan {1} was updated to {2} after token was spent. Total number" + + " of unspent token is {3}. Current time is {4}", + getLineItemId(), deliveryPlan.getPlanId(), + readyAt.get(), deliveryPlan.getUnspentTokens(), now); + } + return tokenClassIncremented; + } + return null; + } + + public Integer getHighestUnspentTokensClass() { + final DeliveryPlan activeDeliveryPlan = getActiveDeliveryPlan(); + return activeDeliveryPlan != null ? activeDeliveryPlan.getHighestUnspentTokensClass() : null; + } + + public boolean isActive(ZonedDateTime now) { + return dateBetween(now, metaData.getStartTimeStamp(), metaData.getEndTimeStamp()); + } + + public DeliveryPlan getActiveDeliveryPlan() { + return activeDeliveryPlan.get(); + } + + public ZonedDateTime getReadyAt() { + return readyAt.get(); + } + + public BigDecimal getCpm() { + if (normalizedPrice != null) { + return normalizedPrice.getCpm(); + } + return null; + } + + public String getCurrency() { + if (normalizedPrice != null) { + return normalizedPrice.getCurrency(); + } + return null; + } + + public String getLineItemId() { + return metaData.getLineItemId(); + } + + public String getExtLineItemId() { + return metaData.getExtLineItemId(); + } + + public String getDealId() { + return metaData.getDealId(); + } + + public String getAccountId() { + return metaData.getAccountId(); + } + + public String getSource() { + return metaData.getSource(); + } + + public Integer getRelativePriority() { + return metaData.getRelativePriority(); + } + + public ZonedDateTime getEndTimeStamp() { + return metaData.getEndTimeStamp(); + } + + public ZonedDateTime getStartTimeStamp() { + return metaData.getStartTimeStamp(); + } + + public ZonedDateTime getUpdatedTimeStamp() { + return metaData.getUpdatedTimeStamp(); + } + + public List getFrequencyCaps() { + return metaData.getFrequencyCaps(); + } + + public List getSizes() { + return metaData.getSizes(); + } + + public TargetingDefinition getTargetingDefinition() { + return targetingDefinition; + } + + public List getFcapIds() { + return fcapIds; + } + + private static List extractFcapIds(LineItemMetaData metaData) { + return CollectionUtils.emptyIfNull(metaData.getFrequencyCaps()).stream() + .map(FrequencyCap::getFcapId) + .collect(Collectors.toList()); + } + + private void updateOrAdvanceActivePlan(ZonedDateTime now, boolean isPlannerResponsive, DeliveryPlan currentPlan) { + final DeliverySchedule currentSchedule = ListUtils.emptyIfNull(metaData.getDeliverySchedules()).stream() + .filter(schedule -> dateBetween(now, schedule.getStartTimeStamp(), schedule.getEndTimeStamp())) + .findFirst() + .orElse(null); + + if (currentSchedule != null) { + final DeliveryPlan resolvedPlan = resolveActivePlan(currentPlan, currentSchedule, isPlannerResponsive); + final ZonedDateTime readyAtBeforeUpdate = readyAt.get(); + if (currentPlan != resolvedPlan) { + readyAt.set(currentPlan == null || !Objects.equals(currentSchedule.getPlanId(), currentPlan.getPlanId()) + ? calculateReadyAfterMovingToNextPlan(now, resolvedPlan) + : calculateReadyAtAfterPlanUpdated(now, resolvedPlan)); + logger.info("ReadyAt for Line Item `{0}` was updated from plan {1} to {2} and readyAt from {3} to {4}" + + " at time is {5}", getLineItemId(), + currentPlan != null ? currentPlan.getPlanId() : " no plan ", resolvedPlan.getPlanId(), + readyAtBeforeUpdate, getReadyAt(), now); + if (logger.isDebugEnabled()) { + logger.debug("Unspent tokens number for plan {0} is {1}", resolvedPlan.getPlanId(), + resolvedPlan.getUnspentTokens()); + } + } + activeDeliveryPlan.set(resolvedPlan); + } else { + activeDeliveryPlan.set(null); + readyAt.set(null); + logger.info("Active plan for Line Item `{0}` was not found at time is {1}, readyAt updated with 'never'," + + " until active plan become available", getLineItemId(), now); + } + } + + private ZonedDateTime calculateReadyAtAfterPlanUpdated(ZonedDateTime now, DeliveryPlan resolvedPlan) { + final ZonedDateTime resolvedReadyAt = resolvedPlan.calculateReadyAt(); + logger.debug("Current plan for Line Item `{0}` was considered as updated from GP response and readyAt will be " + + "updated from {1} to {2} at time is {3}", getLineItemId(), getReadyAt(), resolvedReadyAt, now); + return resolvedReadyAt; + } + + private ZonedDateTime calculateReadyAfterMovingToNextPlan(ZonedDateTime now, DeliveryPlan resolvedPlan) { + return resolvedPlan.getDeliveryTokens().stream().anyMatch(deliveryToken -> deliveryToken.getTotal() > 0) + ? now + : null; + } + + private static DeliveryPlan resolveActivePlan(DeliveryPlan currentPlan, + DeliverySchedule currentSchedule, + boolean isPlannerResponsive) { + if (currentPlan != null) { + if (Objects.equals(currentPlan.getPlanId(), currentSchedule.getPlanId())) { + return currentPlan.getUpdatedTimeStamp().isBefore(currentSchedule.getUpdatedTimeStamp()) + ? currentPlan.mergeWithNextDeliverySchedule(currentSchedule, false) + : currentPlan; + } else if (!isPlannerResponsive) { + return currentPlan.mergeWithNextDeliverySchedule(currentSchedule, true); + } + } + + return DeliveryPlan.of(currentSchedule); + } + + /** + * Returns true when now parameter is after startDate and before expirationDate. + */ + private static boolean dateBetween(ZonedDateTime now, ZonedDateTime startDate, ZonedDateTime expirationDate) { + return (now.isEqual(startDate) || now.isAfter(startDate)) && now.isBefore(expirationDate); + } +} diff --git a/src/main/java/org/prebid/server/deals/lineitem/LineItemStatus.java b/src/main/java/org/prebid/server/deals/lineitem/LineItemStatus.java new file mode 100644 index 00000000000..e05cd399d0d --- /dev/null +++ b/src/main/java/org/prebid/server/deals/lineitem/LineItemStatus.java @@ -0,0 +1,167 @@ +package org.prebid.server.deals.lineitem; + +import io.vertx.core.impl.ConcurrentHashSet; +import lombok.Value; +import org.prebid.server.deals.proto.report.Event; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Value +public class LineItemStatus { + + String lineItemId; + + String source; + + String dealId; + + String extLineItemId; + + String accountId; + + LongAdder domainMatched; + + LongAdder targetMatched; + + LongAdder targetMatchedButFcapped; + + LongAdder targetMatchedButFcapLookupFailed; + + LongAdder pacingDeferred; + + LongAdder sentToBidder; + + LongAdder sentToBidderAsTopMatch; + + LongAdder receivedFromBidder; + + LongAdder receivedFromBidderInvalidated; + + LongAdder sentToClient; + + LongAdder sentToClientAsTopMatch; + + Set lostToLineItems; + + Set events; + + Set deliveryPlans; + + private LineItemStatus(String lineItemId, String source, String dealId, String extLineItemId, String accountId) { + this.lineItemId = lineItemId; + this.source = source; + this.dealId = dealId; + this.extLineItemId = extLineItemId; + this.accountId = accountId; + + domainMatched = new LongAdder(); + targetMatched = new LongAdder(); + targetMatchedButFcapped = new LongAdder(); + targetMatchedButFcapLookupFailed = new LongAdder(); + pacingDeferred = new LongAdder(); + sentToBidder = new LongAdder(); + sentToBidderAsTopMatch = new LongAdder(); + receivedFromBidder = new LongAdder(); + receivedFromBidderInvalidated = new LongAdder(); + sentToClient = new LongAdder(); + sentToClientAsTopMatch = new LongAdder(); + + lostToLineItems = new ConcurrentHashSet<>(); + events = new ConcurrentHashSet<>(); + deliveryPlans = new ConcurrentHashSet<>(); + } + + public static LineItemStatus of(String lineItemId, String source, String dealId, String extLineItemId, + String accountId) { + return new LineItemStatus(lineItemId, source, dealId, extLineItemId, accountId); + } + + public static LineItemStatus of(LineItem lineItem) { + return new LineItemStatus(lineItem.getLineItemId(), lineItem.getSource(), lineItem.getDealId(), + lineItem.getExtLineItemId(), lineItem.getAccountId()); + } + + public static LineItemStatus of(String lineItemId) { + return new LineItemStatus(lineItemId, null, null, null, null); + } + + public void incDomainMatched() { + domainMatched.increment(); + } + + public void incTargetMatched() { + targetMatched.increment(); + } + + public void incTargetMatchedButFcapped() { + targetMatchedButFcapped.increment(); + } + + public void incTargetMatchedButFcapLookupFailed() { + targetMatchedButFcapLookupFailed.increment(); + } + + public void incPacingDeferred() { + pacingDeferred.increment(); + } + + public void incSentToBidder() { + sentToBidder.increment(); + } + + public void incSentToBidderAsTopMatch() { + sentToBidderAsTopMatch.increment(); + } + + public void incReceivedFromBidder() { + receivedFromBidder.increment(); + } + + public void incReceivedFromBidderInvalidated() { + receivedFromBidderInvalidated.increment(); + } + + public void incSentToClient() { + sentToClient.increment(); + } + + public void incSentToClientAsTopMatch() { + sentToClientAsTopMatch.increment(); + } + + public void merge(LineItemStatus other) { + domainMatched.add(other.domainMatched.sum()); + targetMatched.add(other.targetMatched.sum()); + targetMatchedButFcapped.add(other.targetMatchedButFcapped.sum()); + targetMatchedButFcapLookupFailed.add(other.getTargetMatchedButFcapLookupFailed().sum()); + pacingDeferred.add(other.pacingDeferred.sum()); + sentToBidder.add(other.sentToBidder.sum()); + sentToBidderAsTopMatch.add(other.sentToBidderAsTopMatch.sum()); + receivedFromBidder.add(other.receivedFromBidder.sum()); + receivedFromBidderInvalidated.add(other.receivedFromBidderInvalidated.sum()); + sentToClient.add(other.sentToClient.sum()); + sentToClientAsTopMatch.add(other.sentToClientAsTopMatch.sum()); + mergeEvents(other); + } + + private void mergeEvents(LineItemStatus other) { + final Map typesToEvent = other.events.stream() + .collect(Collectors.toMap(Event::getType, Function.identity())); + typesToEvent.forEach(this::addOrUpdateEvent); + } + + private void addOrUpdateEvent(String type, Event distEvent) { + final Event sameTypeEvent = events.stream() + .filter(event -> event.getType().equals(type)) + .findFirst().orElse(null); + if (sameTypeEvent != null) { + sameTypeEvent.getCount().add(distEvent.getCount().sum()); + } else { + events.add(distEvent); + } + } +} diff --git a/src/main/java/org/prebid/server/deals/lineitem/LostToLineItem.java b/src/main/java/org/prebid/server/deals/lineitem/LostToLineItem.java new file mode 100644 index 00000000000..0b7be6d1f21 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/lineitem/LostToLineItem.java @@ -0,0 +1,15 @@ +package org.prebid.server.deals.lineitem; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.concurrent.atomic.LongAdder; + +@AllArgsConstructor(staticName = "of") +@Value +public class LostToLineItem { + + String lineItemId; + + LongAdder count; +} diff --git a/src/main/java/org/prebid/server/deals/model/AdminAccounts.java b/src/main/java/org/prebid/server/deals/model/AdminAccounts.java new file mode 100644 index 00000000000..b5829dab3e6 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/AdminAccounts.java @@ -0,0 +1,13 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@Value +@AllArgsConstructor(staticName = "of") +public class AdminAccounts { + + List accounts; +} diff --git a/src/main/java/org/prebid/server/deals/model/AdminCentralResponse.java b/src/main/java/org/prebid/server/deals/model/AdminCentralResponse.java new file mode 100644 index 00000000000..034389ddd78 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/AdminCentralResponse.java @@ -0,0 +1,25 @@ +package org.prebid.server.deals.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class AdminCentralResponse { + + LogTracer tracer; + + @JsonProperty("storedrequest") + Command storedRequest; + + @JsonProperty("storedrequest-amp") + Command storedRequestAmp; + + @JsonProperty("line-items") + Command lineItems; + + Command account; + + ServicesCommand services; +} diff --git a/src/main/java/org/prebid/server/deals/model/AdminLineItems.java b/src/main/java/org/prebid/server/deals/model/AdminLineItems.java new file mode 100644 index 00000000000..14f2ecca150 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/AdminLineItems.java @@ -0,0 +1,13 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@Value +@AllArgsConstructor(staticName = "of") +public class AdminLineItems { + + List ids; +} diff --git a/src/main/java/org/prebid/server/deals/model/AlertEvent.java b/src/main/java/org/prebid/server/deals/model/AlertEvent.java new file mode 100644 index 00000000000..a231b1f6bbf --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/AlertEvent.java @@ -0,0 +1,25 @@ +package org.prebid.server.deals.model; + +import lombok.Builder; +import lombok.Value; + +import java.time.ZonedDateTime; + +@Builder +@Value +public class AlertEvent { + + private String id; + + private String action; + + private AlertPriority priority; + + private ZonedDateTime updatedAt; + + private String name; + + private String details; + + private AlertSource source; +} diff --git a/src/main/java/org/prebid/server/deals/model/AlertPriority.java b/src/main/java/org/prebid/server/deals/model/AlertPriority.java new file mode 100644 index 00000000000..074fa5e164c --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/AlertPriority.java @@ -0,0 +1,6 @@ +package org.prebid.server.deals.model; + +public enum AlertPriority { + + HIGH, MEDIUM, LOW +} diff --git a/src/main/java/org/prebid/server/deals/model/AlertProxyProperties.java b/src/main/java/org/prebid/server/deals/model/AlertProxyProperties.java new file mode 100644 index 00000000000..28d076129da --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/AlertProxyProperties.java @@ -0,0 +1,23 @@ +package org.prebid.server.deals.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Builder +@Value +public class AlertProxyProperties { + + boolean enabled; + + String url; + + int timeoutSec; + + Map alertTypes; + + String username; + + String password; +} diff --git a/src/main/java/org/prebid/server/deals/model/AlertSource.java b/src/main/java/org/prebid/server/deals/model/AlertSource.java new file mode 100644 index 00000000000..b4be61a2a43 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/AlertSource.java @@ -0,0 +1,25 @@ +package org.prebid.server.deals.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class AlertSource { + + private String env; + + @JsonProperty("data-center") + private String dataCenter; + + private String region; + + private String system; + + @JsonProperty("sub-system") + private String subSystem; + + @JsonProperty("host-id") + private String hostId; +} diff --git a/src/main/java/org/prebid/server/deals/model/Command.java b/src/main/java/org/prebid/server/deals/model/Command.java new file mode 100644 index 00000000000..7acbec5e4c0 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/Command.java @@ -0,0 +1,14 @@ +package org.prebid.server.deals.model; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class Command { + + String cmd; + + ObjectNode body; +} diff --git a/src/main/java/org/prebid/server/deals/model/DeepDebugLog.java b/src/main/java/org/prebid/server/deals/model/DeepDebugLog.java new file mode 100644 index 00000000000..597c55f63b0 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/DeepDebugLog.java @@ -0,0 +1,45 @@ +package org.prebid.server.deals.model; + +import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal; +import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal.Category; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +public class DeepDebugLog { + + private final boolean deepDebugEnabled; + + private final List entries; + + private final Clock clock; + + private DeepDebugLog(boolean deepDebugEnabled, Clock clock) { + this.deepDebugEnabled = deepDebugEnabled; + this.entries = deepDebugEnabled ? new LinkedList<>() : null; + this.clock = Objects.requireNonNull(clock); + } + + public static DeepDebugLog create(boolean deepDebugEnabled, Clock clock) { + return new DeepDebugLog(deepDebugEnabled, clock); + } + + public void add(String lineItemId, Category category, Supplier messageSupplier) { + if (deepDebugEnabled) { + entries.add(ExtTraceDeal.of(lineItemId, ZonedDateTime.now(clock), category, messageSupplier.get())); + } + } + + public boolean isDeepDebugEnabled() { + return deepDebugEnabled; + } + + public List entries() { + return entries == null ? Collections.emptyList() : Collections.unmodifiableList(entries); + } +} diff --git a/src/main/java/org/prebid/server/deals/model/DeliveryProgressProperties.java b/src/main/java/org/prebid/server/deals/model/DeliveryProgressProperties.java new file mode 100644 index 00000000000..95007f33ea0 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/DeliveryProgressProperties.java @@ -0,0 +1,13 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class DeliveryProgressProperties { + + long lineItemStatusTtlSeconds; + + int cachedPlansNumber; +} diff --git a/src/main/java/org/prebid/server/deals/model/DeliveryStatsProperties.java b/src/main/java/org/prebid/server/deals/model/DeliveryStatsProperties.java new file mode 100644 index 00000000000..237c094a33b --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/DeliveryStatsProperties.java @@ -0,0 +1,31 @@ +package org.prebid.server.deals.model; + +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +@Builder +@Value +public class DeliveryStatsProperties { + + @NonNull + String endpoint; + + int cachedReportsNumber; + + long timeoutMs; + + int lineItemsPerReport; + + int reportsIntervalMs; + + int batchesIntervalMs; + + boolean requestCompressionEnabled; + + @NonNull + String username; + + @NonNull + String password; +} diff --git a/src/main/java/org/prebid/server/deals/model/DeploymentProperties.java b/src/main/java/org/prebid/server/deals/model/DeploymentProperties.java new file mode 100644 index 00000000000..afd08710145 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/DeploymentProperties.java @@ -0,0 +1,25 @@ +package org.prebid.server.deals.model; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class DeploymentProperties { + + String pbsHostId; + + String pbsRegion; + + String pbsVendor; + + String profile; + + String infra; + + String dataCenter; + + String system; + + String subSystem; +} diff --git a/src/main/java/org/prebid/server/deals/model/DeviceInfo.java b/src/main/java/org/prebid/server/deals/model/DeviceInfo.java new file mode 100644 index 00000000000..e58f9bcedf9 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/DeviceInfo.java @@ -0,0 +1,35 @@ +package org.prebid.server.deals.model; + +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +@Builder +@Value +public class DeviceInfo { + + @NonNull + String vendor; + + DeviceType deviceType; + + String deviceTypeRaw; + + String osfamily; + + String os; + + String osVersion; + + String manufacturer; + + String model; + + String browser; + + String browserVersion; + + String carrier; + + String language; +} diff --git a/src/main/java/org/prebid/server/deals/model/DeviceType.java b/src/main/java/org/prebid/server/deals/model/DeviceType.java new file mode 100644 index 00000000000..43dbefa13b4 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/DeviceType.java @@ -0,0 +1,67 @@ +package org.prebid.server.deals.model; + +public enum DeviceType { + + MOBILE("mobile"), + DESKTOP("desktop"), + TV("connected tv"), + PHONE("phone"), + DEVICE("connected device"), + SET_TOP_BOX("set top box"), + TABLET("tablet"); + + private String name; + + DeviceType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static DeviceType resolveDeviceType(String deviceType) { + if (deviceType == null) { + return null; + } + + switch (deviceType) { + case "Mobile Phone": + case "Mobile": + case "SmartPhone": + case "SmallScreen": + return MOBILE; + case "Desktop": + case "Single-board Computer": + return DESKTOP; + case "TV": + case "Tv": + return TV; + case "Fixed Wireless Phone": + case "Vehicle Phone": + return PHONE; + case "Tablet": + return TABLET; + case "Digital Home Assistant": + case "Digital Signage Media Player": + case "eReader": + case "EReader": + case "Console": + case "Games Console": + case "Media Player": + case "Payment Terminal": + case "Refrigerator": + case "Vehicle Multimedia System": + case "Weighing Scale": + case "Wristwatch": + case "SmartWatch": + return DEVICE; + case "Set Top Box": + // might not be correct for 51degrees (https://51degrees.com/resources/property-dictionary) + case "MediaHub": + return SET_TOP_BOX; + default: + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/deals/model/ExtUser.java b/src/main/java/org/prebid/server/deals/model/ExtUser.java new file mode 100644 index 00000000000..26f6d610fa3 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/ExtUser.java @@ -0,0 +1,15 @@ +package org.prebid.server.deals.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtUser { + + @JsonProperty("fcapIds") + List fcapIds; +} diff --git a/src/main/java/org/prebid/server/deals/model/LineItemTargeting.java b/src/main/java/org/prebid/server/deals/model/LineItemTargeting.java new file mode 100644 index 00000000000..cabdef1b620 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/LineItemTargeting.java @@ -0,0 +1,16 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +/** + * Defines the contract for lineitems[].targeting. + */ +@AllArgsConstructor(staticName = "of") +@Value +public class LineItemTargeting { + + List size; +} diff --git a/src/main/java/org/prebid/server/deals/model/LineItemTargetingSize.java b/src/main/java/org/prebid/server/deals/model/LineItemTargetingSize.java new file mode 100644 index 00000000000..a6e7c6cb401 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/LineItemTargetingSize.java @@ -0,0 +1,16 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for lineitems[].targeting.size. + */ +@AllArgsConstructor(staticName = "of") +@Value +public class LineItemTargetingSize { + + Integer w; + + Integer h; +} diff --git a/src/main/java/org/prebid/server/deals/model/LogCriteriaFilter.java b/src/main/java/org/prebid/server/deals/model/LogCriteriaFilter.java new file mode 100644 index 00000000000..758a40a6282 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/LogCriteriaFilter.java @@ -0,0 +1,19 @@ +package org.prebid.server.deals.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class LogCriteriaFilter { + + @JsonProperty("accountId") + String accountId; + + @JsonProperty("bidderCode") + String bidderCode; + + @JsonProperty("lineItemId") + String lineItemId; +} diff --git a/src/main/java/org/prebid/server/deals/model/LogTracer.java b/src/main/java/org/prebid/server/deals/model/LogTracer.java new file mode 100644 index 00000000000..fb5c2e9c456 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/LogTracer.java @@ -0,0 +1,19 @@ +package org.prebid.server.deals.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class LogTracer { + + String cmd; + + Boolean raw; + + @JsonProperty("durationInSeconds") + Long durationInSeconds; + + LogCriteriaFilter filters; +} diff --git a/src/main/java/org/prebid/server/deals/model/MatchLineItemsResult.java b/src/main/java/org/prebid/server/deals/model/MatchLineItemsResult.java new file mode 100644 index 00000000000..22d1cd01078 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/MatchLineItemsResult.java @@ -0,0 +1,14 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.deals.lineitem.LineItem; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value +public class MatchLineItemsResult { + + List lineItems; +} diff --git a/src/main/java/org/prebid/server/deals/model/PlannerProperties.java b/src/main/java/org/prebid/server/deals/model/PlannerProperties.java new file mode 100644 index 00000000000..f7eac001842 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/PlannerProperties.java @@ -0,0 +1,26 @@ +package org.prebid.server.deals.model; + +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +@Builder +@Value +public class PlannerProperties { + + @NonNull + String planEndpoint; + + @NonNull + String registerEndpoint; + + long timeoutMs; + + long registerPeriodSeconds; + + @NonNull + String username; + + @NonNull + String password; +} diff --git a/src/main/java/org/prebid/server/deals/model/Segment.java b/src/main/java/org/prebid/server/deals/model/Segment.java new file mode 100644 index 00000000000..648e3cf5f0c --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/Segment.java @@ -0,0 +1,11 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class Segment { + + String id; +} diff --git a/src/main/java/org/prebid/server/deals/model/ServicesCommand.java b/src/main/java/org/prebid/server/deals/model/ServicesCommand.java new file mode 100644 index 00000000000..f58c26791d5 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/ServicesCommand.java @@ -0,0 +1,11 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class ServicesCommand { + + String cmd; +} diff --git a/src/main/java/org/prebid/server/deals/model/SimulationProperties.java b/src/main/java/org/prebid/server/deals/model/SimulationProperties.java new file mode 100644 index 00000000000..fc520afbbcf --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/SimulationProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.deals.model; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class SimulationProperties { + + boolean enabled; + + boolean winEventsEnabled; + + boolean userDetailsEnabled; +} diff --git a/src/main/java/org/prebid/server/deals/model/TxnLog.java b/src/main/java/org/prebid/server/deals/model/TxnLog.java new file mode 100644 index 00000000000..9a65d77b9b4 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/TxnLog.java @@ -0,0 +1,56 @@ +package org.prebid.server.deals.model; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.apache.commons.collections4.Factory; +import org.apache.commons.collections4.MapUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@Getter +@Accessors(fluent = true, chain = true) +@NoArgsConstructor(staticName = "create") +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@EqualsAndHashCode +public class TxnLog { + + Set lineItemsMatchedDomainTargeting = new HashSet<>(); + + Set lineItemsMatchedWholeTargeting = new HashSet<>(); + + Set lineItemsMatchedTargetingFcapped = new HashSet<>(); + + Set lineItemsMatchedTargetingFcapLookupFailed = new HashSet<>(); + + Set lineItemsReadyToServe = new HashSet<>(); + + Set lineItemsPacingDeferred = new HashSet<>(); + + Map> lineItemsSentToBidder = MapUtils.lazyMap(new HashMap<>(), + (Factory>) HashSet::new); + + Map> lineItemsSentToBidderAsTopMatch = MapUtils.lazyMap(new HashMap<>(), + (Factory>) HashSet::new); + + Map> lineItemsReceivedFromBidder = MapUtils.lazyMap(new HashMap<>(), + (Factory>) HashSet::new); + + Set lineItemsResponseInvalidated = new HashSet<>(); + + Set lineItemsSentToClient = new HashSet<>(); + + Map> lostMatchingToLineItems = MapUtils.lazyMap(new HashMap<>(), + (Factory>) HashSet::new); + + Map> lostAuctionToLineItems = MapUtils.lazyMap(new HashMap<>(), + (Factory>) HashSet::new); + + Set lineItemSentToClientAsTopMatch = new HashSet<>(); +} diff --git a/src/main/java/org/prebid/server/deals/model/User.java b/src/main/java/org/prebid/server/deals/model/User.java new file mode 100644 index 00000000000..fcb777fb957 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/User.java @@ -0,0 +1,15 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value +public class User { + + List data; + + ExtUser ext; +} diff --git a/src/main/java/org/prebid/server/deals/model/UserData.java b/src/main/java/org/prebid/server/deals/model/UserData.java new file mode 100644 index 00000000000..e25f41ab6ff --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/UserData.java @@ -0,0 +1,17 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value +public class UserData { + + String id; + + String name; + + List segment; +} diff --git a/src/main/java/org/prebid/server/deals/model/UserDetails.java b/src/main/java/org/prebid/server/deals/model/UserDetails.java new file mode 100644 index 00000000000..5ded7d3a481 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/UserDetails.java @@ -0,0 +1,21 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value +public class UserDetails { + + private static final UserDetails EMPTY = UserDetails.of(null, null); + + List userData; + + List fcapIds; + + public static UserDetails empty() { + return EMPTY; + } +} diff --git a/src/main/java/org/prebid/server/deals/model/UserDetailsProperties.java b/src/main/java/org/prebid/server/deals/model/UserDetailsProperties.java new file mode 100644 index 00000000000..2a01b7a1208 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/UserDetailsProperties.java @@ -0,0 +1,23 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value +public class UserDetailsProperties { + + @NonNull + String userDetailsEndpoint; + + @NonNull + String winEventEndpoint; + + long timeout; + + @NonNull + List userIds; +} diff --git a/src/main/java/org/prebid/server/deals/model/UserDetailsRequest.java b/src/main/java/org/prebid/server/deals/model/UserDetailsRequest.java new file mode 100644 index 00000000000..b17091924a1 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/UserDetailsRequest.java @@ -0,0 +1,15 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value +public class UserDetailsRequest { + + String time; + + List ids; +} diff --git a/src/main/java/org/prebid/server/deals/model/UserDetailsResponse.java b/src/main/java/org/prebid/server/deals/model/UserDetailsResponse.java new file mode 100644 index 00000000000..09e83e5ef11 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/UserDetailsResponse.java @@ -0,0 +1,11 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class UserDetailsResponse { + + User user; +} diff --git a/src/main/java/org/prebid/server/deals/model/UserId.java b/src/main/java/org/prebid/server/deals/model/UserId.java new file mode 100644 index 00000000000..4ab19e907ba --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/UserId.java @@ -0,0 +1,13 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class UserId { + + String type; + + String id; +} diff --git a/src/main/java/org/prebid/server/deals/model/UserIdRule.java b/src/main/java/org/prebid/server/deals/model/UserIdRule.java new file mode 100644 index 00000000000..674cd05f04b --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/UserIdRule.java @@ -0,0 +1,19 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class UserIdRule { + + @NonNull + String type; + + @NonNull + String source; + + @NonNull + String location; +} diff --git a/src/main/java/org/prebid/server/deals/model/UserServiceResult.java b/src/main/java/org/prebid/server/deals/model/UserServiceResult.java new file mode 100644 index 00000000000..c30d8fec532 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/UserServiceResult.java @@ -0,0 +1,18 @@ +package org.prebid.server.deals.model; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.cache.model.DebugHttpCall; + +@AllArgsConstructor(staticName = "of") +@Value +public class UserServiceResult { + + UserDetails userDetails; + + DebugHttpCall cacheHttpCall; + + public static UserServiceResult empty() { + return UserServiceResult.of(UserDetails.empty(), DebugHttpCall.empty()); + } +} diff --git a/src/main/java/org/prebid/server/deals/model/WinEventNotification.java b/src/main/java/org/prebid/server/deals/model/WinEventNotification.java new file mode 100644 index 00000000000..06e4dbcd85f --- /dev/null +++ b/src/main/java/org/prebid/server/deals/model/WinEventNotification.java @@ -0,0 +1,37 @@ +package org.prebid.server.deals.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.deals.proto.FrequencyCap; + +import java.time.ZonedDateTime; +import java.util.List; + +@Builder(toBuilder = true) +@Value +public class WinEventNotification { + + @JsonProperty("bidderCode") + String bidderCode; + + @JsonProperty("bidId") + String bidId; + + @JsonProperty("lineItemId") + String lineItemId; + + String region; + + @JsonProperty("userIds") + List userIds; + + @JsonProperty("winEventDateTime") + ZonedDateTime winEventDateTime; + + @JsonProperty("lineUpdatedDateTime") + ZonedDateTime lineUpdatedDateTime; + + @JsonProperty("frequencyCaps") + List frequencyCaps; +} diff --git a/src/main/java/org/prebid/server/deals/proto/CurrencyServiceState.java b/src/main/java/org/prebid/server/deals/proto/CurrencyServiceState.java new file mode 100644 index 00000000000..937ccdc5c39 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/CurrencyServiceState.java @@ -0,0 +1,13 @@ +package org.prebid.server.deals.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class CurrencyServiceState { + + @JsonProperty("lastUpdate") + String lastUpdate; +} diff --git a/src/main/java/org/prebid/server/deals/proto/DeliverySchedule.java b/src/main/java/org/prebid/server/deals/proto/DeliverySchedule.java new file mode 100644 index 00000000000..32f7b779fa2 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/DeliverySchedule.java @@ -0,0 +1,30 @@ +package org.prebid.server.deals.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.time.ZonedDateTime; +import java.util.Set; + +/** + * Defines the contract for lineItems[].deliverySchedules[]. + */ +@Builder +@Value +public class DeliverySchedule { + + @JsonProperty("planId") + String planId; + + @JsonProperty("startTimeStamp") + ZonedDateTime startTimeStamp; + + @JsonProperty("endTimeStamp") + ZonedDateTime endTimeStamp; + + @JsonProperty("updatedTimeStamp") + ZonedDateTime updatedTimeStamp; + + Set tokens; +} diff --git a/src/main/java/org/prebid/server/deals/proto/FrequencyCap.java b/src/main/java/org/prebid/server/deals/proto/FrequencyCap.java new file mode 100644 index 00000000000..053a9331fab --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/FrequencyCap.java @@ -0,0 +1,23 @@ +package org.prebid.server.deals.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +/** + * Defines the contract for lineItems[].frequencyCap. + */ +@Builder +@Value +public class FrequencyCap { + + @JsonProperty("fcapId") + String fcapId; + + Long count; + + Integer periods; + + @JsonProperty("periodType") + String periodType; +} diff --git a/src/main/java/org/prebid/server/deals/proto/LineItemMetaData.java b/src/main/java/org/prebid/server/deals/proto/LineItemMetaData.java new file mode 100644 index 00000000000..8bba4760ba1 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/LineItemMetaData.java @@ -0,0 +1,57 @@ +package org.prebid.server.deals.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; + +import java.time.ZonedDateTime; +import java.util.List; + +/** + * Defines the contract for lineItems[]. + */ +@Builder(toBuilder = true) +@Value +public class LineItemMetaData { + + @JsonProperty("lineItemId") + String lineItemId; + + @JsonProperty("extLineItemId") + String extLineItemId; + + @JsonProperty("dealId") + String dealId; + + List sizes; + + @JsonProperty("accountId") + String accountId; + + String source; + + Price price; + + @JsonProperty("relativePriority") + Integer relativePriority; + + @JsonProperty("startTimeStamp") + ZonedDateTime startTimeStamp; + + @JsonProperty("endTimeStamp") + ZonedDateTime endTimeStamp; + + @JsonProperty("updatedTimeStamp") + ZonedDateTime updatedTimeStamp; + + String status; + + @JsonProperty("frequencyCaps") + List frequencyCaps; + + @JsonProperty("deliverySchedules") + List deliverySchedules; + + ObjectNode targeting; +} diff --git a/src/main/java/org/prebid/server/deals/proto/LineItemSize.java b/src/main/java/org/prebid/server/deals/proto/LineItemSize.java new file mode 100644 index 00000000000..8715d22a59a --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/LineItemSize.java @@ -0,0 +1,16 @@ +package org.prebid.server.deals.proto; + +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for lineItems[].sizes[]. + */ +@AllArgsConstructor(staticName = "of") +@Value +public class LineItemSize { + + Integer w; + + Integer h; +} diff --git a/src/main/java/org/prebid/server/deals/proto/Price.java b/src/main/java/org/prebid/server/deals/proto/Price.java new file mode 100644 index 00000000000..15c3c5f1963 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/Price.java @@ -0,0 +1,15 @@ +package org.prebid.server.deals.proto; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.math.BigDecimal; + +@AllArgsConstructor(staticName = "of") +@Value +public class Price { + + BigDecimal cpm; + + String currency; +} diff --git a/src/main/java/org/prebid/server/deals/proto/RegisterRequest.java b/src/main/java/org/prebid/server/deals/proto/RegisterRequest.java new file mode 100644 index 00000000000..4895468c6b7 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/RegisterRequest.java @@ -0,0 +1,24 @@ +package org.prebid.server.deals.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.math.BigDecimal; + +@AllArgsConstructor(staticName = "of") +@Value +public class RegisterRequest { + + @JsonProperty("healthIndex") + BigDecimal healthIndex; + + Status status; + + @JsonProperty("hostInstanceId") + String hostInstanceId; + + String region; + + String vendor; +} diff --git a/src/main/java/org/prebid/server/deals/proto/Status.java b/src/main/java/org/prebid/server/deals/proto/Status.java new file mode 100644 index 00000000000..974ae7fe0f2 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/Status.java @@ -0,0 +1,18 @@ +package org.prebid.server.deals.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; + +@AllArgsConstructor(staticName = "of") +@Value +public class Status { + + @JsonProperty("currencyRates") + CurrencyServiceState currencyRates; + + @JsonProperty("dealsStatus") + DeliveryProgressReport deliveryProgressReport; + +} diff --git a/src/main/java/org/prebid/server/deals/proto/Token.java b/src/main/java/org/prebid/server/deals/proto/Token.java new file mode 100644 index 00000000000..4c8acccaed7 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/Token.java @@ -0,0 +1,18 @@ +package org.prebid.server.deals.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for lineItems[].deliverySchedule[].tokens[]. + */ +@AllArgsConstructor(staticName = "of") +@Value +public class Token { + + @JsonProperty("class") + Integer priorityClass; + + Integer total; +} diff --git a/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReport.java b/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReport.java new file mode 100644 index 00000000000..b1a133b2299 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReport.java @@ -0,0 +1,37 @@ +package org.prebid.server.deals.proto.report; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.Set; + +@Builder(toBuilder = true) +@Value +public class DeliveryProgressReport { + + @JsonProperty("reportId") + String reportId; + + @JsonProperty("reportTimeStamp") + String reportTimeStamp; + + @JsonProperty("dataWindowStartTimeStamp") + String dataWindowStartTimeStamp; + + @JsonProperty("dataWindowEndTimeStamp") + String dataWindowEndTimeStamp; + + @JsonProperty("instanceId") + String instanceId; + + String vendor; + + String region; + + @JsonProperty("clientAuctions") + Long clientAuctions; + + @JsonProperty("lineItemStatus") + Set lineItemStatus; +} diff --git a/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReportBatch.java b/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReportBatch.java new file mode 100644 index 00000000000..91226ef7d32 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/report/DeliveryProgressReportBatch.java @@ -0,0 +1,21 @@ +package org.prebid.server.deals.proto.report; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.Set; + +@Value +@AllArgsConstructor(staticName = "of") +public class DeliveryProgressReportBatch { + + Set reports; + + String reportId; + + String dataWindowEndTimeStamp; + + public void removeReports(Set reports) { + this.reports.removeAll(reports); + } +} diff --git a/src/main/java/org/prebid/server/deals/proto/report/DeliverySchedule.java b/src/main/java/org/prebid/server/deals/proto/report/DeliverySchedule.java new file mode 100644 index 00000000000..de1e2df8427 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/report/DeliverySchedule.java @@ -0,0 +1,26 @@ +package org.prebid.server.deals.proto.report; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.Set; + +@Builder(toBuilder = true) +@Value +public class DeliverySchedule { + + @JsonProperty("planId") + String planId; + + @JsonProperty("planStartTimeStamp") + String planStartTimeStamp; + + @JsonProperty("planExpirationTimeStamp") + String planExpirationTimeStamp; + + @JsonProperty("planUpdatedTimeStamp") + String planUpdatedTimeStamp; + + Set tokens; +} diff --git a/src/main/java/org/prebid/server/deals/proto/report/Event.java b/src/main/java/org/prebid/server/deals/proto/report/Event.java new file mode 100644 index 00000000000..686f84b0e96 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/report/Event.java @@ -0,0 +1,15 @@ +package org.prebid.server.deals.proto.report; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.concurrent.atomic.LongAdder; + +@AllArgsConstructor(staticName = "of") +@Value +public class Event { + + String type; + + LongAdder count; +} diff --git a/src/main/java/org/prebid/server/deals/proto/report/LineItemStatus.java b/src/main/java/org/prebid/server/deals/proto/report/LineItemStatus.java new file mode 100644 index 00000000000..cb5f1629848 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/report/LineItemStatus.java @@ -0,0 +1,77 @@ +package org.prebid.server.deals.proto.report; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.Set; + +@Builder +@Value +public class LineItemStatus { + + @JsonProperty("lineItemSource") + String lineItemSource; + + @JsonProperty("lineItemId") + String lineItemId; + + @JsonProperty("dealId") + String dealId; + + @JsonProperty("extLineItemId") + String extLineItemId; + + @JsonProperty("accountAuctions") + Long accountAuctions; + + @JsonProperty("domainMatched") + Long domainMatched; + + @JsonProperty("targetMatched") + Long targetMatched; + + @JsonProperty("targetMatchedButFcapped") + Long targetMatchedButFcapped; + + @JsonProperty("targetMatchedButFcapLookupFailed") + Long targetMatchedButFcapLookupFailed; + + @JsonProperty("pacingDeferred") + Long pacingDeferred; + + @JsonProperty("sentToBidder") + Long sentToBidder; + + @JsonProperty("sentToBidderAsTopMatch") + Long sentToBidderAsTopMatch; + + @JsonProperty("receivedFromBidder") + Long receivedFromBidder; + + @JsonProperty("receivedFromBidderInvalidated") + Long receivedFromBidderInvalidated; + + @JsonProperty("sentToClient") + Long sentToClient; + + @JsonProperty("sentToClientAsTopMatch") + Long sentToClientAsTopMatch; + + @JsonProperty("lostToLineItems") + Set lostToLineItems; + + Set events; + + @JsonProperty("deliverySchedule") + Set deliverySchedule; + + @JsonProperty("readyAt") + String readyAt; + + @JsonProperty("spentTokens") + Long spentTokens; + + @JsonProperty("pacingFrequency") + Long pacingFrequency; +} diff --git a/src/main/java/org/prebid/server/deals/proto/report/LineItemStatusReport.java b/src/main/java/org/prebid/server/deals/proto/report/LineItemStatusReport.java new file mode 100644 index 00000000000..65c05e5c326 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/report/LineItemStatusReport.java @@ -0,0 +1,27 @@ +package org.prebid.server.deals.proto.report; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.time.ZonedDateTime; + +@Builder +@Value +public class LineItemStatusReport { + + @JsonProperty("lineItemId") + String lineItemId; + + @JsonProperty("deliverySchedule") + DeliverySchedule deliverySchedule; + + @JsonProperty("spentTokens") + Long spentTokens; + + @JsonProperty("readyToServeTimestamp") + ZonedDateTime readyToServeTimestamp; + + @JsonProperty("pacingFrequency") + Long pacingFrequency; +} diff --git a/src/main/java/org/prebid/server/deals/proto/report/LostToLineItem.java b/src/main/java/org/prebid/server/deals/proto/report/LostToLineItem.java new file mode 100644 index 00000000000..137f3cc8f4a --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/report/LostToLineItem.java @@ -0,0 +1,18 @@ +package org.prebid.server.deals.proto.report; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class LostToLineItem { + + @JsonProperty("lineItemSource") + String lineItemSource; + + @JsonProperty("lineItemId") + String lineItemId; + + Long count; +} diff --git a/src/main/java/org/prebid/server/deals/proto/report/Token.java b/src/main/java/org/prebid/server/deals/proto/report/Token.java new file mode 100644 index 00000000000..259ca65fc51 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/proto/report/Token.java @@ -0,0 +1,20 @@ +package org.prebid.server.deals.proto.report; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class Token { + + @JsonProperty("class") + Integer priorityClass; + + Integer total; + + Long spent; + + @JsonProperty("totalSpent") + Long totalSpent; +} diff --git a/src/main/java/org/prebid/server/deals/simulation/DealsSimulationAdminHandler.java b/src/main/java/org/prebid/server/deals/simulation/DealsSimulationAdminHandler.java new file mode 100644 index 00000000000..1bb14d329e3 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/simulation/DealsSimulationAdminHandler.java @@ -0,0 +1,164 @@ +package org.prebid.server.deals.simulation; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import io.vertx.ext.web.RoutingContext; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.HttpUtil; + +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DealsSimulationAdminHandler implements Handler { + + private static final TypeReference> BID_RATES_TYPE_REFERENCE + = new TypeReference>() { + }; + + private static final Logger logger = LoggerFactory.getLogger(DealsSimulationAdminHandler.class); + + private static final Pattern URL_SUFFIX_PATTERN = Pattern.compile("/pbs-admin/e2eAdmin(.*)"); + private static final String PLANNER_REGISTER_PATH = "/planner/register"; + private static final String PLANNER_FETCH_PATH = "/planner/fetchLineItems"; + private static final String ADVANCE_PLAN_PATH = "/advancePlans"; + private static final String REPORT_PATH = "/dealstats/report"; + private static final String BID_RATE_PATH = "/bidRate"; + private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; + + private final SimulationAwareRegisterService registerService; + private final SimulationAwarePlannerService plannerService; + private final SimulationAwareDeliveryProgressService deliveryProgressService; + private final SimulationAwareDeliveryStatsService deliveryStatsService; + private final SimulationAwareHttpBidderRequester httpBidderRequester; + private final JacksonMapper mapper; + private final String endpoint; + + public DealsSimulationAdminHandler( + SimulationAwareRegisterService registerService, + SimulationAwarePlannerService plannerService, + SimulationAwareDeliveryProgressService deliveryProgressService, + SimulationAwareDeliveryStatsService deliveryStatsService, + SimulationAwareHttpBidderRequester httpBidderRequester, + JacksonMapper mapper, + String endpoint) { + + this.registerService = Objects.requireNonNull(registerService); + this.plannerService = Objects.requireNonNull(plannerService); + this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); + this.deliveryStatsService = Objects.requireNonNull(deliveryStatsService); + this.httpBidderRequester = httpBidderRequester; + this.mapper = Objects.requireNonNull(mapper); + this.endpoint = Objects.requireNonNull(endpoint); + } + + @Override + public void handle(RoutingContext routingContext) { + final HttpServerRequest request = routingContext.request(); + final Matcher matcher = URL_SUFFIX_PATTERN.matcher(request.uri()); + + if (!matcher.find() || StringUtils.isBlank(matcher.group(1))) { + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.NOT_FOUND.code()) + .end("Requested url was not found")); + return; + } + + try { + final String endpointPath = matcher.group(1); + final ZonedDateTime now = getPgSimDate(endpointPath, request.headers()); + handleEndpoint(routingContext, endpointPath, now); + + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.OK.code()) + .end()); + } catch (InvalidRequestException e) { + logger.error(e.getMessage(), e); + respondWith(routingContext, HttpResponseStatus.BAD_REQUEST, e.getMessage()); + } catch (NotFoundException e) { + logger.error(e.getMessage(), e); + respondWith(routingContext, HttpResponseStatus.NOT_FOUND, e.getMessage()); + } catch (Exception e) { + logger.error(e.getMessage(), e); + respondWith(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + private ZonedDateTime getPgSimDate(String endpointPath, MultiMap headers) { + ZonedDateTime now = null; + if (!endpointPath.equals(BID_RATE_PATH)) { + now = HttpUtil.getDateFromHeader(headers, PG_SIM_TIMESTAMP); + if (now == null) { + throw new InvalidRequestException(String.format( + "pg-sim-timestamp with simulated current date is required for endpoints: %s, %s, %s, %s", + PLANNER_REGISTER_PATH, PLANNER_FETCH_PATH, ADVANCE_PLAN_PATH, REPORT_PATH)); + } + } + return now; + } + + private void handleEndpoint(RoutingContext routingContext, String endpointPath, ZonedDateTime now) { + if (endpointPath.startsWith(PLANNER_REGISTER_PATH)) { + registerService.performRegistration(now); + + } else if (endpointPath.startsWith(PLANNER_FETCH_PATH)) { + plannerService.initiateLineItemsFetching(now); + + } else if (endpointPath.startsWith(ADVANCE_PLAN_PATH)) { + plannerService.advancePlans(now); + + } else if (endpointPath.startsWith(REPORT_PATH)) { + deliveryProgressService.createDeliveryProgressReport(now); + deliveryStatsService.sendDeliveryProgressReports(now); + + } else if (endpointPath.startsWith(BID_RATE_PATH)) { + if (httpBidderRequester != null) { + handleBidRatesEndpoint(routingContext); + } else { + throw new InvalidRequestException(String.format("Calling %s is not make sense since " + + "Prebid Server configured to use real bidder exchanges in simulation mode", BID_RATE_PATH)); + } + } else { + throw new NotFoundException(String.format("Requested url %s was not found", endpointPath)); + } + } + + private void handleBidRatesEndpoint(RoutingContext routingContext) { + final Buffer body = routingContext.getBody(); + if (body == null) { + throw new InvalidRequestException(String.format("Body is required for %s endpoint", BID_RATE_PATH)); + } + + try { + httpBidderRequester.setBidRates(mapper.decodeValue(body, BID_RATES_TYPE_REFERENCE)); + } catch (DecodeException e) { + throw new InvalidRequestException(String.format("Failed to parse bid rates body: %s", e.getMessage())); + } + } + + private void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(status.code()) + .end(body)); + } + + private static class NotFoundException extends RuntimeException { + NotFoundException(String message) { + super(message); + } + } +} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryProgressService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryProgressService.java new file mode 100644 index 00000000000..fce81110c8b --- /dev/null +++ b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryProgressService.java @@ -0,0 +1,74 @@ +package org.prebid.server.deals.simulation; + +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.deals.DeliveryProgressReportFactory; +import org.prebid.server.deals.DeliveryProgressService; +import org.prebid.server.deals.DeliveryStatsService; +import org.prebid.server.deals.LineItemService; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.model.DeliveryProgressProperties; +import org.prebid.server.log.CriteriaLogManager; +import org.prebid.server.util.HttpUtil; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.Map; + +public class SimulationAwareDeliveryProgressService extends DeliveryProgressService { + + private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; + + private long readyAtAdjustment; + private volatile boolean firstReportUpdate; + + public SimulationAwareDeliveryProgressService(DeliveryProgressProperties deliveryProgressProperties, + LineItemService lineItemService, + DeliveryStatsService deliveryStatsService, + DeliveryProgressReportFactory deliveryProgressReportFactory, + long readyAtAdjustment, + Clock clock, + CriteriaLogManager criteriaLogManager) { + + super( + deliveryProgressProperties, + lineItemService, + deliveryStatsService, + deliveryProgressReportFactory, + clock, + criteriaLogManager); + this.readyAtAdjustment = readyAtAdjustment; + this.firstReportUpdate = true; + } + + @Override + public void shutdown() { + // disable sending report during bean destroying process + } + + @Override + public void processAuctionEvent(AuctionContext auctionContext) { + final ZonedDateTime now = HttpUtil.getDateFromHeader(auctionContext.getHttpRequest().getHeaders(), + PG_SIM_TIMESTAMP); + if (firstReportUpdate) { + firstReportUpdate = false; + updateDeliveryProgressesStartTime(now); + } + super.processAuctionEvent(auctionContext.getTxnLog(), auctionContext.getAccount().getId(), now); + } + + protected void incrementTokens(LineItem lineItem, ZonedDateTime now, Map planIdToTokenPriority) { + final Integer classPriority = lineItem.incSpentToken(now, readyAtAdjustment); + if (classPriority != null) { + planIdToTokenPriority.put(lineItem.getActiveDeliveryPlan().getPlanId(), classPriority); + } + } + + private void updateDeliveryProgressesStartTime(ZonedDateTime now) { + overallDeliveryProgress.setStartTimeStamp(now); + currentDeliveryProgress.setStartTimeStamp(now); + } + + void createDeliveryProgressReport(ZonedDateTime now) { + createDeliveryProgressReports(now); + } +} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryStatsService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryStatsService.java new file mode 100644 index 00000000000..7debdb649ed --- /dev/null +++ b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareDeliveryStatsService.java @@ -0,0 +1,52 @@ +package org.prebid.server.deals.simulation; + +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import org.prebid.server.deals.AlertHttpService; +import org.prebid.server.deals.DeliveryProgressReportFactory; +import org.prebid.server.deals.DeliveryStatsService; +import org.prebid.server.deals.model.DeliveryStatsProperties; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.Metrics; +import org.prebid.server.vertx.http.HttpClient; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; + +public class SimulationAwareDeliveryStatsService extends DeliveryStatsService { + + private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .toFormatter(); + + private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; + + public SimulationAwareDeliveryStatsService(DeliveryStatsProperties deliveryStatsProperties, + DeliveryProgressReportFactory deliveryProgressReportFactory, + AlertHttpService alertHttpService, + HttpClient httpClient, + Metrics metrics, + Clock clock, + Vertx vertx, + JacksonMapper mapper) { + super(deliveryStatsProperties, + deliveryProgressReportFactory, + alertHttpService, + httpClient, + metrics, + clock, + vertx, + mapper); + } + + @Override + protected Future sendReport(DeliveryProgressReport deliveryProgressReport, MultiMap headers, + ZonedDateTime now) { + return super.sendReport(deliveryProgressReport, + headers().add(PG_SIM_TIMESTAMP, UTC_MILLIS_FORMATTER.format(now)), now); + } +} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareHttpBidderRequester.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareHttpBidderRequester.java new file mode 100644 index 00000000000..3875f406fe1 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareHttpBidderRequester.java @@ -0,0 +1,170 @@ +package org.prebid.server.deals.simulation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import io.vertx.core.Future; +import lombok.AllArgsConstructor; +import lombok.Value; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.BidderErrorNotifier; +import org.prebid.server.bidder.BidderRequestCompletionTrackerFactory; +import org.prebid.server.bidder.HttpBidderRequestEnricher; +import org.prebid.server.bidder.HttpBidderRequester; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.deals.LineItemService; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.Timeout; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.proto.openrtb.ext.request.ExtDeal; +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.vertx.http.HttpClient; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class SimulationAwareHttpBidderRequester extends HttpBidderRequester { + + private static final BigDecimal DEFAULT_CPM = BigDecimal.ONE; + private static final String DEFAULT_ADM = ""; + private static final String DEFAULT_CRID = "crid"; + private static final String DEFAULT_CURRENCY = "USD"; + private static final String BID_ID_FORMAT = "%s-%s"; + + private Map bidRates; + private LineItemService lineItemService; + private final JacksonMapper mapper; + + public SimulationAwareHttpBidderRequester( + HttpClient httpClient, + BidderRequestCompletionTrackerFactory bidderRequestCompletionTrackerFactory, + BidderErrorNotifier bidderErrorNotifier, + HttpBidderRequestEnricher requestEnricher, + LineItemService lineItemService, + JacksonMapper mapper) { + + super(httpClient, bidderRequestCompletionTrackerFactory, bidderErrorNotifier, requestEnricher); + + this.lineItemService = Objects.requireNonNull(lineItemService); + this.mapper = Objects.requireNonNull(mapper); + this.bidRates = new HashMap<>(); + } + + void setBidRates(Map bidRates) { + this.bidRates.putAll(bidRates); + } + + @Override + public Future requestBids(Bidder bidder, + BidderRequest bidderRequest, + Timeout timeout, + CaseInsensitiveMultiMap requestHeaders, + boolean debugEnabled) { + + final List imps = bidderRequest.getBidRequest().getImp(); + final Map idToImps = imps.stream().collect(Collectors.toMap(Imp::getId, Function.identity())); + final Map> impsToDealInfo = imps.stream() + .filter(imp -> imp.getPmp() != null) + .collect(Collectors.toMap(Imp::getId, imp -> imp.getPmp().getDeals().stream() + .map(deal -> DealInfo.of(deal.getId(), getLineItemId(deal))) + .filter(dealInfo -> dealInfo.getLineItemId() != null) + .collect(Collectors.toSet()))); + + if (impsToDealInfo.values().stream().noneMatch(CollectionUtils::isNotEmpty)) { + return Future.succeededFuture(BidderSeatBid.of(Collections.emptyList(), Collections.emptyList(), + Collections.singletonList(BidderError.failedToRequestBids( + "Matched or ready to serve line items were not found, but required in simulation mode")))); + } + + final List bidderBids = impsToDealInfo.entrySet().stream() + .flatMap(impToDealInfo -> impToDealInfo.getValue() + .stream() + .map(dealInfo -> createBid(idToImps.get(impToDealInfo.getKey()), dealInfo.getDealId(), + dealInfo.getLineItemId())) + .filter(Objects::nonNull)) + .map(bid -> BidderBid.of(bid, BidType.banner, DEFAULT_CURRENCY)) + .collect(Collectors.toList()); + + return Future.succeededFuture(BidderSeatBid.of(bidderBids, Collections.emptyList(), Collections.emptyList())); + } + + private String getLineItemId(Deal deal) { + final JsonNode extDealNode = deal.getExt(); + final ExtDeal extDeal = extDealNode != null ? getExtDeal(extDealNode) : null; + final ExtDealLine extDealLine = extDeal != null ? extDeal.getLine() : null; + return extDealLine != null ? extDealLine.getLineItemId() : null; + } + + private Bid createBid(Imp imp, String dealId, String lineItemId) { + final Double rate = bidRates.get(lineItemId); + if (rate == null) { + throw new PreBidException(String.format("Bid rate for line item with id %s was not found", lineItemId)); + } + final String impId = imp.getId(); + final LineItem lineItem = lineItemService.getLineItemById(lineItemId); + final List sizes = getLineItemSizes(imp); + return Math.random() < rate + ? Bid.builder() + .id(String.format(BID_ID_FORMAT, impId, lineItemId)) + .impid(impId) + .dealid(dealId) + .price(lineItem != null ? lineItem.getCpm() : DEFAULT_CPM) + .adm(DEFAULT_ADM) + .crid(DEFAULT_CRID) + .w(sizes.isEmpty() ? 0 : sizes.get(0).getW()) + .h(sizes.isEmpty() ? 0 : sizes.get(0).getH()) + .build() + : null; + } + + private List getLineItemSizes(Imp imp) { + return imp.getPmp().getDeals().stream() + .map(Deal::getExt) + .filter(Objects::nonNull) + .map(this::getExtDeal) + .filter(Objects::nonNull) + .map(ExtDeal::getLine) + .filter(Objects::nonNull) + .map(ExtDealLine::getSizes) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private ExtDeal getExtDeal(JsonNode extDeal) { + try { + return mapper.mapper().treeToValue(extDeal, ExtDeal.class); + } catch (JsonProcessingException e) { + throw new PreBidException( + String.format("Error decoding bidRequest.imp.pmp.deal.ext: %s", e.getMessage()), e); + } + } + + @Value + @AllArgsConstructor(staticName = "of") + private static class DealInfo { + + String dealId; + + String lineItemId; + } +} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareLineItemService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareLineItemService.java new file mode 100644 index 00000000000..016d9db3f53 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareLineItemService.java @@ -0,0 +1,45 @@ +package org.prebid.server.deals.simulation; + +import com.iab.openrtb.request.Imp; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.deals.LineItemService; +import org.prebid.server.deals.TargetingService; +import org.prebid.server.deals.events.ApplicationEventService; +import org.prebid.server.deals.model.MatchLineItemsResult; +import org.prebid.server.log.CriteriaLogManager; +import org.prebid.server.util.HttpUtil; +import org.springframework.beans.factory.annotation.Value; + +import java.time.Clock; + +public class SimulationAwareLineItemService extends LineItemService { + + private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; + + public SimulationAwareLineItemService(int maxDealsPerBidder, + TargetingService targetingService, + BidderCatalog bidderCatalog, + CurrencyConversionService conversionService, + ApplicationEventService applicationEventService, + @Value("${auction.ad-server-currency}}") String adServerCurrency, + Clock clock, + CriteriaLogManager criteriaLogManager) { + + super(maxDealsPerBidder, targetingService, bidderCatalog, conversionService, applicationEventService, + adServerCurrency, clock, criteriaLogManager); + } + + @Override + public boolean accountHasDeals(AuctionContext auctionContext) { + return accountHasDeals(auctionContext.getAccount().getId(), + HttpUtil.getDateFromHeader(auctionContext.getHttpRequest().getHeaders(), PG_SIM_TIMESTAMP)); + } + + @Override + public MatchLineItemsResult findMatchingLineItems(AuctionContext auctionContext, Imp imp) { + return findMatchingLineItems(auctionContext, imp, + HttpUtil.getDateFromHeader(auctionContext.getHttpRequest().getHeaders(), PG_SIM_TIMESTAMP)); + } +} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwarePlannerService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwarePlannerService.java new file mode 100644 index 00000000000..9ad770b96f4 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/simulation/SimulationAwarePlannerService.java @@ -0,0 +1,107 @@ +package org.prebid.server.deals.simulation; + +import io.vertx.core.AsyncResult; +import io.vertx.core.MultiMap; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.deals.AlertHttpService; +import org.prebid.server.deals.DeliveryProgressService; +import org.prebid.server.deals.PlannerService; +import org.prebid.server.deals.model.AlertPriority; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.deals.model.PlannerProperties; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.Metrics; +import org.prebid.server.vertx.http.HttpClient; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public class SimulationAwarePlannerService extends PlannerService { + + private static final Logger logger = LoggerFactory.getLogger(SimulationAwarePlannerService.class); + private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .toFormatter(); + + private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; + private static final String PBS_PLANNER_CLIENT_ERROR = "pbs-planner-client-error"; + + private final SimulationAwareLineItemService lineItemService; + private final Metrics metrics; + private final AlertHttpService alertHttpService; + + private List lineItemMetaData; + + public SimulationAwarePlannerService(PlannerProperties plannerProperties, + DeploymentProperties deploymentProperties, + SimulationAwareLineItemService lineItemService, + DeliveryProgressService deliveryProgressService, + AlertHttpService alertHttpService, + HttpClient httpClient, + Metrics metrics, + Clock clock, + JacksonMapper mapper) { + super( + plannerProperties, + deploymentProperties, + lineItemService, + deliveryProgressService, + alertHttpService, + httpClient, + metrics, + clock, + mapper); + + this.lineItemService = Objects.requireNonNull(lineItemService); + this.alertHttpService = Objects.requireNonNull(alertHttpService); + this.metrics = Objects.requireNonNull(metrics); + this.lineItemMetaData = new ArrayList<>(); + } + + public void advancePlans(ZonedDateTime now) { + lineItemService.updateLineItems(lineItemMetaData, isPlannerResponsive.get(), now); + lineItemService.advanceToNextPlan(now); + } + + public void initiateLineItemsFetching(ZonedDateTime now) { + fetchLineItemMetaData(planEndpoint, headers(now)) + .setHandler(this::handleInitializationResult); + } + + /** + * Handles result of initialization process. Sets metadata if request was successful. + */ + @Override + protected void handleInitializationResult(AsyncResult> plannerResponse) { + if (plannerResponse.succeeded()) { + metrics.updatePlannerRequestMetric(true); + isPlannerResponsive.set(true); + lineItemService.updateIsPlannerResponsive(true); + lineItemMetaData = plannerResponse.result(); + } else { + alert(plannerResponse.cause().getMessage(), AlertPriority.HIGH, logger::warn); + logger.warn("Failed to retrieve line items from Planner after retry. Reason: {0}", + plannerResponse.cause().getMessage()); + isPlannerResponsive.set(false); + lineItemService.updateIsPlannerResponsive(false); + metrics.updatePlannerRequestMetric(false); + } + } + + private MultiMap headers(ZonedDateTime now) { + return headers().add(PG_SIM_TIMESTAMP, UTC_MILLIS_FORMATTER.format(now)); + } + + private void alert(String message, AlertPriority alertPriority, Consumer logger) { + alertHttpService.alert(PBS_PLANNER_CLIENT_ERROR, alertPriority, message); + logger.accept(message); + } +} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareRegisterService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareRegisterService.java new file mode 100644 index 00000000000..d185285937e --- /dev/null +++ b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareRegisterService.java @@ -0,0 +1,61 @@ +package org.prebid.server.deals.simulation; + +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.deals.AlertHttpService; +import org.prebid.server.deals.DeliveryProgressService; +import org.prebid.server.deals.RegisterService; +import org.prebid.server.deals.events.AdminEventService; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.deals.model.PlannerProperties; +import org.prebid.server.health.HealthMonitor; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.vertx.http.HttpClient; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; + +public class SimulationAwareRegisterService extends RegisterService { + + private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .toFormatter(); + private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; + + public SimulationAwareRegisterService(PlannerProperties plannerProperties, + DeploymentProperties deploymentProperties, + AdminEventService adminEventService, + DeliveryProgressService deliveryProgressService, + AlertHttpService alertHttpService, + HealthMonitor healthMonitor, + CurrencyConversionService currencyConversionService, + HttpClient httpClient, + Vertx vertx, + JacksonMapper mapper) { + super(plannerProperties, + deploymentProperties, + adminEventService, + deliveryProgressService, + alertHttpService, + healthMonitor, + currencyConversionService, + httpClient, + vertx, + mapper); + } + + @Override + public void initialize() { + // disable timer initialization for simulation mode + } + + public void performRegistration(ZonedDateTime now) { + register(headers(now)); + } + + private MultiMap headers(ZonedDateTime now) { + return headers().add(PG_SIM_TIMESTAMP, UTC_MILLIS_FORMATTER.format(now)); + } +} diff --git a/src/main/java/org/prebid/server/deals/simulation/SimulationAwareUserService.java b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareUserService.java new file mode 100644 index 00000000000..78d9f8ae409 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/simulation/SimulationAwareUserService.java @@ -0,0 +1,57 @@ +package org.prebid.server.deals.simulation; + +import io.vertx.core.Future; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.deals.LineItemService; +import org.prebid.server.deals.UserService; +import org.prebid.server.deals.model.SimulationProperties; +import org.prebid.server.deals.model.UserDetails; +import org.prebid.server.deals.model.UserDetailsProperties; +import org.prebid.server.execution.Timeout; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.metric.Metrics; +import org.prebid.server.vertx.http.HttpClient; + +import java.time.Clock; + +public class SimulationAwareUserService extends UserService { + + private final boolean winEventsEnabled; + private final boolean userDetailsEnabled; + + public SimulationAwareUserService(UserDetailsProperties userDetailsProperties, + SimulationProperties simulationProperties, + String dataCenterRegion, + LineItemService lineItemService, + HttpClient httpClient, + Clock clock, + Metrics metrics, + JacksonMapper mapper) { + super( + userDetailsProperties, + dataCenterRegion, + lineItemService, + httpClient, + clock, + metrics, + mapper); + + this.winEventsEnabled = simulationProperties.isWinEventsEnabled(); + this.userDetailsEnabled = simulationProperties.isUserDetailsEnabled(); + } + + @Override + public Future getUserDetails(AuctionContext context, Timeout timeout) { + return userDetailsEnabled + ? super.getUserDetails(context, timeout) + : Future.succeededFuture(UserDetails.empty()); + } + + @Override + public void processWinEvent(String lineItemId, String bidId, UidsCookie uids) { + if (winEventsEnabled) { + super.processWinEvent(lineItemId, bidId, uids); + } + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/RequestContext.java b/src/main/java/org/prebid/server/deals/targeting/RequestContext.java new file mode 100644 index 00000000000..55bd6d2e6a2 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/RequestContext.java @@ -0,0 +1,394 @@ +package org.prebid.server.deals.targeting; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.deals.targeting.model.GeoLocation; +import org.prebid.server.deals.targeting.model.Size; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; +import org.prebid.server.exception.TargetingSyntaxException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.FlexibleExtension; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtSite; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.util.ObjectUtils; +import org.prebid.server.util.StreamUtil; + +import java.beans.BeanInfo; +import java.beans.FeatureDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class RequestContext { + + private final BidRequest bidRequest; + private final Imp imp; + private final TxnLog txnLog; + private final ObjectNode deviceExt; + private final ObjectNode geoExt; + private final ObjectNode userExt; + + private final AttributeReader userAttributeReader = AttributeReader.forUser(); + private final AttributeReader siteAttributeReader = AttributeReader.forSite(); + private final AttributeReader appAttributeReader = AttributeReader.forApp(); + private final AttributeReader impContextDataAttributeReader = AttributeReader.forImpContextData(); + private final AttributeReader impBidderAttributeReader = AttributeReader.forImpBidder(); + + public RequestContext(BidRequest bidRequest, Imp imp, TxnLog txnLog, JacksonMapper mapper) { + this.bidRequest = Objects.requireNonNull(bidRequest); + this.imp = Objects.requireNonNull(imp); + this.deviceExt = getExtNode(bidRequest.getDevice(), Device::getExt, mapper); + this.geoExt = getExtNode(bidRequest.getDevice(), + dev -> getIfNotNull(getIfNotNull(dev, Device::getGeo), Geo::getExt), mapper); + this.userExt = getExtNode(bidRequest.getUser(), User::getExt, mapper); + this.txnLog = Objects.requireNonNull(txnLog); + } + + private static ObjectNode getExtNode(T target, Function extExtractor, + JacksonMapper mapper) { + final FlexibleExtension ext = target != null ? extExtractor.apply(target) : null; + return ext != null ? (ObjectNode) mapper.mapper().valueToTree(ext) : null; + } + + public String lookupString(TargetingCategory category) { + final TargetingCategory.Type type = category.type(); + switch (type) { + case domain: + return org.apache.commons.lang3.ObjectUtils.firstNonNull( + getIfNotNull(getIfNotNull(bidRequest.getSite(), Site::getPublisher), Publisher::getDomain), + getIfNotNull(bidRequest.getSite(), Site::getDomain)); + case referrer: + return getIfNotNull(bidRequest.getSite(), Site::getPage); + case appBundle: + return getIfNotNull(bidRequest.getApp(), App::getBundle); + case adslot: + return getFirstNonNullStringFromImpExt( + "context.data.pbadslot", + "context.data.adserver.adslot", + "data.pbadslot", + "data.adserver.adslot"); + case deviceGeoExt: + return getValueFrom(geoExt, category, RequestContext::nodeToString); + case deviceExt: + return getValueFrom(deviceExt, category, RequestContext::nodeToString); + case bidderParam: + return impBidderAttributeReader.readFromExt(imp, category, RequestContext::nodeToString); + case userFirstPartyData: + return userAttributeReader.read( + bidRequest.getUser(), category, RequestContext::nodeToString, String.class); + case siteFirstPartyData: + return getSiteFirstPartyData(category, RequestContext::nodeToString); + default: + throw new TargetingSyntaxException( + String.format("Unexpected category for fetching string value for: %s", type)); + } + } + + public Integer lookupInteger(TargetingCategory category) { + final TargetingCategory.Type type = category.type(); + switch (type) { + case pagePosition: + return getIfNotNull(getIfNotNull(imp, Imp::getBanner), Banner::getPos); + case dow: + return getIntegerFromUserExt("time.userdow"); + case hour: + return getIntegerFromUserExt("time.userhour"); + case bidderParam: + return impBidderAttributeReader.readFromExt(imp, category, RequestContext::nodeToInteger); + case userFirstPartyData: + return userAttributeReader.read( + bidRequest.getUser(), category, RequestContext::nodeToInteger, Integer.class); + case siteFirstPartyData: + return getSiteFirstPartyData(category, RequestContext::nodeToInteger); + default: + throw new TargetingSyntaxException( + String.format("Unexpected category for fetching integer value for: %s", type)); + } + } + + public List lookupStrings(TargetingCategory category) { + final TargetingCategory.Type type = category.type(); + switch (type) { + case mediaType: + return getMediaTypes(); + case bidderParam: + return impBidderAttributeReader + .readFromExt(imp, category, node -> nodeToList(node, RequestContext::nodeToString)); + case userSegment: + return getSegments(category); + case userFirstPartyData: + return userAttributeReader.readFromExt( + bidRequest.getUser(), category, node -> nodeToList(node, RequestContext::nodeToString)); + case siteFirstPartyData: + return getSiteFirstPartyData(category, node -> nodeToList(node, RequestContext::nodeToString)); + default: + throw new TargetingSyntaxException( + String.format("Unexpected category for fetching string values for: %s", type)); + } + } + + public List lookupIntegers(TargetingCategory category) { + final TargetingCategory.Type type = category.type(); + switch (type) { + case bidderParam: + return impBidderAttributeReader + .readFromExt(imp, category, node -> nodeToList(node, RequestContext::nodeToInteger)); + case userFirstPartyData: + return userAttributeReader.readFromExt( + bidRequest.getUser(), category, node -> nodeToList(node, RequestContext::nodeToInteger)); + case siteFirstPartyData: + return getSiteFirstPartyData(category, node -> nodeToList(node, RequestContext::nodeToInteger)); + default: + throw new TargetingSyntaxException( + String.format("Unexpected category for fetching integer values for: %s", type)); + } + } + + public List lookupSizes(TargetingCategory category) { + final TargetingCategory.Type type = category.type(); + if (type != TargetingCategory.Type.size) { + throw new TargetingSyntaxException( + String.format("Unexpected category for fetching sizes for: %s", type)); + } + + return ListUtils.emptyIfNull(getIfNotNull(getIfNotNull(imp, Imp::getBanner), Banner::getFormat)) + .stream() + .map(format -> Size.of(format.getW(), format.getH())) + .collect(Collectors.toList()); + } + + public GeoLocation lookupGeoLocation(TargetingCategory category) { + final TargetingCategory.Type type = category.type(); + if (type != TargetingCategory.Type.location) { + throw new TargetingSyntaxException( + String.format("Unexpected category for fetching geo location for: %s", type)); + } + + final Geo geo = getIfNotNull(getIfNotNull(bidRequest, BidRequest::getDevice), Device::getGeo); + final Float lat = getIfNotNull(geo, Geo::getLat); + final Float lon = getIfNotNull(geo, Geo::getLon); + + return lat != null && lon != null ? GeoLocation.of(lat, lon) : null; + } + + public TxnLog txnLog() { + return txnLog; + } + + private String getFirstNonNullStringFromImpExt(String... path) { + return Arrays.stream(path).map(this::getStringFromImpExt).filter(Objects::nonNull).findFirst().orElse(null); + } + + private String getStringFromImpExt(String path) { + return getIfNotNull(getIfNotNull(imp, Imp::getExt), + node -> nodeToString(node.at(toJsonPointer(path)))); + } + + private static T getIfNotNull(S source, Function getter) { + return source != null ? getter.apply(source) : null; + } + + private Integer getIntegerFromUserExt(String path) { + return getIfNotNull(getIfNotNull(userExt, node -> node.at(toJsonPointer(path))), + RequestContext::nodeToInteger); + } + + private List getMediaTypes() { + final List mediaTypes = new ArrayList<>(); + if (imp.getBanner() != null) { + mediaTypes.add("banner"); + } + if (imp.getVideo() != null) { + mediaTypes.add("video"); + } + if (imp.getXNative() != null) { + mediaTypes.add("native"); + } + return mediaTypes; + } + + private static T getValueFrom(ObjectNode objectNode, TargetingCategory category, + Function valueExtractor) { + final JsonNode jsonNode = getIfNotNull(objectNode, node -> node.at(toJsonPointer(category.path()))); + return getIfNotNull(jsonNode, valueExtractor); + } + + private T getSiteFirstPartyData(TargetingCategory category, Function valueExtractor) { + return ObjectUtils.firstNonNull( + () -> impContextDataAttributeReader.readFromExt(imp, category, valueExtractor), + () -> siteAttributeReader.readFromExt(bidRequest.getSite(), category, valueExtractor), + () -> appAttributeReader.readFromExt(bidRequest.getApp(), category, valueExtractor)); + } + + public List getSegments(TargetingCategory category) { + return ListUtils.emptyIfNull(getIfNotNull(getIfNotNull(bidRequest, BidRequest::getUser), User::getData)) + .stream() + .filter(Objects::nonNull) + .filter(data -> Objects.equals(data.getId(), category.path())) + .flatMap(data -> ListUtils.emptyIfNull(data.getSegment()).stream()) + .map(Segment::getId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static String toJsonPointer(String path) { + return Arrays.stream(path.split("\\.")).collect(Collectors.joining("/", "/", StringUtils.EMPTY)); + } + + private static String nodeToString(JsonNode node) { + return node.isTextual() ? node.asText() : null; + } + + private static Integer nodeToInteger(JsonNode node) { + return node.isInt() ? node.asInt() : null; + } + + private static List nodeToList(JsonNode node, Function valueExtractor) { + if (node.isArray()) { + return StreamUtil.asStream(node.spliterator()) + .map(valueExtractor) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } + + private static class AttributeReader { + + private static final Set> SUPPORTED_PROPERTY_TYPES = new HashSet<>(Arrays.asList( + String.class, Integer.class, int.class)); + + private static final String EXT_PREBID = "prebid"; + private static final String EXT_BIDDER = "bidder"; + private static final String EXT_DATA = "data"; + private static final String EXT_CONTEXT = "context"; + + private final Map properties; + private final Function extPathExtractor; + + private AttributeReader(Class type, Function extPathExtractor) { + this.properties = supportedBeanProperties(type); + this.extPathExtractor = extPathExtractor; + } + + public static AttributeReader forUser() { + return new AttributeReader<>( + User.class, + user -> getIfNotNull(getIfNotNull(user, User::getExt), ExtUser::getData)); + } + + public static AttributeReader forSite() { + return new AttributeReader<>( + Site.class, + site -> getIfNotNull(getIfNotNull(site, Site::getExt), ExtSite::getData)); + } + + public static AttributeReader forApp() { + return new AttributeReader<>( + App.class, + app -> getIfNotNull(getIfNotNull(app, App::getExt), ExtApp::getData)); + } + + public static AttributeReader forImpContextData() { + return new AttributeReader<>( + Imp.class, + imp -> getIfNotNull(getIfNotNull(getIfNotNull( + imp, + Imp::getExt), + node -> node.get(EXT_CONTEXT)), + node -> node.get(EXT_DATA))); + } + + public static AttributeReader forImpBidder() { + return new AttributeReader<>( + Imp.class, + imp -> getIfNotNull(getIfNotNull(getIfNotNull( + imp, + Imp::getExt), + node -> node.get(EXT_PREBID)), + node -> node.get(EXT_BIDDER))); + } + + public A read( + T target, TargetingCategory category, Function valueExtractor, Class attributeType) { + + return ObjectUtils.firstNonNull( + // look in the object itself + () -> readFromObject(target, category, attributeType), + // then examine ext if value not found on top level or if it is nested attribute + () -> readFromExt(target, category, valueExtractor)); + } + + public A readFromObject(T target, TargetingCategory category, Class attributeType) { + if (isTopLevelAttribute(category.path())) { + return getIfNotNull(target, user -> readProperty(user, category.path(), attributeType)); + } + return null; + } + + public A readFromExt(T target, TargetingCategory category, Function valueExtractor) { + return getIfNotNull(getIfNotNull(getIfNotNull( + target, + extPathExtractor), + node -> node.at(toJsonPointer(category.path()))), + valueExtractor); + } + + private boolean isTopLevelAttribute(String path) { + return !path.contains("."); + } + + private static Map supportedBeanProperties(Class beanClass) { + try { + final BeanInfo beanInfo = Introspector.getBeanInfo(beanClass, Object.class); + return Arrays.stream(beanInfo.getPropertyDescriptors()) + .filter(pd -> SUPPORTED_PROPERTY_TYPES.contains(pd.getPropertyType())) + .collect(Collectors.toMap(FeatureDescriptor::getName, Function.identity())); + } catch (IntrospectionException e) { + return ExceptionUtils.rethrow(e); + } + } + + @SuppressWarnings("unchecked") + private A readProperty(T target, String path, Class attributeType) { + final PropertyDescriptor descriptor = properties.get(path); + + if (descriptor != null && descriptor.getPropertyType().equals(attributeType)) { + try { + return (A) descriptor.getReadMethod().invoke(target); + } catch (IllegalAccessException | InvocationTargetException e) { + // just ignore + } + } + + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/TargetingDefinition.java b/src/main/java/org/prebid/server/deals/targeting/TargetingDefinition.java new file mode 100644 index 00000000000..fe4f79eac78 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/TargetingDefinition.java @@ -0,0 +1,12 @@ +package org.prebid.server.deals.targeting; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.deals.targeting.interpret.Expression; + +@AllArgsConstructor(staticName = "of") +@Value +public class TargetingDefinition { + + private final Expression rootExpression; +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/And.java b/src/main/java/org/prebid/server/deals/targeting/interpret/And.java new file mode 100644 index 00000000000..bfe69b2882a --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/And.java @@ -0,0 +1,27 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; + +import java.util.Collections; +import java.util.List; + +@EqualsAndHashCode +public class And implements NonTerminalExpression { + + private final List expressions; + + public And(List expressions) { + this.expressions = Collections.unmodifiableList(expressions); + } + + @Override + public boolean matches(RequestContext context) { + for (final Expression expression : expressions) { + if (!expression.matches(context)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/DomainMetricAwareExpression.java b/src/main/java/org/prebid/server/deals/targeting/interpret/DomainMetricAwareExpression.java new file mode 100644 index 00000000000..9d682256384 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/DomainMetricAwareExpression.java @@ -0,0 +1,25 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; + +@EqualsAndHashCode +public class DomainMetricAwareExpression implements Expression { + + private final Expression domainFunction; + private final String lineItemId; + + public DomainMetricAwareExpression(Expression domainFunction, String lineItemId) { + this.domainFunction = domainFunction; + this.lineItemId = lineItemId; + } + + @Override + public boolean matches(RequestContext requestContext) { + final boolean matches = domainFunction.matches(requestContext); + if (matches) { + requestContext.txnLog().lineItemsMatchedDomainTargeting().add(lineItemId); + } + return matches; + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Expression.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Expression.java new file mode 100644 index 00000000000..332e6a2a2eb --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/Expression.java @@ -0,0 +1,8 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.prebid.server.deals.targeting.RequestContext; + +public interface Expression { + + boolean matches(RequestContext context); +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/In.java b/src/main/java/org/prebid/server/deals/targeting/interpret/In.java new file mode 100644 index 00000000000..3d056ff736b --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/In.java @@ -0,0 +1,29 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@EqualsAndHashCode +public abstract class In implements TerminalExpression { + + protected final TargetingCategory category; + + protected List values; + + public In(TargetingCategory category, List values) { + this.category = Objects.requireNonNull(category); + this.values = Collections.unmodifiableList(values); + } + + @Override + public boolean matches(RequestContext context) { + return values.contains(lookupActualValue(context)); + } + + protected abstract T lookupActualValue(RequestContext context); +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/InIntegers.java b/src/main/java/org/prebid/server/deals/targeting/interpret/InIntegers.java new file mode 100644 index 00000000000..4ac0f6c9241 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/InIntegers.java @@ -0,0 +1,20 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +public class InIntegers extends In { + + public InIntegers(TargetingCategory category, List values) { + super(category, values); + } + + @Override + public Integer lookupActualValue(RequestContext context) { + return context.lookupInteger(category); + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/InStrings.java b/src/main/java/org/prebid/server/deals/targeting/interpret/InStrings.java new file mode 100644 index 00000000000..9220de4c9f5 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/InStrings.java @@ -0,0 +1,27 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import java.util.List; +import java.util.stream.Collectors; + +@EqualsAndHashCode(callSuper = true) +public class InStrings extends In { + + public InStrings(TargetingCategory category, List values) { + super(category, toLowerCase(values)); + } + + @Override + public String lookupActualValue(RequestContext context) { + final String actualValue = context.lookupString(category); + + return actualValue != null ? actualValue.toLowerCase() : null; + } + + private static List toLowerCase(List values) { + return values.stream().map(String::toLowerCase).collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Intersects.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Intersects.java new file mode 100644 index 00000000000..a72a9c65727 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/Intersects.java @@ -0,0 +1,29 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@EqualsAndHashCode +public abstract class Intersects implements TerminalExpression { + + protected final TargetingCategory category; + + protected List values; + + public Intersects(TargetingCategory category, List values) { + this.category = Objects.requireNonNull(category); + this.values = Collections.unmodifiableList(values); + } + + @Override + public boolean matches(RequestContext context) { + return !Collections.disjoint(values, lookupActualValues(context)); + } + + protected abstract List lookupActualValues(RequestContext context); +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsIntegers.java b/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsIntegers.java new file mode 100644 index 00000000000..0103e740670 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsIntegers.java @@ -0,0 +1,20 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +public class IntersectsIntegers extends Intersects { + + public IntersectsIntegers(TargetingCategory category, List values) { + super(category, values); + } + + @Override + public List lookupActualValues(RequestContext context) { + return context.lookupIntegers(category); + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsSizes.java b/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsSizes.java new file mode 100644 index 00000000000..da713795234 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsSizes.java @@ -0,0 +1,21 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.model.Size; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +public class IntersectsSizes extends Intersects { + + public IntersectsSizes(TargetingCategory category, List values) { + super(category, values); + } + + @Override + public List lookupActualValues(RequestContext context) { + return context.lookupSizes(category); + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsStrings.java b/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsStrings.java new file mode 100644 index 00000000000..b0d5949b20c --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/IntersectsStrings.java @@ -0,0 +1,25 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import java.util.List; +import java.util.stream.Collectors; + +@EqualsAndHashCode(callSuper = true) +public class IntersectsStrings extends Intersects { + + public IntersectsStrings(TargetingCategory category, List values) { + super(category, toLowerCase(values)); + } + + @Override + public List lookupActualValues(RequestContext context) { + return toLowerCase(context.lookupStrings(category)); + } + + private static List toLowerCase(List values) { + return values.stream().map(String::toLowerCase).collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Matches.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Matches.java new file mode 100644 index 00000000000..d16c3a61f1a --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/Matches.java @@ -0,0 +1,44 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import java.util.Objects; +import java.util.function.BiFunction; + +@EqualsAndHashCode +public class Matches implements TerminalExpression { + + private static final String WILDCARD = "*"; + + private final TargetingCategory category; + + private BiFunction method; + + private String value; + + public Matches(TargetingCategory category, String value) { + this.category = Objects.requireNonNull(category); + this.method = resolveMethod(Objects.requireNonNull(value)); + this.value = value.replaceAll("\\*", "").toLowerCase(); + } + + @Override + public boolean matches(RequestContext context) { + final String valueToMatch = context.lookupString(category); + return valueToMatch != null && method.apply(valueToMatch.toLowerCase(), value); + } + + private static BiFunction resolveMethod(String value) { + if (value.startsWith(WILDCARD) && value.endsWith(WILDCARD)) { + return String::contains; + } else if (value.startsWith(WILDCARD)) { + return String::endsWith; + } else if (value.endsWith(WILDCARD)) { + return String::startsWith; + } else { + return String::equals; + } + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/NonTerminalExpression.java b/src/main/java/org/prebid/server/deals/targeting/interpret/NonTerminalExpression.java new file mode 100644 index 00000000000..ee9f4235a3d --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/NonTerminalExpression.java @@ -0,0 +1,4 @@ +package org.prebid.server.deals.targeting.interpret; + +public interface NonTerminalExpression extends Expression { +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Not.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Not.java new file mode 100644 index 00000000000..ed8f1a35875 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/Not.java @@ -0,0 +1,21 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; + +import java.util.Objects; + +@EqualsAndHashCode +public class Not implements NonTerminalExpression { + + private final Expression expression; + + public Not(Expression expression) { + this.expression = Objects.requireNonNull(expression); + } + + @Override + public boolean matches(RequestContext context) { + return !expression.matches(context); + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Or.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Or.java new file mode 100644 index 00000000000..a4740f889ad --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/Or.java @@ -0,0 +1,27 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; + +import java.util.Collections; +import java.util.List; + +@EqualsAndHashCode +public class Or implements NonTerminalExpression { + + private final List expressions; + + public Or(List expressions) { + this.expressions = Collections.unmodifiableList(expressions); + } + + @Override + public boolean matches(RequestContext context) { + for (final Expression expression : expressions) { + if (expression.matches(context)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/TerminalExpression.java b/src/main/java/org/prebid/server/deals/targeting/interpret/TerminalExpression.java new file mode 100644 index 00000000000..7c89885cc49 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/TerminalExpression.java @@ -0,0 +1,4 @@ +package org.prebid.server.deals.targeting.interpret; + +public interface TerminalExpression extends Expression { +} diff --git a/src/main/java/org/prebid/server/deals/targeting/interpret/Within.java b/src/main/java/org/prebid/server/deals/targeting/interpret/Within.java new file mode 100644 index 00000000000..1e2bf337b50 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/interpret/Within.java @@ -0,0 +1,49 @@ +package org.prebid.server.deals.targeting.interpret; + +import lombok.EqualsAndHashCode; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.model.GeoLocation; +import org.prebid.server.deals.targeting.model.GeoRegion; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import java.util.Objects; + +@EqualsAndHashCode +public class Within implements TerminalExpression { + + private static final int EARTH_RADIUS_MI = 3959; + + private final TargetingCategory category; + + private GeoRegion value; + + public Within(TargetingCategory category, GeoRegion value) { + this.category = Objects.requireNonNull(category); + this.value = Objects.requireNonNull(value); + } + + @Override + public boolean matches(RequestContext context) { + final GeoLocation location = context.lookupGeoLocation(category); + + return location != null && isLocationWithinRegion(location); + } + + private boolean isLocationWithinRegion(GeoLocation location) { + final double distance = calculateDistance(location.getLat(), location.getLon(), value.getLat(), value.getLon()); + + return value.getRadiusMiles() > distance; + } + + private static double calculateDistance(double startLat, double startLong, double endLat, double endLong) { + final double dLat = Math.toRadians(endLat - startLat); + final double dLong = Math.toRadians(endLong - startLong); + + double a = Math.pow(Math.sin(dLat / 2), 2) + + Math.cos(Math.toRadians(startLat)) * Math.cos(Math.toRadians(endLat)) + * Math.pow(Math.sin(dLong / 2), 2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return EARTH_RADIUS_MI * c; + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/model/GeoLocation.java b/src/main/java/org/prebid/server/deals/targeting/model/GeoLocation.java new file mode 100644 index 00000000000..4918d500e13 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/model/GeoLocation.java @@ -0,0 +1,13 @@ +package org.prebid.server.deals.targeting.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class GeoLocation { + + Float lat; + + Float lon; +} diff --git a/src/main/java/org/prebid/server/deals/targeting/model/GeoRegion.java b/src/main/java/org/prebid/server/deals/targeting/model/GeoRegion.java new file mode 100644 index 00000000000..f7ae09c5ffd --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/model/GeoRegion.java @@ -0,0 +1,17 @@ +package org.prebid.server.deals.targeting.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class GeoRegion { + + Float lat; + + Float lon; + + @JsonProperty("radiusMiles") + Float radiusMiles; +} diff --git a/src/main/java/org/prebid/server/deals/targeting/model/Size.java b/src/main/java/org/prebid/server/deals/targeting/model/Size.java new file mode 100644 index 00000000000..5b54b220596 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/model/Size.java @@ -0,0 +1,13 @@ +package org.prebid.server.deals.targeting.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class Size { + + Integer w; + + Integer h; +} diff --git a/src/main/java/org/prebid/server/deals/targeting/model/UserSegments.java b/src/main/java/org/prebid/server/deals/targeting/model/UserSegments.java new file mode 100644 index 00000000000..ed3976985d0 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/model/UserSegments.java @@ -0,0 +1,15 @@ +package org.prebid.server.deals.targeting.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@Value +@AllArgsConstructor(staticName = "of") +public class UserSegments { + + String source; + + List ids; +} diff --git a/src/main/java/org/prebid/server/deals/targeting/syntax/BooleanOperator.java b/src/main/java/org/prebid/server/deals/targeting/syntax/BooleanOperator.java new file mode 100644 index 00000000000..3200c4c02c9 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/syntax/BooleanOperator.java @@ -0,0 +1,33 @@ +package org.prebid.server.deals.targeting.syntax; + +import java.util.Arrays; + +public enum BooleanOperator { + + AND("$and"), + OR("$or"), + NOT("$not"); + + private final String value; + + BooleanOperator(String value) { + this.value = value; + } + + public String value() { + return value; + } + + public static boolean isBooleanOperator(String candidate) { + return Arrays.stream(BooleanOperator.values()).anyMatch(op -> op.value.equals(candidate)); + } + + public static BooleanOperator fromString(String candidate) { + for (final BooleanOperator op : values()) { + if (op.value.equals(candidate)) { + return op; + } + } + throw new IllegalArgumentException(String.format("Unrecognized boolean operator: %s", candidate)); + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/syntax/MatchingFunction.java b/src/main/java/org/prebid/server/deals/targeting/syntax/MatchingFunction.java new file mode 100644 index 00000000000..154063f874b --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/syntax/MatchingFunction.java @@ -0,0 +1,34 @@ +package org.prebid.server.deals.targeting.syntax; + +import java.util.Arrays; + +public enum MatchingFunction { + + MATCHES("$matches"), + IN("$in"), + INTERSECTS("$intersects"), + WITHIN("$within"); + + private final String value; + + MatchingFunction(String value) { + this.value = value; + } + + public String value() { + return value; + } + + public static boolean isMatchingFunction(String candidate) { + return Arrays.stream(MatchingFunction.values()).anyMatch(op -> op.value.equals(candidate)); + } + + public static MatchingFunction fromString(String candidate) { + for (final MatchingFunction op : values()) { + if (op.value.equals(candidate)) { + return op; + } + } + throw new IllegalArgumentException(String.format("Unrecognized matching function: %s", candidate)); + } +} diff --git a/src/main/java/org/prebid/server/deals/targeting/syntax/TargetingCategory.java b/src/main/java/org/prebid/server/deals/targeting/syntax/TargetingCategory.java new file mode 100644 index 00000000000..fcc3409c9b8 --- /dev/null +++ b/src/main/java/org/prebid/server/deals/targeting/syntax/TargetingCategory.java @@ -0,0 +1,134 @@ +package org.prebid.server.deals.targeting.syntax; + +import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.exception.TargetingSyntaxException; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Objects; + +@EqualsAndHashCode +public class TargetingCategory { + + private static final String BIDDER_PARAM_PATH_PATTERN = "\\w+(\\.\\w+)+"; + + private static final EnumSet DYNAMIC_TYPES = EnumSet.of( + Type.deviceGeoExt, + Type.deviceExt, + Type.bidderParam, + Type.userSegment, + Type.userFirstPartyData, + Type.siteFirstPartyData); + + private static final EnumSet STATIC_TYPES = EnumSet.complementOf(DYNAMIC_TYPES); + + private final Type type; + private final String path; + + public TargetingCategory(Type type) { + this(type, null); + } + + public TargetingCategory(Type type, String path) { + this.type = Objects.requireNonNull(type); + this.path = path; + } + + public static boolean isTargetingCategory(String candidate) { + final boolean isSimpleCategory = STATIC_TYPES.stream().anyMatch(op -> op.attribute().equals(candidate)); + return isSimpleCategory || DYNAMIC_TYPES.stream().anyMatch(op -> candidate.startsWith(op.attribute())); + } + + public static TargetingCategory fromString(String candidate) { + for (final Type type : STATIC_TYPES) { + if (type.attribute().equals(candidate)) { + return new TargetingCategory(type); + } + } + + for (final Type type : DYNAMIC_TYPES) { + if (candidate.startsWith(type.attribute())) { + return parseDynamicCategory(candidate, type); + } + } + + throw new IllegalArgumentException(String.format("Unrecognized targeting category: %s", candidate)); + } + + private static TargetingCategory parseDynamicCategory(String candidate, Type type) { + switch (type) { + case deviceGeoExt: + case deviceExt: + case userSegment: + case userFirstPartyData: + case siteFirstPartyData: + return parseByTypeAttribute(candidate, type); + case bidderParam: + return parseBidderParam(candidate, type); + default: + throw new IllegalStateException( + String.format("Unexpected dynamic targeting category type %s", type)); + } + } + + private static TargetingCategory parseByTypeAttribute(String candidate, Type type) { + final String candidatePath = StringUtils.substringAfter(candidate, type.attribute()); + return new TargetingCategory(type, candidatePath); + } + + private static TargetingCategory parseBidderParam(String candidate, Type type) { + final String candidatePath = StringUtils.substringAfter(candidate, type.attribute()); + if (candidatePath.matches(BIDDER_PARAM_PATH_PATTERN)) { + return new TargetingCategory(type, candidatePath); + } else { + throw new TargetingSyntaxException( + String.format("BidderParam path is incorrect: %s", candidatePath)); + } + } + + public Type type() { + return type; + } + + public String path() { + return path; + } + + public enum Type { + size("adunit.size"), + mediaType("adunit.mediatype"), + adslot("adunit.adslot"), + domain("site.domain"), + referrer("site.referrer"), + appBundle("app.bundle"), + deviceGeoExt("device.geo.ext."), + deviceExt("device.ext."), + pagePosition("pos"), + location("geo.distance"), + bidderParam("bidp."), + userSegment("segment."), + userFirstPartyData("ufpd."), + siteFirstPartyData("sfpd."), + dow("user.ext.time.userdow"), + hour("user.ext.time.userhour"); + + private String attribute; + + Type(String attribute) { + this.attribute = attribute; + } + + public String attribute() { + return attribute; + } + + public static Type fromString(String attribute) { + return Arrays.stream(values()) + .filter(value -> value.attribute.equals(attribute)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + String.format("Unrecognized targeting category type: %s", attribute))); + } + } +} diff --git a/src/main/java/org/prebid/server/events/EventRequest.java b/src/main/java/org/prebid/server/events/EventRequest.java index e27e8860995..59ee222c82b 100644 --- a/src/main/java/org/prebid/server/events/EventRequest.java +++ b/src/main/java/org/prebid/server/events/EventRequest.java @@ -14,6 +14,8 @@ public class EventRequest { String bidId; + String auctionId; + String accountId; String bidder; @@ -26,6 +28,8 @@ public class EventRequest { Analytics analytics; + String lineItemId; + public enum Type { win, imp diff --git a/src/main/java/org/prebid/server/events/EventUtil.java b/src/main/java/org/prebid/server/events/EventUtil.java index 7caf4ed03e5..a5f2b190c0b 100644 --- a/src/main/java/org/prebid/server/events/EventUtil.java +++ b/src/main/java/org/prebid/server/events/EventUtil.java @@ -24,6 +24,7 @@ public class EventUtil { private static final String BIDDER_PARAMETER = "bidder"; private static final String TIMESTAMP_PARAMETER = "ts"; + private static final String AUCTION_ID = "aid"; private static final String FORMAT_PARAMETER = "f"; private static final String BLANK_FORMAT = "b"; // default @@ -36,11 +37,13 @@ public class EventUtil { private static final String ENABLED_ANALYTICS = "1"; // default private static final String DISABLED_ANALYTICS = "0"; + private static final String LINE_ITEM_ID_PARAMETER = "l"; + private EventUtil() { } - public static void validateType(RoutingContext context) { - final String type = context.request().params().get(TYPE_PARAMETER); + public static void validateType(RoutingContext routingContext) { + final String type = routingContext.request().params().get(TYPE_PARAMETER); if (ObjectUtils.notEqual(type, IMP_TYPE) && ObjectUtils.notEqual(type, WIN_TYPE)) { throw new IllegalArgumentException(String.format( "Type '%s' is required query parameter. Possible values are %s and %s, but was %s", @@ -48,24 +51,24 @@ public static void validateType(RoutingContext context) { } } - public static void validateAccountId(RoutingContext context) { - final String accountId = context.request().params().get(ACCOUNT_ID_PARAMETER); + public static void validateAccountId(RoutingContext routingContext) { + final String accountId = routingContext.request().params().get(ACCOUNT_ID_PARAMETER); if (StringUtils.isBlank(accountId)) { throw new IllegalArgumentException(String.format( "Account '%s' is required query parameter and can't be empty", ACCOUNT_ID_PARAMETER)); } } - public static void validateBidId(RoutingContext context) { - final String bidId = context.request().params().get(BID_ID_PARAMETER); + public static void validateBidId(RoutingContext routingContext) { + final String bidId = routingContext.request().params().get(BID_ID_PARAMETER); if (StringUtils.isBlank(bidId)) { throw new IllegalArgumentException(String.format( "BidId '%s' is required query parameter and can't be empty", BID_ID_PARAMETER)); } } - public static void validateFormat(RoutingContext context) { - final String format = context.request().params().get(FORMAT_PARAMETER); + public static void validateFormat(RoutingContext routingContext) { + final String format = routingContext.request().params().get(FORMAT_PARAMETER); if (StringUtils.isNotEmpty(format) && !format.equals(BLANK_FORMAT) && !format.equals(IMAGE_FORMAT)) { throw new IllegalArgumentException(String.format( "Format '%s' query parameter is invalid. Possible values are %s and %s, but was %s", @@ -73,8 +76,8 @@ public static void validateFormat(RoutingContext context) { } } - public static void validateAnalytics(RoutingContext context) { - final String analytics = context.request().params().get(ANALYTICS_PARAMETER); + public static void validateAnalytics(RoutingContext routingContext) { + final String analytics = routingContext.request().params().get(ANALYTICS_PARAMETER); if (StringUtils.isNotEmpty(analytics) && !analytics.equals(ENABLED_ANALYTICS) && !analytics.equals(DISABLED_ANALYTICS)) { throw new IllegalArgumentException(String.format( @@ -83,8 +86,8 @@ public static void validateAnalytics(RoutingContext context) { } } - public static void validateTimestamp(RoutingContext context) { - final String timestamp = StringUtils.stripToNull(context.request().params().get(TIMESTAMP_PARAMETER)); + public static void validateTimestamp(RoutingContext routingContext) { + final String timestamp = StringUtils.stripToNull(routingContext.request().params().get(TIMESTAMP_PARAMETER)); if (timestamp != null) { try { Long.parseLong(timestamp); @@ -95,8 +98,8 @@ public static void validateTimestamp(RoutingContext context) { } } - public static void validateIntegration(RoutingContext context) { - final String value = context.request().getParam(INTEGRATION_PARAMETER); + public static void validateIntegration(RoutingContext routingContext) { + final String value = routingContext.request().getParam(INTEGRATION_PARAMETER); if (StringUtils.isNotEmpty(value)) { if (value.length() > INTEGRATION_PARAMETER_MAX_LENGTH) { throw new IllegalArgumentException(String.format( @@ -112,8 +115,8 @@ public static void validateIntegration(RoutingContext context) { } } - public static EventRequest from(RoutingContext context) { - final MultiMap queryParams = context.request().params(); + public static EventRequest from(RoutingContext routingContext) { + final MultiMap queryParams = routingContext.request().params(); final String typeAsString = queryParams.get(TYPE_PARAMETER); final EventRequest.Type type = typeAsString.equals(WIN_TYPE) ? EventRequest.Type.win : EventRequest.Type.imp; @@ -128,15 +131,19 @@ public static EventRequest from(RoutingContext context) { final String timestampAsString = StringUtils.stripToNull(queryParams.get(TIMESTAMP_PARAMETER)); final Long timestamp = timestampAsString != null ? Long.valueOf(timestampAsString) : null; + final String auctionId = StringUtils.stripToNull(queryParams.get(AUCTION_ID)); + return EventRequest.builder() .type(type) .bidId(queryParams.get(BID_ID_PARAMETER)) + .auctionId(auctionId) .accountId(queryParams.get(ACCOUNT_ID_PARAMETER)) .bidder(queryParams.get(BIDDER_PARAMETER)) .timestamp(timestamp) .format(format) .analytics(analytics) .integration(queryParams.get(INTEGRATION_PARAMETER)) + .lineItemId(queryParams.get(LINE_ITEM_ID_PARAMETER)) .build(); } @@ -154,6 +161,11 @@ static String toUrl(String externalUrl, EventRequest eventRequest) { private static String optionalParameters(EventRequest eventRequest) { final StringBuilder result = new StringBuilder(); + // auction ID + if (StringUtils.isNotEmpty(eventRequest.getAuctionId())) { + result.append(nameValueAsQueryString(AUCTION_ID, eventRequest.getAuctionId())); + } + // timestamp if (eventRequest.getTimestamp() != null) { result.append(nameValueAsQueryString(TIMESTAMP_PARAMETER, eventRequest.getTimestamp().toString())); @@ -182,6 +194,10 @@ private static String optionalParameters(EventRequest eventRequest) { result.append(nameValueAsQueryString(ANALYTICS_PARAMETER, DISABLED_ANALYTICS)); } + result.append(StringUtils.isNotEmpty(eventRequest.getLineItemId()) + ? nameValueAsQueryString(LINE_ITEM_ID_PARAMETER, eventRequest.getLineItemId()) + : StringUtils.EMPTY); // skip parameter + return result.toString(); } diff --git a/src/main/java/org/prebid/server/events/EventsContext.java b/src/main/java/org/prebid/server/events/EventsContext.java index 8c26814f2c4..d60087abc60 100644 --- a/src/main/java/org/prebid/server/events/EventsContext.java +++ b/src/main/java/org/prebid/server/events/EventsContext.java @@ -14,6 +14,8 @@ public class EventsContext { boolean enabledForRequest; + String auctionId; + Long auctionTimestamp; String integration; diff --git a/src/main/java/org/prebid/server/events/EventsService.java b/src/main/java/org/prebid/server/events/EventsService.java index 68d2af7b3dd..59accc7037e 100644 --- a/src/main/java/org/prebid/server/events/EventsService.java +++ b/src/main/java/org/prebid/server/events/EventsService.java @@ -16,60 +16,93 @@ public EventsService(String externalUrl) { /** * Returns {@link Events} object based on given params. */ - public Events createEvent(String bidId, String bidder, String accountId, Long timestamp, String integration) { + public Events createEvent(String bidId, + String bidder, + String accountId, + String lineItemId, + boolean analyticsEnabled, + EventsContext eventsContext) { return Events.of( eventUrl( EventRequest.Type.win, bidId, bidder, accountId, - timestamp, + lineItemId, + analytics(analyticsEnabled), EventRequest.Format.image, - integration), + eventsContext), eventUrl( EventRequest.Type.imp, bidId, bidder, accountId, - timestamp, + lineItemId, + analytics(analyticsEnabled), EventRequest.Format.image, - integration)); + eventsContext)); } /** * Returns url for win tracking. */ - public String winUrl(String bidId, String bidder, String accountId, Long timestamp, String integration) { + public String winUrl(String bidId, String bidder, String accountId, String lineItemId, + boolean analyticsEnabled, EventsContext eventsContext) { return eventUrl( - EventRequest.Type.win, bidId, bidder, accountId, timestamp, EventRequest.Format.image, integration); + EventRequest.Type.win, + bidId, + bidder, + accountId, + lineItemId, + analytics(analyticsEnabled), + EventRequest.Format.image, + eventsContext); } /** * Returns url for VAST tracking. */ - public String vastUrlTracking(String bidId, String bidder, String accountId, Long timestamp, String integration) { - return eventUrl( - EventRequest.Type.imp, bidId, bidder, accountId, timestamp, EventRequest.Format.blank, integration); + public String vastUrlTracking(String bidId, + String bidder, + String accountId, + String lineItemId, + EventsContext eventsContext) { + return eventUrl(EventRequest.Type.imp, + bidId, + bidder, + accountId, + lineItemId, + null, + EventRequest.Format.blank, + eventsContext); } private String eventUrl(EventRequest.Type type, String bidId, String bidder, String accountId, - Long timestamp, + String lineItemId, + EventRequest.Analytics analytics, EventRequest.Format format, - String integration) { + EventsContext eventsContext) { final EventRequest eventRequest = EventRequest.builder() .type(type) .bidId(bidId) + .auctionId(eventsContext.getAuctionId()) .accountId(accountId) .bidder(bidder) - .timestamp(timestamp) + .timestamp(eventsContext.getAuctionTimestamp()) .format(format) - .integration(integration) + .integration(eventsContext.getIntegration()) + .lineItemId(lineItemId) + .analytics(analytics) .build(); return EventUtil.toUrl(externalUrl, eventRequest); } + + private static EventRequest.Analytics analytics(boolean analyticsEnabled) { + return analyticsEnabled ? null : EventRequest.Analytics.disabled; + } } diff --git a/src/main/java/org/prebid/server/exception/TargetingSyntaxException.java b/src/main/java/org/prebid/server/exception/TargetingSyntaxException.java new file mode 100644 index 00000000000..b5fbd75b47d --- /dev/null +++ b/src/main/java/org/prebid/server/exception/TargetingSyntaxException.java @@ -0,0 +1,12 @@ +package org.prebid.server.exception; + +public class TargetingSyntaxException extends RuntimeException { + + public TargetingSyntaxException(String message) { + super(message); + } + + public TargetingSyntaxException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/prebid/server/exception/UnauthorizedUidsException.java b/src/main/java/org/prebid/server/exception/UnauthorizedUidsException.java new file mode 100644 index 00000000000..90d87a57d62 --- /dev/null +++ b/src/main/java/org/prebid/server/exception/UnauthorizedUidsException.java @@ -0,0 +1,9 @@ +package org.prebid.server.exception; + +@SuppressWarnings("serial") +public class UnauthorizedUidsException extends RuntimeException { + + public UnauthorizedUidsException(String message) { + super(message); + } +} diff --git a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java index dd19676e7f6..be6bcf20ac6 100755 --- a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java @@ -23,30 +23,40 @@ public class CircuitBreakerSecuredGeoLocationService implements GeoLocationServi private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); private static final int LOG_PERIOD_SECONDS = 5; - private final CircuitBreaker breaker; private final GeoLocationService geoLocationService; - private final Metrics metrics; + private final CircuitBreaker breaker; - public CircuitBreakerSecuredGeoLocationService(Vertx vertx, GeoLocationService geoLocationService, Metrics metrics, - int openingThreshold, long openingIntervalMs, - long closingIntervalMs, Clock clock) { + public CircuitBreakerSecuredGeoLocationService(Vertx vertx, + GeoLocationService geoLocationService, + Metrics metrics, + int openingThreshold, + long openingIntervalMs, + long closingIntervalMs, + Clock clock) { + + this.geoLocationService = Objects.requireNonNull(geoLocationService); - breaker = new CircuitBreaker("geolocation-service-circuit-breaker", Objects.requireNonNull(vertx), + breaker = new CircuitBreaker("geo_cb", Objects.requireNonNull(vertx), openingThreshold, openingIntervalMs, closingIntervalMs, Objects.requireNonNull(clock)) .openHandler(ignored -> circuitOpened()) .halfOpenHandler(ignored -> circuitHalfOpened()) .closeHandler(ignored -> circuitClosed()); - this.geoLocationService = Objects.requireNonNull(geoLocationService); - this.metrics = Objects.requireNonNull(metrics); + metrics.createGeoLocationCircuitBreakerGauge(breaker::isOpen); logger.info("Initialized GeoLocation service with Circuit Breaker"); } + @Override + public Future lookup(String ip, Timeout timeout) { + return breaker.execute(promise -> geoLocationService.lookup(ip, timeout).setHandler(promise)); + } + private void circuitOpened() { - conditionalLogger.warn("GeoLocation service is unavailable, circuit opened.", - LOG_PERIOD_SECONDS, TimeUnit.SECONDS); - metrics.updateGeoLocationCircuitBreakerMetric(true); + conditionalLogger.warn( + "GeoLocation service is unavailable, circuit opened.", + LOG_PERIOD_SECONDS, + TimeUnit.SECONDS); } private void circuitHalfOpened() { @@ -55,11 +65,5 @@ private void circuitHalfOpened() { private void circuitClosed() { logger.warn("GeoLocation service becomes working, circuit closed."); - metrics.updateGeoLocationCircuitBreakerMetric(false); - } - - @Override - public Future lookup(String ip, Timeout timeout) { - return breaker.execute(promise -> geoLocationService.lookup(ip, timeout).setHandler(promise)); } } diff --git a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java index 2b025176f51..45464fce4a6 100644 --- a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java @@ -4,12 +4,16 @@ import com.maxmind.geoip2.DatabaseReader; import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.CityResponse; +import com.maxmind.geoip2.record.City; +import com.maxmind.geoip2.record.Continent; +import com.maxmind.geoip2.record.Country; import com.maxmind.geoip2.record.Location; import com.maxmind.geoip2.record.Subdivision; import io.vertx.core.Future; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.execution.RemoteFileProcessor; import org.prebid.server.execution.Timeout; import org.prebid.server.geolocation.model.GeoInfo; @@ -21,7 +25,7 @@ import java.util.zip.GZIPInputStream; /** - * Implementation of of the {@link GeoLocationService} + * Implementation of the {@link GeoLocationService} * backed by MaxMind free database */ public class MaxMindGeoLocationService implements GeoLocationService, RemoteFileProcessor { @@ -65,31 +69,54 @@ public Future lookup(String ip, Timeout timeout) { try { final InetAddress inetAddress = InetAddress.getByName(ip); + final CityResponse cityResponse = databaseReader.city(inetAddress); + final Location location = cityResponse != null ? cityResponse.getLocation() : null; + return Future.succeededFuture(GeoInfo.builder() .vendor(VENDOR) - .continent(getCity(inetAddress).getContinent().getCode().toLowerCase()) - .country(getCity(inetAddress).getCountry().getIsoCode().toLowerCase()) - .region(getRegionCode(inetAddress)) - //metro code is skipped as Max Mind uses Google's version (Nielsen DMAs required) - .city(getCity(inetAddress).getCity().getName()) - .lat(getLocation(inetAddress).getLatitude().floatValue()) - .lon(getLocation(inetAddress).getLongitude().floatValue()) + .continent(resolveContinent(cityResponse)) + .country(resolveCountry(cityResponse)) + .region(resolveRegion(cityResponse)) + // metro code is skipped as Max Mind uses Google's version (Nielsen DMAs required) + .city(resolveCity(cityResponse)) + .lat(resolveLatitude(location)) + .lon(resolveLongitude(location)) .build()); } catch (IOException | GeoIp2Exception e) { return Future.failedFuture(e); } } - private CityResponse getCity(InetAddress inetAddress) throws IOException, GeoIp2Exception { - return databaseReader.city(inetAddress); + private static String resolveContinent(CityResponse cityResponse) { + final Continent continent = cityResponse != null ? cityResponse.getContinent() : null; + final String code = continent != null ? continent.getCode() : null; + return StringUtils.lowerCase(code); + } + + private static String resolveCountry(CityResponse cityResponse) { + final Country country = cityResponse != null ? cityResponse.getCountry() : null; + final String isoCode = country != null ? country.getIsoCode() : null; + return StringUtils.lowerCase(isoCode); + } + + private static String resolveRegion(CityResponse cityResponse) throws IOException, GeoIp2Exception { + final List subdivisions = cityResponse != null ? cityResponse.getSubdivisions() : null; + final Subdivision firstSubdivision = CollectionUtils.isEmpty(subdivisions) ? null : subdivisions.get(0); + return firstSubdivision != null ? firstSubdivision.getIsoCode() : null; + } + + private static String resolveCity(CityResponse cityResponse) { + final City city = cityResponse != null ? cityResponse.getCity() : null; + return city != null ? city.getName() : null; } - private String getRegionCode(InetAddress inetAddress) throws IOException, GeoIp2Exception { - final List subdivisions = getCity(inetAddress).getSubdivisions(); - return CollectionUtils.isEmpty(subdivisions) ? null : subdivisions.get(0).getIsoCode(); + private static Float resolveLatitude(Location location) { + final Double latitude = location != null ? location.getLatitude() : null; + return latitude != null ? latitude.floatValue() : null; } - private Location getLocation(InetAddress inetAddress) throws IOException, GeoIp2Exception { - return getCity(inetAddress).getLocation(); + private static Float resolveLongitude(Location location) { + final Double longitude = location != null ? location.getLongitude() : null; + return longitude != null ? longitude.floatValue() : null; } } diff --git a/src/main/java/org/prebid/server/handler/AccountCacheInvalidationHandler.java b/src/main/java/org/prebid/server/handler/AccountCacheInvalidationHandler.java index 68bb4f3ad0c..8ad70ec3bd0 100644 --- a/src/main/java/org/prebid/server/handler/AccountCacheInvalidationHandler.java +++ b/src/main/java/org/prebid/server/handler/AccountCacheInvalidationHandler.java @@ -2,7 +2,7 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; -import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; import org.apache.commons.lang3.StringUtils; import org.prebid.server.settings.CachingApplicationSettings; @@ -18,21 +18,26 @@ public class AccountCacheInvalidationHandler implements Handler private static final String ACCOUNT_ID_PARAM = "account"; private final CachingApplicationSettings cachingApplicationSettings; + private final String endpoint; - public AccountCacheInvalidationHandler(CachingApplicationSettings cachingApplicationSettings) { + public AccountCacheInvalidationHandler(CachingApplicationSettings cachingApplicationSettings, String endpoint) { this.cachingApplicationSettings = Objects.requireNonNull(cachingApplicationSettings); + this.endpoint = Objects.requireNonNull(endpoint); } @Override - public void handle(RoutingContext context) { - final HttpServerRequest request = context.request(); - final String accountId = request.getParam(ACCOUNT_ID_PARAM); + public void handle(RoutingContext routingContext) { + final String accountId = routingContext.request().getParam(ACCOUNT_ID_PARAM); if (StringUtils.isBlank(accountId)) { - HttpUtil.respondWith(context, HttpResponseStatus.BAD_REQUEST, "Account id is not defined"); + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end("Account id is not defined")); } else { cachingApplicationSettings.invalidateAccountCache(accountId); - HttpUtil.respondWith(context, HttpResponseStatus.OK, null); + HttpUtil.executeSafely(routingContext, endpoint, + HttpServerResponse::end); } } } diff --git a/src/main/java/org/prebid/server/handler/AuctionHandler.java b/src/main/java/org/prebid/server/handler/AuctionHandler.java deleted file mode 100644 index c79c8d28545..00000000000 --- a/src/main/java/org/prebid/server/handler/AuctionHandler.java +++ /dev/null @@ -1,493 +0,0 @@ -package org.prebid.server.handler; - -import io.netty.handler.codec.http.HttpHeaderValues; -import io.vertx.core.AsyncResult; -import io.vertx.core.CompositeFuture; -import io.vertx.core.Future; -import io.vertx.core.Handler; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.web.RoutingContext; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.PreBidRequestContextFactory; -import org.prebid.server.auction.PrivacyEnforcementService; -import org.prebid.server.auction.TargetingKeywordsCreator; -import org.prebid.server.auction.model.AdapterResponse; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.auction.model.Tuple2; -import org.prebid.server.auction.model.Tuple3; -import org.prebid.server.bidder.Adapter; -import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.bidder.HttpAdapterConnector; -import org.prebid.server.bidder.Usersyncer; -import org.prebid.server.cache.CacheService; -import org.prebid.server.cache.proto.BidCacheResult; -import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.MetricName; -import org.prebid.server.metric.Metrics; -import org.prebid.server.privacy.gdpr.TcfDefinerService; -import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; -import org.prebid.server.proto.request.AdUnit; -import org.prebid.server.proto.request.PreBidRequest; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.BidderInfo; -import org.prebid.server.proto.response.BidderStatus; -import org.prebid.server.proto.response.MediaType; -import org.prebid.server.proto.response.PreBidResponse; -import org.prebid.server.settings.ApplicationSettings; -import org.prebid.server.settings.model.Account; -import org.prebid.server.util.HttpUtil; - -import java.math.BigDecimal; -import java.time.Clock; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class AuctionHandler implements Handler { - - private static final Logger logger = LoggerFactory.getLogger(AuctionHandler.class); - - private static final MetricName REQUEST_TYPE_METRIC = MetricName.legacy; - private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000); - - private final ApplicationSettings applicationSettings; - private final BidderCatalog bidderCatalog; - private final PreBidRequestContextFactory preBidRequestContextFactory; - private final CacheService cacheService; - private final Metrics metrics; - private final HttpAdapterConnector httpAdapterConnector; - private final Clock clock; - private final TcfDefinerService tcfDefinerService; - private final PrivacyEnforcementService privacyEnforcementService; - private final JacksonMapper mapper; - private final Integer gdprHostVendorId; - - public AuctionHandler(ApplicationSettings applicationSettings, - BidderCatalog bidderCatalog, - PreBidRequestContextFactory preBidRequestContextFactory, - CacheService cacheService, - Metrics metrics, - HttpAdapterConnector httpAdapterConnector, - Clock clock, - TcfDefinerService tcfDefinerService, - PrivacyEnforcementService privacyEnforcementService, - JacksonMapper mapper, - Integer gdprHostVendorId) { - - this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - this.preBidRequestContextFactory = Objects.requireNonNull(preBidRequestContextFactory); - this.cacheService = Objects.requireNonNull(cacheService); - this.metrics = Objects.requireNonNull(metrics); - this.httpAdapterConnector = Objects.requireNonNull(httpAdapterConnector); - this.clock = Objects.requireNonNull(clock); - this.tcfDefinerService = Objects.requireNonNull(tcfDefinerService); - this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); - this.mapper = Objects.requireNonNull(mapper); - this.gdprHostVendorId = gdprHostVendorId; - } - - /** - * Auction handler will resolve all bidders in the incoming ad request, issue the request to the different - * clients, then return an array of the responses. - */ - @Override - public void handle(RoutingContext context) { - final long startTime = clock.millis(); - - final boolean isSafari = HttpUtil.isSafari(context.request().headers().get(HttpUtil.USER_AGENT_HEADER)); - - metrics.updateSafariRequestsMetric(isSafari); - - preBidRequestContextFactory.fromRequest(context) - .recover(exception -> failWithInvalidRequest( - String.format("Error parsing request: %s", exception.getMessage()), exception)) - - .map(preBidRequestContext -> - updateAppAndNoCookieAndImpsMetrics(preBidRequestContext, isSafari)) - - .compose(preBidRequestContext -> accountFrom(preBidRequestContext) - .map(account -> Tuple2.of(preBidRequestContext, account))) - - .map(this::updateAccountRequestAndRequestTimeMetric) - - .compose((Tuple2 result) -> - CompositeFuture.join(submitRequestsToExchanges(result.getLeft())) - .map(bidderResults -> Tuple3.of(result.getLeft(), result.getRight(), - bidderResults.list()))) - - .compose((Tuple3> result) -> - resolveVendorsToGdpr(result.getLeft(), result.getMiddle(), result.getRight()) - .map(vendorsToGdpr -> Tuple3.of(result.getLeft(), result.getMiddle(), - composePreBidResponse(result.getLeft(), result.getRight(), vendorsToGdpr)))) - - .compose((Tuple3 result) -> - processCacheMarkup(result.getLeft(), result.getRight(), result.getMiddle().getId()) - .recover(exception -> failWith( - String.format("Prebid cache failed: %s", exception.getMessage()), exception)) - .map(response -> Tuple3.of(result.getLeft(), result.getMiddle(), response))) - - .map((Tuple3 result) -> - addTargetingKeywords(result.getLeft().getPreBidRequest(), result.getMiddle(), - result.getRight())) - - .setHandler(preBidResponseResult -> - respondWith(bidResponseOrError(preBidResponseResult), context, startTime)); - } - - private PreBidRequestContext updateAppAndNoCookieAndImpsMetrics(PreBidRequestContext preBidRequestContext, - boolean isSafari) { - final PreBidRequest preBidRequest = preBidRequestContext.getPreBidRequest(); - final List adUnits = preBidRequest.getAdUnits(); - - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(preBidRequest.getApp() != null, - !preBidRequestContext.isNoLiveUids(), isSafari, adUnits.size()); - - final Map mediaTypeToCount = adUnits.stream() - .map(AdUnit::getMediaTypes) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .filter(Objects::nonNull) - .collect(Collectors.groupingBy(String::toLowerCase, Collectors.counting())); - - metrics.updateImpTypesMetrics(mediaTypeToCount); - - return preBidRequestContext; - } - - private Tuple2 updateAccountRequestAndRequestTimeMetric( - Tuple2 preBidRequestContextAccount) { - - final String accountId = preBidRequestContextAccount.getLeft().getPreBidRequest().getAccountId(); - metrics.updateAccountRequestMetrics(accountId, REQUEST_TYPE_METRIC); - - return preBidRequestContextAccount; - } - - private static Future failWithInvalidRequest(String message, Throwable exception) { - return Future.failedFuture(new InvalidRequestException(message, exception)); - } - - private Future accountFrom(PreBidRequestContext preBidRequestContext) { - return applicationSettings.getAccountById(preBidRequestContext.getPreBidRequest().getAccountId(), - preBidRequestContext.getTimeout()) - .recover(AuctionHandler::failWithUnknownAccountOrPropagateOriginal); - } - - private static Future failWithUnknownAccountOrPropagateOriginal(Throwable exception) { - return exception instanceof PreBidException - // transform into InvalidRequestException if account is unknown - ? failWithInvalidRequest("Unknown account id: Unknown account", exception) - // otherwise propagate exception as is because it is of system nature - : Future.failedFuture(exception); - } - - private static Future failWith(String message, Throwable exception) { - return Future.failedFuture(new PreBidException(message, exception)); - } - - @SuppressWarnings("rawtypes") - private List submitRequestsToExchanges(PreBidRequestContext preBidRequestContext) { - return preBidRequestContext.getAdapterRequests().stream() - .filter(ar -> isValidAdapterName(ar.getBidderCode())) - .map(ar -> httpAdapterConnector.call(adapterByName(ar.getBidderCode()), - usersyncerByName(ar.getBidderCode()), ar, preBidRequestContext)) - .collect(Collectors.toList()); - } - - private String adapterNameFor(String name) { - return bidderCatalog.isAlias(name) ? bidderCatalog.nameByAlias(name) : name; - } - - private boolean isValidAdapterName(String name) { - return bidderCatalog.isValidAdapterName(adapterNameFor(name)); - } - - private Adapter adapterByName(String name) { - return bidderCatalog.adapterByName(adapterNameFor(name)); - } - - private Usersyncer usersyncerByName(String name) { - return bidderCatalog.usersyncerByName(adapterNameFor(name)); - } - - private BidderInfo bidderInfoByName(String name) { - return bidderCatalog.bidderInfoByName(adapterNameFor(name)); - } - - private Future> resolveVendorsToGdpr(PreBidRequestContext preBidRequestContext, - Account account, - List adapterResponses) { - // todo Process also but bidders name (not every bidder have GVL id) - final Set vendorIds = adapterResponses.stream() - .map(adapterResponse -> adapterResponse.getBidderStatus().getBidder()) - .filter(this::isValidAdapterName) - .map(bidder -> bidderInfoByName(bidder).getGdpr().getVendorId()) - .collect(Collectors.toSet()); - - final boolean hostVendorIdIsMissing = gdprHostVendorId != null && !vendorIds.contains(gdprHostVendorId); - if (hostVendorIdIsMissing) { - vendorIds.add(gdprHostVendorId); - } - - return privacyEnforcementService.contextFromLegacyRequest(preBidRequestContext, account) - .compose(privacyContext -> tcfDefinerService.resultForVendorIds( - vendorIds, privacyContext.getTcfContext())) - .map(gdprResponse -> toVendorsToGdpr(gdprResponse.getActions(), hostVendorIdIsMissing)); - } - - private Map toVendorsToGdpr( - Map vendorToAction, boolean hostVendorIdIsMissing) { - - final Map result; - - if (vendorToAction.containsKey(gdprHostVendorId) && vendorToAction.get(gdprHostVendorId).isBlockPixelSync()) { - result = Collections.emptyMap(); // deny all by host vendor - } else { - result = vendorToAction.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> !entry.getValue().isBlockPixelSync())); - if (hostVendorIdIsMissing) { - result.remove(gdprHostVendorId); - } - } - - return result; - } - - private PreBidResponse composePreBidResponse(PreBidRequestContext preBidRequestContext, - List adapterResponses, - Map vendorsToGdpr) { - adapterResponses.stream() - .filter(ar -> ar.getError() != null) - .forEach(this::updateAdapterErrorMetrics); - - final List bidderStatuses = Stream.concat( - adapterResponses.stream() - .map(adapterResponse -> updateBidderStatus(adapterResponse.getBidderStatus(), vendorsToGdpr)) - .peek(bs -> updateResponseTimeMetrics(bs, preBidRequestContext)), - invalidBidderStatuses(preBidRequestContext)) - .collect(Collectors.toList()); - - final List bids = adapterResponses.stream() - .filter(ar -> StringUtils.isBlank(ar.getBidderStatus().getError())) - .peek(ar -> updateBidResultMetrics(ar, preBidRequestContext)) - .flatMap(ar -> ar.getBids().stream()) - .collect(Collectors.toList()); - - return PreBidResponse.builder() - .status(preBidRequestContext.isNoLiveUids() ? "no_cookie" : "OK") - .tid(preBidRequestContext.getPreBidRequest().getTid()) - .bidderStatus(bidderStatuses) - .bids(bids) - .build(); - } - - private BidderStatus updateBidderStatus(BidderStatus bidderStatus, Map vendorsToGdpr) { - final int vendorId = bidderInfoByName(bidderStatus.getBidder()).getGdpr().getVendorId(); - return Objects.equals(vendorsToGdpr.get(vendorId), true) - ? bidderStatus - : bidderStatus.toBuilder().usersync(null).build(); - } - - private void updateAdapterErrorMetrics(AdapterResponse adapterResponse) { - final MetricName errorMetric; - switch (adapterResponse.getError().getType()) { - case bad_input: - errorMetric = MetricName.badinput; - break; - case bad_server_response: - errorMetric = MetricName.badserverresponse; - break; - case timeout: - errorMetric = MetricName.timeout; - break; - case generic: - default: - errorMetric = MetricName.unknown_error; - } - - metrics.updateAdapterRequestErrorMetric(adapterResponse.getBidderStatus().getBidder(), errorMetric); - } - - private void updateResponseTimeMetrics(BidderStatus bidderStatus, PreBidRequestContext preBidRequestContext) { - metrics.updateAdapterResponseTime(bidderStatus.getBidder(), - preBidRequestContext.getPreBidRequest().getAccountId(), bidderStatus.getResponseTimeMs()); - } - - private Stream invalidBidderStatuses(PreBidRequestContext preBidRequestContext) { - return preBidRequestContext.getAdapterRequests().stream() - .filter(ar -> !bidderCatalog.isValidName(adapterNameFor(ar.getBidderCode()))) - .map(ar -> BidderStatus.builder().bidder(ar.getBidderCode()).error("Unsupported bidder").build()); - } - - private void updateBidResultMetrics(AdapterResponse adapterResponse, PreBidRequestContext preBidRequestContext) { - final BidderStatus bidderStatus = adapterResponse.getBidderStatus(); - final String bidder = bidderStatus.getBidder(); - - metrics.updateAdapterRequestTypeAndNoCookieMetrics(bidder, REQUEST_TYPE_METRIC, - Objects.equals(bidderStatus.getNoCookie(), Boolean.TRUE)); - - final String accountId = preBidRequestContext.getPreBidRequest().getAccountId(); - - for (final Bid bid : adapterResponse.getBids()) { - final long cpm = bid.getPrice().multiply(THOUSAND).longValue(); - metrics.updateAdapterBidMetrics(bidder, accountId, cpm, bid.getAdm() != null, - ObjectUtils.defaultIfNull(bid.getMediaType(), MediaType.banner).toString()); // default to banner - } - - if (Objects.equals(bidderStatus.getNoBid(), Boolean.TRUE)) { - metrics.updateAdapterRequestNobidMetrics(bidder, accountId); - } else { - metrics.updateAdapterRequestGotbidsMetrics(bidder, accountId); - } - } - - private Future processCacheMarkup(PreBidRequestContext preBidRequestContext, - PreBidResponse preBidResponse, - String accountId) { - - final Future result; - - final Integer cacheMarkup = preBidRequestContext.getPreBidRequest().getCacheMarkup(); - final List bids = preBidResponse.getBids(); - if (!bids.isEmpty() && cacheMarkup != null && (cacheMarkup == 1 || cacheMarkup == 2)) { - result = (cacheMarkup == 1 - ? cacheService.cacheBids(bids, preBidRequestContext.getTimeout(), accountId) - : cacheService.cacheBidsVideoOnly(bids, preBidRequestContext.getTimeout(), accountId)) - .map(bidCacheResults -> mergeBidsWithCacheResults(preBidResponse, bidCacheResults)); - } else { - result = Future.succeededFuture(preBidResponse); - } - - return result; - } - - private static PreBidResponse mergeBidsWithCacheResults(PreBidResponse preBidResponse, - List bidCacheResults) { - final List bids = preBidResponse.getBids(); - for (int i = 0; i < bids.size(); i++) { - final BidCacheResult result = bidCacheResults.get(i); - // IMPORTANT: see javadoc in Bid class - bids.get(i) - .setAdm(null) - .setNurl(null) - .setCacheId(result.getCacheId()) - .setCacheUrl(result.getCacheUrl()); - } - - return preBidResponse; - } - - /** - * Sorts the bids and adds ad server targeting keywords to each bid. - * The bids are sorted by cpm to find the highest bid. - * The ad server targeting keywords are added to all bids, with specific keywords for the highest bid. - */ - private static PreBidResponse addTargetingKeywords(PreBidRequest preBidRequest, Account account, - PreBidResponse preBidResponse) { - final Integer sortBids = preBidRequest.getSortBids(); - if (sortBids != null && sortBids == 1) { - final TargetingKeywordsCreator keywordsCreator = - TargetingKeywordsCreator.create(account.getPriceGranularity(), true, true, false, 0); - - final Map> adUnitCodeToBids = preBidResponse.getBids().stream() - .collect(Collectors.groupingBy(Bid::getCode)); - - for (final List bids : adUnitCodeToBids.values()) { - bids.sort(Comparator.comparing(Bid::getPrice) - .reversed() - .thenComparing(Bid::getResponseTimeMs)); - - for (final Bid bid : bids) { - final boolean isFirstBid = bid == bids.get(0); - // IMPORTANT: see javadoc in Bid class - bid.setAdServerTargeting(joinMaps( - keywordsCreator.makeFor(bid, isFirstBid), bid.getAdServerTargeting())); - } - } - } - - return preBidResponse; - } - - private static Map joinMaps(Map left, Map right) { - if (right != null) { - left.putAll(right); - } - return left; - } - - private void respondWith(PreBidResponse response, RoutingContext context, long startTime) { - // don't send the response if client has gone - if (context.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); - return; - } - - context.response() - .exceptionHandler(this::handleResponseException) - .putHeader(HttpUtil.DATE_HEADER, date()) - .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) - .end(mapper.encode(response)); - - metrics.updateRequestTimeMetric(clock.millis() - startTime); - } - - private void handleResponseException(Throwable exception) { - logger.warn("Failed to send auction response: {0}", exception.getMessage()); - metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); - } - - private static String date() { - return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now()); - } - - private PreBidResponse bidResponseOrError(AsyncResult responseResult) { - final MetricName responseStatus; - - final PreBidResponse result; - - if (responseResult.succeeded()) { - responseStatus = MetricName.ok; - result = responseResult.result(); - } else { - final Throwable exception = responseResult.cause(); - final boolean isRequestInvalid = exception instanceof InvalidRequestException; - - responseStatus = isRequestInvalid ? MetricName.badinput : MetricName.err; - - if (!isRequestInvalid) { - logger.error("Failed to process /auction request", exception); - } - - result = error(isRequestInvalid || exception instanceof PreBidException - ? exception.getMessage() - : "Unexpected server error"); - } - - updateRequestMetric(responseStatus); - - return result; - } - - private void updateRequestMetric(MetricName requestStatus) { - metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, requestStatus); - } - - private static PreBidResponse error(String status) { - return PreBidResponse.builder().status(status).build(); - } -} diff --git a/src/main/java/org/prebid/server/handler/BidderParamHandler.java b/src/main/java/org/prebid/server/handler/BidderParamHandler.java index e43215dc19d..aa0d5b188a9 100644 --- a/src/main/java/org/prebid/server/handler/BidderParamHandler.java +++ b/src/main/java/org/prebid/server/handler/BidderParamHandler.java @@ -1,17 +1,15 @@ package org.prebid.server.handler; import io.vertx.core.Handler; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; +import org.prebid.server.model.Endpoint; +import org.prebid.server.util.HttpUtil; import org.prebid.server.validation.BidderParamValidator; import java.util.Objects; public class BidderParamHandler implements Handler { - private static final Logger logger = LoggerFactory.getLogger(BidderParamHandler.class); - private final BidderParamValidator bidderParamValidator; public BidderParamHandler(BidderParamValidator bidderParamValidator) { @@ -19,12 +17,9 @@ public BidderParamHandler(BidderParamValidator bidderParamValidator) { } @Override - public void handle(RoutingContext context) { - // don't send the response if client has gone - if (context.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - return; - } - context.response().end(bidderParamValidator.schemas()); + public void handle(RoutingContext routingContext) { + HttpUtil.executeSafely(routingContext, Endpoint.bidder_params, + response -> response + .end(bidderParamValidator.schemas())); } } diff --git a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java index 30cdec79030..9c0f15f81c8 100644 --- a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java +++ b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java @@ -2,42 +2,55 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; +import lombok.Value; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.CookieSyncEvent; import org.prebid.server.auction.PrivacyEnforcementService; -import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.auction.model.CookieSyncContext; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.UsersyncInfoAssembler; +import org.prebid.server.bidder.UsersyncMethodChooser; +import org.prebid.server.bidder.UsersyncUtil; import org.prebid.server.bidder.Usersyncer; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.cookie.model.UidWithExpiry; import org.prebid.server.cookie.proto.Uids; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.exception.UnauthorizedUidsException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.ConditionalLogger; import org.prebid.server.metric.Metrics; +import org.prebid.server.model.Endpoint; import org.prebid.server.privacy.gdpr.TcfDefinerService; +import org.prebid.server.privacy.gdpr.model.HostVendorTcfResponse; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; +import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.gdpr.model.TcfResponse; import org.prebid.server.privacy.model.Privacy; -import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.request.CookieSyncRequest; import org.prebid.server.proto.response.BidderUsersyncStatus; import org.prebid.server.proto.response.CookieSyncResponse; import org.prebid.server.proto.response.UsersyncInfo; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountCookieSyncConfig; +import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; import org.prebid.server.util.HttpUtil; import java.util.ArrayList; @@ -53,9 +66,15 @@ public class CookieSyncHandler implements Handler { private static final Logger logger = LoggerFactory.getLogger(CookieSyncHandler.class); + private static final ConditionalLogger BAD_REQUEST_LOGGER = new ConditionalLogger(logger); private static final String REJECTED_BY_TCF = "Rejected by TCF"; private static final String REJECTED_BY_CCPA = "Rejected by CCPA"; + private static final String METRICS_UNKNOWN_BIDDER = "UNKNOWN"; + + // Probably this should be moved to config since hardcoding of "uid" param is not ideal + private static final String HOST_BIDDER_USERSYNC_URL_TEMPLATE = + "%s/setuid?bidder=%s&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=%s"; private final String externalUrl; private final long defaultTimeout; @@ -66,9 +85,10 @@ public class CookieSyncHandler implements Handler { private final TcfDefinerService tcfDefinerService; private final PrivacyEnforcementService privacyEnforcementService; private final Integer gdprHostVendorId; - private final boolean defaultCoopSync; + private final Boolean defaultCoopSync; private final List> listOfCoopSyncBidders; - private final AnalyticsReporter analyticsReporter; + private final Set setOfCoopSyncBidders; + private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; private final TimeoutFactory timeoutFactory; private final JacksonMapper mapper; @@ -83,7 +103,7 @@ public CookieSyncHandler(String externalUrl, Integer gdprHostVendorId, boolean defaultCoopSync, List> listOfCoopSyncBidders, - AnalyticsReporter analyticsReporter, + AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, TimeoutFactory timeoutFactory, JacksonMapper mapper) { @@ -96,12 +116,13 @@ public CookieSyncHandler(String externalUrl, this.activeBidders = activeBidders(bidderCatalog); this.tcfDefinerService = Objects.requireNonNull(tcfDefinerService); this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); - this.gdprHostVendorId = gdprHostVendorId; + this.gdprHostVendorId = validateHostVendorId(gdprHostVendorId); this.defaultCoopSync = defaultCoopSync; this.listOfCoopSyncBidders = CollectionUtils.isNotEmpty(listOfCoopSyncBidders) ? listOfCoopSyncBidders : Collections.singletonList(activeBidders); - this.analyticsReporter = Objects.requireNonNull(analyticsReporter); + this.setOfCoopSyncBidders = flatMapToSet(this.listOfCoopSyncBidders); + this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.mapper = Objects.requireNonNull(mapper); @@ -111,80 +132,120 @@ private static Set activeBidders(BidderCatalog bidderCatalog) { return bidderCatalog.names().stream().filter(bidderCatalog::isActive).collect(Collectors.toSet()); } + private static Integer validateHostVendorId(Integer gdprHostVendorId) { + if (gdprHostVendorId == null) { + logger.warn("gdpr.host-vendor-id not specified. Will skip host company GDPR checks"); + } + return gdprHostVendorId; + } + + private static Set flatMapToSet(List> listOfStringLists) { + return listOfStringLists.stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + @Override - public void handle(RoutingContext context) { + public void handle(RoutingContext routingContext) { metrics.updateCookieSyncRequestMetric(); - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(context); - if (!uidsCookie.allowsSync()) { - final int status = HttpResponseStatus.UNAUTHORIZED.code(); - final String message = "User has opted out"; - context.response().setStatusCode(status).setStatusMessage(message).end(); - analyticsReporter.processEvent(CookieSyncEvent.error(status, message)); - return; + toCookieSyncContext(routingContext) + .setHandler(cookieSyncContextResult -> handleCookieSyncContextResult(cookieSyncContextResult, + routingContext)); + } + + private Future toCookieSyncContext(RoutingContext routingContext) { + final CookieSyncRequest cookieSyncRequest; + try { + cookieSyncRequest = parseRequest(routingContext); + } catch (Exception e) { + return Future.failedFuture(e); } - final Buffer body = context.getBody(); + final String requestAccount = cookieSyncRequest.getAccount(); + final Timeout timeout = timeoutFactory.create(defaultTimeout); + final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + + return accountById(requestAccount, timeout) + .compose(account -> privacyEnforcementService.contextFromCookieSyncRequest( + cookieSyncRequest, routingContext.request(), account, timeout) + .map(privacyContext -> CookieSyncContext.builder() + .routingContext(routingContext) + .uidsCookie(uidsCookie) + .cookieSyncRequest(cookieSyncRequest) + .usersyncMethodChooser( + UsersyncMethodChooser.from(cookieSyncRequest.getFilterSettings())) + .timeout(timeout) + .account(account) + .privacyContext(privacyContext) + .build())); + } + + private CookieSyncRequest parseRequest(RoutingContext routingContext) { + final Buffer body = routingContext.getBody(); if (body == null) { - final int status = HttpResponseStatus.BAD_REQUEST.code(); - final String message = "Request has no body"; - context.response().setStatusCode(status).setStatusMessage(message).end(); - analyticsReporter.processEvent(CookieSyncEvent.error(status, message)); - return; + throw new InvalidRequestException("Request has no body"); } - final CookieSyncRequest cookieSyncRequest; try { - cookieSyncRequest = mapper.decodeValue(body, CookieSyncRequest.class); + return mapper.decodeValue(body, CookieSyncRequest.class); } catch (DecodeException e) { - final int status = HttpResponseStatus.BAD_REQUEST.code(); final String message = "Request body cannot be parsed"; - context.response().setStatusCode(status).setStatusMessage(message).end(); - analyticsReporter.processEvent(CookieSyncEvent.error(status, message)); logger.info(message, e); - return; + throw new InvalidRequestException(message); + } + } + + private Future accountById(String accountId, Timeout timeout) { + return StringUtils.isBlank(accountId) + ? Future.succeededFuture(Account.empty(accountId)) + : applicationSettings.getAccountById(accountId, timeout) + .otherwise(Account.empty(accountId)); + } + + private void handleCookieSyncContextResult(AsyncResult cookieSyncContextResult, + RoutingContext routingContext) { + + if (cookieSyncContextResult.succeeded()) { + final CookieSyncContext cookieSyncContext = cookieSyncContextResult.result(); + + final TcfContext tcfContext = cookieSyncContext.getPrivacyContext().getTcfContext(); + try { + validateCookieSyncContext(cookieSyncContext); + } catch (InvalidRequestException | UnauthorizedUidsException e) { + handleErrors(e, routingContext, tcfContext); + return; + } + + isAllowedForHostVendorId(tcfContext) + .setHandler(hostTcfResponseResult -> respondByTcfResponse( + hostTcfResponseResult, + biddersToSync(cookieSyncContext), + cookieSyncContext)); + } else { + handleErrors(cookieSyncContextResult.cause(), routingContext, null); } + } - if (gdprParamsNotConsistent(cookieSyncRequest)) { - final int status = HttpResponseStatus.BAD_REQUEST.code(); - final String message = "gdpr_consent is required if gdpr is 1"; - context.response().setStatusCode(status).setStatusMessage(message).end(); - analyticsReporter.processEvent(CookieSyncEvent.error(status, message)); - return; + private void validateCookieSyncContext(CookieSyncContext cookieSyncContext) { + final UidsCookie uidsCookie = cookieSyncContext.getUidsCookie(); + if (!uidsCookie.allowsSync()) { + throw new UnauthorizedUidsException("Sync is not allowed for this uids"); } - final Integer limit = cookieSyncRequest.getLimit(); - final Boolean coopSync = cookieSyncRequest.getCoopSync(); - final Set biddersToSync = biddersToSync(cookieSyncRequest.getBidders(), coopSync, limit); + final CookieSyncRequest cookieSyncRequest = cookieSyncContext.getCookieSyncRequest(); + if (isGdprParamsNotConsistent(cookieSyncRequest)) { + throw new InvalidRequestException("gdpr_consent is required if gdpr is 1"); + } - final String requestAccount = cookieSyncRequest.getAccount(); - final Set vendorIds = Collections.singleton(gdprHostVendorId); - final Timeout timeout = timeoutFactory.create(defaultTimeout); + final TcfContext tcfContext = cookieSyncContext.getPrivacyContext().getTcfContext(); + if (StringUtils.equals(tcfContext.getGdpr(), "1") && BooleanUtils.isFalse(tcfContext.getIsConsentValid())) { + metrics.updateUserSyncTcfInvalidMetric(); + throw new InvalidRequestException("Consent string is invalid"); + } + } - accountById(requestAccount, timeout) - .compose(account -> privacyEnforcementService.contextFromCookieSyncRequest( - cookieSyncRequest, context.request(), account, timeout) - .map(privacyContext -> Tuple2.of(account, privacyContext))) - .map((Tuple2 accountAndPrivacy) -> tcfDefinerService.resultForVendorIds( - vendorIds, accountAndPrivacy.getRight().getTcfContext()) - .compose(this::handleVendorIdResult) - .compose(ignored -> tcfDefinerService.resultForBidderNames( - biddersToSync, - accountAndPrivacy.getRight().getTcfContext(), - accountAndPrivacy.getLeft().getGdpr())) - .map(tcfResponse -> handleBidderNamesResult( - tcfResponse, - context, - accountAndPrivacy.getLeft(), - uidsCookie, - biddersToSync, - accountAndPrivacy.getRight().getPrivacy(), - limit)) - .otherwise(ignored -> handleTcfError( - context, uidsCookie, biddersToSync, accountAndPrivacy.getRight().getPrivacy(), limit))); - } - - private static boolean gdprParamsNotConsistent(CookieSyncRequest request) { + private static boolean isGdprParamsNotConsistent(CookieSyncRequest request) { return Objects.equals(request.getGdpr(), 1) && StringUtils.isBlank(request.getGdprConsent()); } @@ -193,13 +254,20 @@ private static boolean gdprParamsNotConsistent(CookieSyncRequest request) { *

* If bidder list was omitted in request, that means sync should be done for all bidders. */ - private Set biddersToSync(List requestBidders, Boolean requestCoop, Integer requestLimit) { + private Set biddersToSync(CookieSyncContext cookieSyncContext) { + final CookieSyncRequest cookieSyncRequest = cookieSyncContext.getCookieSyncRequest(); + + final List requestBidders = cookieSyncRequest.getBidders(); + if (CollectionUtils.isEmpty(requestBidders)) { return activeBidders; } - final boolean coop = requestCoop != null ? requestCoop : defaultCoopSync; - if (coop) { + final Account account = cookieSyncContext.getAccount(); + + if (coopSyncAllowed(cookieSyncRequest, account)) { + final Integer requestLimit = resolveLimit(cookieSyncContext); + return requestLimit == null ? addAllCoopSyncBidders(requestBidders) : addCoopSyncBidders(requestBidders, requestLimit); @@ -208,11 +276,46 @@ private Set biddersToSync(List requestBidders, Boolean requestCo return new HashSet<>(requestBidders); } - private Set addAllCoopSyncBidders(List bidders) { - final Set updatedBidders = listOfCoopSyncBidders.stream() - .flatMap(Collection::stream) - .collect(Collectors.toSet()); + private Boolean coopSyncAllowed(CookieSyncRequest cookieSyncRequest, Account account) { + final Boolean requestCoopSync = cookieSyncRequest.getCoopSync(); + if (requestCoopSync != null) { + return requestCoopSync; + } + + final AccountCookieSyncConfig accountCookieSyncConfig = account.getCookieSync(); + final Boolean accountCoopSync = accountCookieSyncConfig != null + ? accountCookieSyncConfig.getDefaultCoopSync() + : null; + + return ObjectUtils.firstNonNull(accountCoopSync, defaultCoopSync); + } + /** + * If host vendor id is null, host allowed to sync cookies. + */ + private Future isAllowedForHostVendorId(TcfContext tcfContext) { + return gdprHostVendorId == null + ? Future.succeededFuture(HostVendorTcfResponse.allowedVendor()) + : tcfDefinerService.resultForVendorIds(Collections.singleton(gdprHostVendorId), tcfContext) + .map(this::toHostVendorTcfResponse); + } + + private HostVendorTcfResponse toHostVendorTcfResponse(TcfResponse tcfResponse) { + return HostVendorTcfResponse.of(tcfResponse.getUserInGdprScope(), tcfResponse.getCountry(), + isCookieSyncAllowed(tcfResponse)); + } + + private boolean isCookieSyncAllowed(TcfResponse hostTcfResponse) { + final Map vendorIdToAction = hostTcfResponse.getActions(); + final PrivacyEnforcementAction hostActions = vendorIdToAction != null + ? vendorIdToAction.get(gdprHostVendorId) + : null; + + return hostActions != null && !hostActions.isBlockPixelSync(); + } + + private Set addAllCoopSyncBidders(List bidders) { + final Set updatedBidders = new HashSet<>(setOfCoopSyncBidders); updatedBidders.addAll(bidders); return updatedBidders; } @@ -246,38 +349,61 @@ private Set addCoopSyncBidders(List bidders, int limit) { return allBidders; } - /** - * Determines original bidder's name. - */ - private String bidderNameFor(String bidder) { - return bidderCatalog.isAlias(bidder) ? bidderCatalog.nameByAlias(bidder) : bidder; - } + private void respondByTcfResponse(AsyncResult hostTcfResponseResult, + Set biddersToSync, + CookieSyncContext cookieSyncContext) { - private Future handleVendorIdResult(TcfResponse tcfResponse) { + final TcfContext tcfContext = cookieSyncContext.getPrivacyContext().getTcfContext(); + if (hostTcfResponseResult.succeeded()) { - final Map vendorIdToAction = tcfResponse.getActions(); - final PrivacyEnforcementAction hostActions = vendorIdToAction != null - ? vendorIdToAction.get(gdprHostVendorId) - : null; + // Host vendor tcf response can be not populated if host vendor id is not defined, + // we can't be sure if we can use it. So we get tcf values from response for all bidders. + final HostVendorTcfResponse hostVendorTcfResponse = hostTcfResponseResult.result(); + if (hostVendorTcfResponse.isVendorAllowed()) { + + final AccountPrivacyConfig accountPrivacyConfig = cookieSyncContext.getAccount().getPrivacy(); + final AccountGdprConfig accountGdprConfig = + accountPrivacyConfig != null ? accountPrivacyConfig.getGdpr() : null; + tcfDefinerService.resultForBidderNames(biddersToSync, tcfContext, accountGdprConfig) + .setHandler(tcfResponseResult -> respondByTcfResultForBidders(tcfResponseResult, + biddersToSync, cookieSyncContext)); + } else { + // Reject all bidders when Host TCF response has blocked pixel + final RejectedBidders rejectedBidders = RejectedBidders.of(biddersToSync, Collections.emptySet()); + respondWithRejectedBidders(cookieSyncContext, biddersToSync, rejectedBidders); + } - if (hostActions == null || hostActions.isBlockPixelSync()) { - return Future.failedFuture("host vendor should be allowed by TCF verification"); + } else { + final Throwable error = hostTcfResponseResult.cause(); + final RoutingContext routingContext = cookieSyncContext.getRoutingContext(); + handleErrors(error, routingContext, tcfContext); } + } + + private void respondByTcfResultForBidders(AsyncResult> tcfResponseResult, + Set biddersToSync, + CookieSyncContext cookieSyncContext) { + if (tcfResponseResult.succeeded()) { + final TcfResponse tcfResponse = tcfResponseResult.result(); + + final RejectedBidders rejectedBidders = rejectedRequestBiddersToSync(tcfResponse, cookieSyncContext, + biddersToSync); - return Future.succeededFuture(); + respondWithRejectedBidders(cookieSyncContext, biddersToSync, rejectedBidders); + } else { + final Throwable error = tcfResponseResult.cause(); + final RoutingContext routingContext = cookieSyncContext.getRoutingContext(); + final TcfContext tcfContext = cookieSyncContext.getPrivacyContext().getTcfContext(); + handleErrors(error, routingContext, tcfContext); + } } - /** - * Handles TCF verification result. - */ - private Void handleBidderNamesResult(TcfResponse tcfResponse, - RoutingContext context, - Account account, - UidsCookie uidsCookie, - Set biddersToSync, - Privacy privacy, - Integer limit) { + private RejectedBidders rejectedRequestBiddersToSync(TcfResponse tcfResponse, + CookieSyncContext cookieSyncContext, + Set biddersToSync) { + final Account account = cookieSyncContext.getAccount(); + final Privacy privacy = cookieSyncContext.getPrivacyContext().getPrivacy(); final Set ccpaEnforcedBidders = extractCcpaEnforcedBidders(account, biddersToSync, privacy); final Map bidderNameToAction = tcfResponse.getActions(); @@ -289,146 +415,121 @@ private Void handleBidderNamesResult(TcfResponse tcfResponse, || bidderNameToAction.get(bidder).isBlockPixelSync()) .collect(Collectors.toSet()); - respondWith(context, uidsCookie, privacy, biddersToSync, biddersRejectedByTcf, ccpaEnforcedBidders, limit); - - return null; + return RejectedBidders.of(biddersRejectedByTcf, ccpaEnforcedBidders); } - private Void handleTcfError(RoutingContext context, - UidsCookie uidsCookie, - Set biddersToSync, - Privacy privacy, - Integer limit) { - - respondWith(context, uidsCookie, privacy, biddersToSync, biddersToSync, Collections.emptySet(), limit); - - return null; + private Set extractCcpaEnforcedBidders(Account account, Collection biddersToSync, Privacy privacy) { + if (privacyEnforcementService.isCcpaEnforced(privacy.getCcpa(), account)) { + return biddersToSync.stream() + .filter(bidder -> bidderCatalog.bidderInfoByName(bidder).isCcpaEnforced()) + .collect(Collectors.toSet()); + } + return Collections.emptySet(); } /** * Make HTTP response for given bidders. */ - private void respondWith(RoutingContext context, - UidsCookie uidsCookie, - Privacy privacy, - Collection bidders, - Set biddersRejectedByTcf, - Set biddersRejectedByCcpa, - Integer limit) { + private void respondWithRejectedBidders(CookieSyncContext cookieSyncContext, + Collection bidders, + RejectedBidders rejectedBidders) { - updateCookieSyncTcfMetrics(bidders, biddersRejectedByTcf); + updateCookieSyncTcfMetrics(bidders, rejectedBidders.getRejectedByTcf()); + final UidsCookie uidsCookie = cookieSyncContext.getUidsCookie(); final List bidderStatuses = bidders.stream() - .map(bidder -> bidderStatusFor( - bidder, context, uidsCookie, biddersRejectedByTcf, biddersRejectedByCcpa, privacy)) + .map(bidder -> bidderStatusFor(bidder, cookieSyncContext, rejectedBidders)) .filter(Objects::nonNull) // skip bidder with live UID .collect(Collectors.toList()); + updateCookieSyncMatchMetrics(bidders, bidderStatuses); - final List updatedBidderStatuses; - if (limit != null && limit > 0 && limit < bidderStatuses.size()) { - Collections.shuffle(bidderStatuses); - updatedBidderStatuses = bidderStatuses.subList(0, limit); - } else { - updatedBidderStatuses = bidderStatuses; - } + final List updatedBidderStatuses = + trimBiddersToLimit(bidderStatuses, resolveLimit(cookieSyncContext)); + final String cookieSyncStatus = uidsCookie.hasLiveUids() ? "ok" : "no_cookie"; - final CookieSyncResponse response = CookieSyncResponse.of(uidsCookie.hasLiveUids() ? "ok" : "no_cookie", - updatedBidderStatuses); - final String body = mapper.encode(response); + final HttpResponseStatus status = HttpResponseStatus.OK; + final CookieSyncResponse cookieSyncResponse = CookieSyncResponse.of(cookieSyncStatus, updatedBidderStatuses); + final String body = mapper.encode(cookieSyncResponse); - // don't send the response if client has gone - if (context.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - return; - } - context.response() - .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) - .end(body); + HttpUtil.executeSafely(cookieSyncContext.getRoutingContext(), Endpoint.cookie_sync, + response -> response + .setStatusCode(status.code()) + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .end(body)); - analyticsReporter.processEvent(CookieSyncEvent.builder() - .status(HttpResponseStatus.OK.code()) + final CookieSyncEvent event = CookieSyncEvent.builder() + .status(status.code()) .bidderStatus(updatedBidderStatuses) - .build()); - } - - private Set extractCcpaEnforcedBidders(Account account, Collection biddersToSync, Privacy privacy) { - if (privacyEnforcementService.isCcpaEnforced(privacy.getCcpa(), account)) { - return biddersToSync.stream() - .filter(bidder -> bidderCatalog.bidderInfoByName(bidderNameFor(bidder)).isCcpaEnforced()) - .collect(Collectors.toSet()); - } - return Collections.emptySet(); + .build(); + final TcfContext tcfContext = cookieSyncContext.getPrivacyContext().getTcfContext(); + analyticsDelegator.processEvent(event, tcfContext); } private void updateCookieSyncTcfMetrics(Collection syncBidders, Collection rejectedBidders) { for (String bidder : syncBidders) { if (rejectedBidders.contains(bidder)) { - metrics.updateCookieSyncTcfBlockedMetric(bidder); + metrics.updateCookieSyncTcfBlockedMetric( + bidderCatalog.isValidName(bidder) ? bidder : METRICS_UNKNOWN_BIDDER); } else { metrics.updateCookieSyncGenMetric(bidder); } } } - private void updateCookieSyncMatchMetrics(Collection syncBidders, - Collection requiredUsersyncs) { - syncBidders.stream() - .filter(bidder -> requiredUsersyncs.stream().noneMatch(usersync -> bidder.equals(usersync.getBidder()))) - .forEach(metrics::updateCookieSyncMatchesMetric); - } - /** * Creates {@link BidderUsersyncStatus} for given bidder. */ private BidderUsersyncStatus bidderStatusFor(String bidder, - RoutingContext context, - UidsCookie uidsCookie, - Set biddersRejectedByTcf, - Set biddersRejectedByCcpa, - Privacy privacy) { + CookieSyncContext cookieSyncContext, + RejectedBidders rejectedBidders) { - final boolean isNotAlias = !bidderCatalog.isAlias(bidder); + final Set biddersRejectedByTcf = rejectedBidders.getRejectedByTcf(); + final Set biddersRejectedByCcpa = rejectedBidders.getRejectedByCcpa(); - if (isNotAlias && !bidderCatalog.isValidName(bidder)) { + if (!bidderCatalog.isValidName(bidder)) { return bidderStatusBuilder(bidder) .error("Unsupported bidder") .build(); - } else if (isNotAlias && !bidderCatalog.isActive(bidder)) { + } else if (!bidderCatalog.isActive(bidder)) { return bidderStatusBuilder(bidder) .error(String.format("%s is not configured properly on this Prebid Server deploy. " + "If you believe this should work, contact the company hosting the service " + "and tell them to check their configuration.", bidder)) .build(); - } else if (isNotAlias && biddersRejectedByTcf.contains(bidder)) { + } else if (biddersRejectedByTcf.contains(bidder)) { return bidderStatusBuilder(bidder) .error(REJECTED_BY_TCF) .build(); - } else if (isNotAlias && biddersRejectedByCcpa.contains(bidder)) { + } else if (biddersRejectedByCcpa.contains(bidder)) { return bidderStatusBuilder(bidder) .error(REJECTED_BY_CCPA) .build(); - } else { - final Usersyncer usersyncer = bidderCatalog.usersyncerByName(bidderNameFor(bidder)); + } - if (StringUtils.isEmpty(usersyncer.getUsersyncUrl())) { - // there is nothing to sync - return null; - } + final Usersyncer usersyncer = bidderCatalog.usersyncerByName(bidder); - final UsersyncInfo hostBidderUsersyncInfo = hostBidderUsersyncInfo(context, privacy, usersyncer); + final Usersyncer.UsersyncMethod usersyncMethod = + cookieSyncContext.getUsersyncMethodChooser().choose(usersyncer, bidder); + if (usersyncMethod == null) { + // there is nothing to sync + return null; + } - if (hostBidderUsersyncInfo != null || !uidsCookie.hasLiveUidFrom(usersyncer.getCookieFamilyName())) { - return bidderStatusBuilder(bidder) - .noCookie(true) - .usersync(ObjectUtils.defaultIfNull( - hostBidderUsersyncInfo, - UsersyncInfoAssembler.from(usersyncer).withPrivacy(privacy).assemble())) - .build(); - } + final RoutingContext routingContext = cookieSyncContext.getRoutingContext(); + final UidsCookie uidsCookie = cookieSyncContext.getUidsCookie(); + final String cookieFamilyName = usersyncer.getCookieFamilyName(); + final String uidFromHostCookieToSet = resolveUidFromHostCookie(routingContext, cookieFamilyName); + if (uidFromHostCookieToSet == null && uidsCookie.hasLiveUidFrom(cookieFamilyName)) { + return null; } - return null; + final Privacy privacy = cookieSyncContext.getPrivacyContext().getPrivacy(); + + return bidderStatusBuilder(bidder) + .noCookie(true) + .usersync(toUsersyncInfo(usersyncMethod, cookieFamilyName, uidFromHostCookieToSet, privacy)) + .build(); } private static BidderUsersyncStatus.BidderUsersyncStatusBuilder bidderStatusBuilder(String bidder) { @@ -436,8 +537,7 @@ private static BidderUsersyncStatus.BidderUsersyncStatusBuilder bidderStatusBuil } /** - * Returns {@link UsersyncInfo} with updated usersync-url (pointed directly to Prebid Server /setuid endpoint) - * or null if normal usersync flow should be applied. + * Returns UID from host cookie to sync with uids cookie or null if normal usersync flow should be applied. *

* Uids cookie should be in sync with host-cookie value, so the next conditions must be satisfied: *

@@ -447,37 +547,138 @@ private static BidderUsersyncStatus.BidderUsersyncStatusBuilder bidderStatusBuil *

* 3. Host-bidder uid value in uids cookie should not exist or be different from host-cookie uid value. */ - private UsersyncInfo hostBidderUsersyncInfo(RoutingContext context, Privacy privacy, Usersyncer usersyncer) { - final String cookieFamilyName = usersyncer.getCookieFamilyName(); - if (Objects.equals(cookieFamilyName, uidsCookieService.getHostCookieFamily())) { - - final Map cookies = HttpUtil.cookiesAsMap(context); - final String hostCookieUid = uidsCookieService.parseHostCookie(cookies); - - if (hostCookieUid != null) { - final Uids parsedUids = uidsCookieService.parseUids(cookies); - final Map uidsMap = parsedUids != null ? parsedUids.getUids() : null; - final UidWithExpiry uidWithExpiry = uidsMap != null ? uidsMap.get(cookieFamilyName) : null; - final String uid = uidWithExpiry != null ? uidWithExpiry.getUid() : null; - - if (!Objects.equals(hostCookieUid, uid)) { - final String url = String.format("%s/setuid?bidder=%s&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}" - + "&us_privacy={{us_privacy}}&uid=%s", externalUrl, cookieFamilyName, - HttpUtil.encodeUrl(hostCookieUid)); - return UsersyncInfoAssembler.from(usersyncer) - .withUrl(url) - .withPrivacy(privacy) - .assemble(); - } - } + private String resolveUidFromHostCookie(RoutingContext routingContext, String cookieFamilyName) { + if (!Objects.equals(cookieFamilyName, uidsCookieService.getHostCookieFamily())) { + return null; } - return null; + + final Map cookies = HttpUtil.cookiesAsMap(routingContext); + final String hostCookieUid = uidsCookieService.parseHostCookie(cookies); + + if (hostCookieUid == null) { + return null; + } + + final Uids parsedUids = uidsCookieService.parseUids(cookies); + final Map uidsMap = parsedUids != null ? parsedUids.getUids() : null; + final UidWithExpiry uidWithExpiry = uidsMap != null ? uidsMap.get(cookieFamilyName) : null; + final String uid = uidWithExpiry != null ? uidWithExpiry.getUid() : null; + + if (Objects.equals(hostCookieUid, uid)) { + return null; + } + + return hostCookieUid; } - private Future accountById(String accountId, Timeout timeout) { - return StringUtils.isBlank(accountId) - ? Future.succeededFuture(Account.empty(accountId)) - : applicationSettings.getAccountById(accountId, timeout) - .otherwise(Account.empty(accountId)); + private UsersyncInfo toUsersyncInfo(Usersyncer.UsersyncMethod usersyncMethod, + String cookieFamilyName, + String uidFromHostCookieToSet, + Privacy privacy) { + + final UsersyncInfoAssembler usersyncInfoAssembler = UsersyncInfoAssembler.from(usersyncMethod); + + return (uidFromHostCookieToSet == null + ? usersyncInfoAssembler + : usersyncInfoAssembler + .withUrl(toHostBidderUsersyncUrl(cookieFamilyName, usersyncMethod, uidFromHostCookieToSet))) + .withPrivacy(privacy) + .assemble(); + } + + /** + * Returns updated usersync-url pointed directly to Prebid Server /setuid endpoint. + */ + private String toHostBidderUsersyncUrl(String cookieFamilyName, + Usersyncer.UsersyncMethod usersyncMethod, + String hostCookieUid) { + + final String url = String.format( + HOST_BIDDER_USERSYNC_URL_TEMPLATE, + externalUrl, + cookieFamilyName, + HttpUtil.encodeUrl(hostCookieUid)); + + return UsersyncUtil.enrichUsersyncUrlWithFormat(url, usersyncMethod.getType()); + } + + private void updateCookieSyncMatchMetrics(Collection syncBidders, + Collection requiredUsersyncs) { + syncBidders.stream() + .filter(bidder -> requiredUsersyncs.stream().noneMatch(usersync -> bidder.equals(usersync.getBidder()))) + .forEach(metrics::updateCookieSyncMatchesMetric); + } + + private static Integer resolveLimit(CookieSyncContext cookieSyncContext) { + final Integer limit = cookieSyncContext.getCookieSyncRequest().getLimit(); + + final AccountCookieSyncConfig cookieSyncConfig = cookieSyncContext.getAccount().getCookieSync(); + if (cookieSyncConfig == null) { + return limit; + } + + final Integer resolvedLimit = ObjectUtils.defaultIfNull(limit, cookieSyncConfig.getDefaultLimit()); + if (resolvedLimit == null) { + return null; + } + + final Integer maxLimit = cookieSyncConfig.getMaxLimit(); + + return maxLimit == null ? resolvedLimit : Math.min(resolvedLimit, maxLimit); + } + + private static List trimBiddersToLimit(List bidderStatuses, + Integer limit) { + + if (limit != null && limit > 0 && limit < bidderStatuses.size()) { + Collections.shuffle(bidderStatuses); + return bidderStatuses.subList(0, limit); + } + + return bidderStatuses; + } + + private void handleErrors(Throwable error, RoutingContext routingContext, TcfContext tcfContext) { + final String message = error.getMessage(); + final HttpResponseStatus status; + final String body; + + if (error instanceof InvalidRequestException) { + status = HttpResponseStatus.BAD_REQUEST; + body = String.format("Invalid request format: %s", message); + + metrics.updateUserSyncBadRequestMetric(); + BAD_REQUEST_LOGGER.info(message, 0.01); + } else if (error instanceof UnauthorizedUidsException) { + status = HttpResponseStatus.UNAUTHORIZED; + body = String.format("Unauthorized: %s", message); + + metrics.updateUserSyncOptoutMetric(); + } else { + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + body = String.format("Unexpected setuid processing error: %s", message); + + logger.warn(body, error); + } + + HttpUtil.executeSafely(routingContext, Endpoint.cookie_sync, + response -> response + .setStatusCode(status.code()) + .end(body)); + + final CookieSyncEvent cookieSyncEvent = CookieSyncEvent.error(status.code(), body); + if (tcfContext == null) { + analyticsDelegator.processEvent(cookieSyncEvent); + } else { + analyticsDelegator.processEvent(cookieSyncEvent, tcfContext); + } + } + + @Value(staticConstructor = "of") + private static class RejectedBidders { + + Set rejectedByTcf; + + Set rejectedByCcpa; } } diff --git a/src/main/java/org/prebid/server/handler/CurrencyRatesHandler.java b/src/main/java/org/prebid/server/handler/CurrencyRatesHandler.java index 34e20c2d6bd..fa7d54cafd4 100644 --- a/src/main/java/org/prebid/server/handler/CurrencyRatesHandler.java +++ b/src/main/java/org/prebid/server/handler/CurrencyRatesHandler.java @@ -28,28 +28,29 @@ public class CurrencyRatesHandler implements Handler { private static final Logger logger = LoggerFactory.getLogger(CurrencyRatesHandler.class); private final CurrencyConversionService currencyConversionService; + private final String endpoint; private final JacksonMapper mapper; - public CurrencyRatesHandler(CurrencyConversionService currencyConversionService, JacksonMapper mapper) { + public CurrencyRatesHandler(CurrencyConversionService currencyConversionService, String endpoint, + JacksonMapper mapper) { this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.endpoint = Objects.requireNonNull(endpoint); this.mapper = Objects.requireNonNull(mapper); } @Override - public void handle(RoutingContext context) { + public void handle(RoutingContext routingContext) { try { - context.response().headers().add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); - context.response() - .end(mapper.mapper().writeValueAsString( - Response.of( - currencyConversionService.isExternalRatesActive(), - currencyConversionService.getCurrencyServerUrl(), - toNanos(currencyConversionService.getRefreshPeriod()), - currencyConversionService.getLastUpdated(), - currencyConversionService.getExternalCurrencyRates()))); + final String body = mapper.mapper().writeValueAsString(Response.of( + currencyConversionService.isExternalRatesActive(), + currencyConversionService.getCurrencyServerUrl(), + toNanos(currencyConversionService.getRefreshPeriod()), + currencyConversionService.getLastUpdated(), + currencyConversionService.getExternalCurrencyRates())); + + respondWithOk(routingContext, body); } catch (IOException e) { - logger.error("Critical error when marshaling latest currency rates update response", e); - context.response().setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()).end(); + respondWithServerError(routingContext, e); } } @@ -57,6 +58,25 @@ private static Long toNanos(Long refreshPeriod) { return refreshPeriod != null ? TimeUnit.MILLISECONDS.toNanos(refreshPeriod) : null; } + private void respondWithOk(RoutingContext routingContext, String body) { + respondWith(routingContext, HttpResponseStatus.OK, body); + } + + private void respondWithServerError(RoutingContext routingContext, Throwable exception) { + final String message = "Critical error when marshaling latest currency rates update response"; + logger.error(message, exception); + + respondWith(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, message); + } + + private void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(status.code()) + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .end(body)); + } + @AllArgsConstructor(staticName = "of") @Value private static class Response { diff --git a/src/main/java/org/prebid/server/handler/DealsStatusHandler.java b/src/main/java/org/prebid/server/handler/DealsStatusHandler.java new file mode 100644 index 00000000000..965197e0088 --- /dev/null +++ b/src/main/java/org/prebid/server/handler/DealsStatusHandler.java @@ -0,0 +1,48 @@ +package org.prebid.server.handler; + +import io.netty.handler.codec.http.HttpHeaderValues; +import io.vertx.core.Handler; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import io.vertx.ext.web.RoutingContext; +import org.prebid.server.deals.DeliveryProgressService; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.HttpUtil; + +import java.util.Objects; + +public class DealsStatusHandler implements Handler { + + private static final Logger logger = LoggerFactory.getLogger(DealsStatusHandler.class); + + private final DeliveryProgressService deliveryProgressService; + private final JacksonMapper mapper; + + public DealsStatusHandler(DeliveryProgressService deliveryProgressService, JacksonMapper mapper) { + this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public void handle(RoutingContext routingContext) { + final DeliveryProgressReport deliveryProgressReport = deliveryProgressService + .getOverallDeliveryProgressReport(); + final String body = mapper.encode(deliveryProgressReport); + + // don't send the response if client has gone + if (routingContext.response().closed()) { + logger.warn("The client already closed connection, response will be skipped"); + return; + } + + routingContext.response() + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .exceptionHandler(this::handleResponseException) + .end(body); + } + + private void handleResponseException(Throwable throwable) { + logger.warn("Failed to send deals status response: {0}", throwable.getMessage()); + } +} diff --git a/src/main/java/org/prebid/server/handler/ExceptionHandler.java b/src/main/java/org/prebid/server/handler/ExceptionHandler.java index 70bf0abe3cd..5a4ba845d08 100644 --- a/src/main/java/org/prebid/server/handler/ExceptionHandler.java +++ b/src/main/java/org/prebid/server/handler/ExceptionHandler.java @@ -3,6 +3,7 @@ import io.vertx.core.Handler; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.metric.Metrics; import java.util.Objects; @@ -11,7 +12,7 @@ public class ExceptionHandler implements Handler { private static final Logger logger = LoggerFactory.getLogger(ExceptionHandler.class); - private Metrics metrics; + private final Metrics metrics; public ExceptionHandler(Metrics metrics) { this.metrics = Objects.requireNonNull(metrics); @@ -23,7 +24,13 @@ public static ExceptionHandler create(Metrics metrics) { @Override public void handle(Throwable exception) { - logger.warn("Error while establishing HTTP connection", exception); + logger.warn("Generic error handler: {0}, cause: {1}", + errorMessageFrom(exception), errorMessageFrom(exception.getCause())); metrics.updateConnectionAcceptErrors(); } + + private static String errorMessageFrom(Throwable exception) { + final String message = exception != null ? exception.getMessage() : null; + return StringUtils.defaultIfEmpty(message, "''"); + } } diff --git a/src/main/java/org/prebid/server/handler/GetuidsHandler.java b/src/main/java/org/prebid/server/handler/GetuidsHandler.java index 38c5d26a41f..4c26e36b507 100644 --- a/src/main/java/org/prebid/server/handler/GetuidsHandler.java +++ b/src/main/java/org/prebid/server/handler/GetuidsHandler.java @@ -2,14 +2,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.vertx.core.Handler; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.model.Endpoint; +import org.prebid.server.util.HttpUtil; import java.util.Map; import java.util.Objects; @@ -17,8 +17,6 @@ public class GetuidsHandler implements Handler { - private static final Logger logger = LoggerFactory.getLogger(GetuidsHandler.class); - private final UidsCookieService uidsCookieService; private final JacksonMapper mapper; @@ -28,21 +26,21 @@ public GetuidsHandler(UidsCookieService uidsCookieService, JacksonMapper mapper) } @Override - public void handle(RoutingContext context) { - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(context); - final Map uids = uidsCookie.getCookieUids().getUids().entrySet().stream() + public void handle(RoutingContext routingContext) { + final Map uids = uidsFrom(routingContext); + final String body = mapper.encode(BuyerUids.of(uids)); + + HttpUtil.executeSafely(routingContext, Endpoint.getuids, response -> response + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpUtil.APPLICATION_JSON_CONTENT_TYPE) + .end(body)); + } + + private Map uidsFrom(RoutingContext routingContext) { + final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + return uidsCookie.getCookieUids().getUids().entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, // Extract just the uid for each bidder uidEntry -> uidEntry.getValue().getUid())); - - final String body = mapper.encode(BuyerUids.of(uids)); - - // don't send the response if client has gone - if (context.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - return; - } - context.response().end(body); } @AllArgsConstructor(staticName = "of") diff --git a/src/main/java/org/prebid/server/handler/HttpInteractionLogHandler.java b/src/main/java/org/prebid/server/handler/HttpInteractionLogHandler.java index 45cd8041733..84e92b76141 100644 --- a/src/main/java/org/prebid/server/handler/HttpInteractionLogHandler.java +++ b/src/main/java/org/prebid/server/handler/HttpInteractionLogHandler.java @@ -3,10 +3,12 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.log.model.HttpLogSpec; +import org.prebid.server.util.HttpUtil; import java.util.Arrays; import java.util.Objects; @@ -21,15 +23,17 @@ public class HttpInteractionLogHandler implements Handler { private final int maxLimit; private final HttpInteractionLogger httpInteractionLogger; + private final String endpoint; - public HttpInteractionLogHandler(int maxLimit, HttpInteractionLogger httpInteractionLogger) { + public HttpInteractionLogHandler(int maxLimit, HttpInteractionLogger httpInteractionLogger, String endpoint) { this.maxLimit = maxLimit; this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); + this.endpoint = Objects.requireNonNull(endpoint); } @Override - public void handle(RoutingContext context) { - final MultiMap parameters = context.request().params(); + public void handle(RoutingContext routingContext) { + final MultiMap parameters = routingContext.request().params(); try { httpInteractionLogger.setSpec(HttpLogSpec.of( @@ -38,12 +42,15 @@ public void handle(RoutingContext context) { readAccount(parameters), readBidder(parameters), readLimit(parameters))); + + HttpUtil.executeSafely(routingContext, endpoint, + HttpServerResponse::end); } catch (InvalidRequestException e) { - context.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end(e.getMessage()); - return; + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end(e.getMessage())); } - - context.response().end(); } private HttpLogSpec.Endpoint readEndpoint(MultiMap parameters) { diff --git a/src/main/java/org/prebid/server/handler/LineItemStatusHandler.java b/src/main/java/org/prebid/server/handler/LineItemStatusHandler.java new file mode 100644 index 00000000000..b32ae8b9cc2 --- /dev/null +++ b/src/main/java/org/prebid/server/handler/LineItemStatusHandler.java @@ -0,0 +1,79 @@ +package org.prebid.server.handler; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Handler; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import io.vertx.ext.web.RoutingContext; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.deals.DeliveryProgressService; +import org.prebid.server.deals.proto.report.LineItemStatusReport; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.HttpUtil; + +import java.time.ZonedDateTime; +import java.util.Objects; + +public class LineItemStatusHandler implements Handler { + + private static final Logger logger = LoggerFactory.getLogger(LineItemStatusHandler.class); + + private static final String ID_PARAM = "id"; + private static final String PG_SIM_TIMESTAMP = "pg-sim-timestamp"; + + private final DeliveryProgressService deliveryProgressService; + private final JacksonMapper mapper; + private final String endpoint; + + public LineItemStatusHandler(DeliveryProgressService deliveryProgressService, JacksonMapper mapper, + String endpoint) { + this.deliveryProgressService = Objects.requireNonNull(deliveryProgressService); + this.mapper = Objects.requireNonNull(mapper); + this.endpoint = Objects.requireNonNull(endpoint); + } + + @Override + public void handle(RoutingContext routingContext) { + routingContext.response() + .exceptionHandler(LineItemStatusHandler::handleResponseException); + + final String lineItemId = lineItemIdFrom(routingContext); + if (StringUtils.isEmpty(lineItemId)) { + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end(String.format("%s parameter is required", ID_PARAM))); + return; + } + + try { + final ZonedDateTime time = HttpUtil.getDateFromHeader(routingContext.request().headers(), PG_SIM_TIMESTAMP); + final LineItemStatusReport report = deliveryProgressService.getLineItemStatusReport(lineItemId, time); + + HttpUtil.headers().forEach(entry -> routingContext.response().putHeader(entry.getKey(), entry.getValue())); + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.OK.code()) + .end(mapper.encode(report))); + } catch (PreBidException e) { + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end(e.getMessage())); + } catch (Exception e) { + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) + .end(e.getMessage())); + } + } + + private static String lineItemIdFrom(RoutingContext routingContext) { + return routingContext.request().getParam(ID_PARAM); + } + + private static void handleResponseException(Throwable exception) { + logger.warn("Failed to send line item status response: {0}", exception.getMessage()); + } +} diff --git a/src/main/java/org/prebid/server/handler/LoggerControlKnobHandler.java b/src/main/java/org/prebid/server/handler/LoggerControlKnobHandler.java index dff04531364..4e54ffb0502 100644 --- a/src/main/java/org/prebid/server/handler/LoggerControlKnobHandler.java +++ b/src/main/java/org/prebid/server/handler/LoggerControlKnobHandler.java @@ -3,9 +3,11 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.log.LoggerControlKnob; +import org.prebid.server.util.HttpUtil; import java.time.Duration; import java.util.Arrays; @@ -24,24 +26,28 @@ public class LoggerControlKnobHandler implements Handler { private final long maxDurationMs; private final LoggerControlKnob loggerControlKnob; + private final String endpoint; - public LoggerControlKnobHandler(long maxDurationMs, LoggerControlKnob loggerControlKnob) { + public LoggerControlKnobHandler(long maxDurationMs, LoggerControlKnob loggerControlKnob, String endpoint) { this.maxDurationMs = maxDurationMs; this.loggerControlKnob = Objects.requireNonNull(loggerControlKnob); + this.endpoint = Objects.requireNonNull(endpoint); } @Override - public void handle(RoutingContext context) { - final MultiMap parameters = context.request().params(); - + public void handle(RoutingContext routingContext) { try { + final MultiMap parameters = routingContext.request().params(); loggerControlKnob.changeLogLevel(readLevel(parameters), readDuration(parameters)); + + HttpUtil.executeSafely(routingContext, endpoint, + HttpServerResponse::end); } catch (InvalidRequestException e) { - context.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end(e.getMessage()); - return; + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end(e.getMessage())); } - - context.response().end(); } private String readLevel(MultiMap parameters) { diff --git a/src/main/java/org/prebid/server/handler/NoCacheHandler.java b/src/main/java/org/prebid/server/handler/NoCacheHandler.java index 99bb6c9b847..e708fc46730 100644 --- a/src/main/java/org/prebid/server/handler/NoCacheHandler.java +++ b/src/main/java/org/prebid/server/handler/NoCacheHandler.java @@ -11,11 +11,11 @@ public static NoCacheHandler create() { } @Override - public void handle(RoutingContext context) { - context.response() + public void handle(RoutingContext routingContext) { + routingContext.response() .putHeader(HttpUtil.CACHE_CONTROL_HEADER, "no-cache, no-store, must-revalidate") .putHeader(HttpUtil.PRAGMA_HEADER, "no-cache") .putHeader(HttpUtil.EXPIRES_HEADER, "0"); - context.next(); + routingContext.next(); } } diff --git a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java index 72993a9abc8..67ccb151602 100644 --- a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java +++ b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java @@ -6,20 +6,29 @@ import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerResponse; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.HttpContext; import org.prebid.server.analytics.model.NotificationEvent; +import org.prebid.server.cookie.UidsCookieService; +import org.prebid.server.deals.UserService; +import org.prebid.server.deals.events.ApplicationEventService; import org.prebid.server.events.EventRequest; import org.prebid.server.events.EventUtil; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.model.Endpoint; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountEventsConfig; +import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ResourceUtil; import java.io.IOException; @@ -38,18 +47,30 @@ public class NotificationEventHandler implements Handler { private static final long DEFAULT_TIMEOUT = 1000L; - private final AnalyticsReporter analyticsReporter; + private final UidsCookieService uidsCookieService; + private final ApplicationEventService applicationEventService; + private final UserService userService; + private final AnalyticsReporterDelegator analyticsDelegator; private final TimeoutFactory timeoutFactory; private final ApplicationSettings applicationSettings; + private final boolean dealsEnabled; private final TrackingPixel trackingPixel; - public NotificationEventHandler(AnalyticsReporter analyticsReporter, + public NotificationEventHandler(UidsCookieService uidsCookieService, + ApplicationEventService applicationEventService, + UserService userService, + AnalyticsReporterDelegator analyticsDelegator, TimeoutFactory timeoutFactory, - ApplicationSettings applicationSettings) { + ApplicationSettings applicationSettings, + boolean dealsEnabled) { - this.analyticsReporter = Objects.requireNonNull(analyticsReporter); + this.uidsCookieService = Objects.requireNonNull(uidsCookieService); + this.applicationEventService = applicationEventService; + this.userService = userService; + this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.applicationSettings = Objects.requireNonNull(applicationSettings); + this.dealsEnabled = dealsEnabled; trackingPixel = createTrackingPixel(); } @@ -66,31 +87,29 @@ private static TrackingPixel createTrackingPixel() { } @Override - public void handle(RoutingContext context) { + public void handle(RoutingContext routingContext) { try { - EventUtil.validateType(context); - EventUtil.validateBidId(context); - EventUtil.validateTimestamp(context); - EventUtil.validateFormat(context); - EventUtil.validateAnalytics(context); - EventUtil.validateIntegration(context); + EventUtil.validateType(routingContext); + EventUtil.validateBidId(routingContext); + EventUtil.validateTimestamp(routingContext); + EventUtil.validateFormat(routingContext); + EventUtil.validateAnalytics(routingContext); + EventUtil.validateIntegration(routingContext); } catch (IllegalArgumentException e) { - respondWithBadStatus(context, e.getMessage()); + respondWithBadRequest(routingContext, e.getMessage()); return; } try { - EventUtil.validateAccountId(context); + EventUtil.validateAccountId(routingContext); } catch (IllegalArgumentException e) { - respondWithUnauthorized(context, e.getMessage()); + respondWithUnauthorized(routingContext, e.getMessage()); return; } - final EventRequest eventRequest = EventUtil.from(context); - if (eventRequest.getAnalytics() == EventRequest.Analytics.enabled) { - getAccountById(eventRequest.getAccountId()) - .setHandler(async -> handleEvent(async, eventRequest, context)); - } + final EventRequest eventRequest = EventUtil.from(routingContext); + getAccountById(eventRequest.getAccountId()) + .setHandler(async -> handleEvent(async, eventRequest, routingContext)); } /** @@ -106,64 +125,98 @@ private Future getAccountById(String accountId) { */ private static Future handleAccountExceptionOrFallback(Throwable exception, String accountId) { if (exception instanceof PreBidException) { - return Future.succeededFuture(Account.builder().id(accountId).eventsEnabled(false).build()); + return Future.succeededFuture(Account.builder() + .id(accountId) + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(false)) + .build()) + .build()); } - logger.warn("Error occurred while fetching account", exception); return Future.failedFuture(exception); } - private void handleEvent(AsyncResult async, EventRequest eventRequest, RoutingContext context) { + private void handleEvent(AsyncResult async, EventRequest eventRequest, RoutingContext routingContext) { if (async.failed()) { - respondWithServerError(context, async.cause()); + respondWithServerError(routingContext, "Error occurred while fetching account", async.cause()); } else { final Account account = async.result(); - if (Objects.equals(account.getEventsEnabled(), true)) { + final String lineItemId = eventRequest.getLineItemId(); + final String bidId = eventRequest.getBidId(); + if (dealsEnabled && lineItemId != null) { + applicationEventService.publishLineItemWinEvent(lineItemId); + userService.processWinEvent(lineItemId, bidId, uidsCookieService.parseFromRequest(routingContext)); + } + + boolean eventsEnabledForAccount = Objects.equals(accountEventsEnabled(account), true); + boolean eventsEnabledForRequest = eventRequest.getAnalytics() == EventRequest.Analytics.enabled; + + if (!eventsEnabledForAccount && eventsEnabledForRequest) { + respondWithUnauthorized(routingContext, + String.format("Account '%s' doesn't support events", account.getId())); + return; + } + + final EventRequest.Type eventType = eventRequest.getType(); + if (eventsEnabledForRequest) { final NotificationEvent notificationEvent = NotificationEvent.builder() - .type(eventRequest.getType() == EventRequest.Type.win + .type(eventType == EventRequest.Type.win ? NotificationEvent.Type.win : NotificationEvent.Type.imp) .bidId(eventRequest.getBidId()) .account(account) .bidder(eventRequest.getBidder()) .timestamp(eventRequest.getTimestamp()) .integration(eventRequest.getIntegration()) - .httpContext(HttpContext.from(context)) + .httpContext(HttpContext.from(routingContext)) + .lineItemId(lineItemId) .build(); - analyticsReporter.processEvent(notificationEvent); - respondWithOkStatus(context, eventRequest.getFormat() == EventRequest.Format.image); - } else { - respondWithUnauthorized(context, String.format("Account '%s' doesn't support events", account.getId())); + analyticsDelegator.processEvent(notificationEvent); + } + respondWithOk(routingContext, eventRequest.getFormat() == EventRequest.Format.image); } } - private void respondWithOkStatus(RoutingContext context, boolean respondWithPixel) { + private static Boolean accountEventsEnabled(Account account) { + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + final AccountEventsConfig accountEventsConfig = + accountAuctionConfig != null ? accountAuctionConfig.getEvents() : null; + + return accountEventsConfig != null ? accountEventsConfig.getEnabled() : null; + } + + private void respondWithOk(RoutingContext routingContext, boolean respondWithPixel) { if (respondWithPixel) { - context.response() - .putHeader(HttpHeaders.CONTENT_TYPE, trackingPixel.getContentType()) - .end(Buffer.buffer(trackingPixel.getContent())); + HttpUtil.executeSafely(routingContext, Endpoint.event, + response -> response + .putHeader(HttpHeaders.CONTENT_TYPE, trackingPixel.getContentType()) + .end(Buffer.buffer(trackingPixel.getContent()))); } else { - context.response().end(); + HttpUtil.executeSafely(routingContext, Endpoint.event, + HttpServerResponse::end); } } - private static void respondWithBadStatus(RoutingContext context, String message) { - respondWithError(context, HttpResponseStatus.BAD_REQUEST, message); + private static void respondWithBadRequest(RoutingContext routingContext, String message) { + respondWith(routingContext, HttpResponseStatus.BAD_REQUEST, message); } - private static void respondWithUnauthorized(RoutingContext context, String message) { - respondWithError(context, HttpResponseStatus.UNAUTHORIZED, message); + private static void respondWithUnauthorized(RoutingContext routingContext, String message) { + respondWith(routingContext, HttpResponseStatus.UNAUTHORIZED, message); } - private static void respondWithServerError(RoutingContext context, Throwable exception) { - final String message = "Error occurred while fetching account"; + private static void respondWithServerError(RoutingContext routingContext, String message, Throwable exception) { logger.warn(message, exception); - respondWithError(context, HttpResponseStatus.INTERNAL_SERVER_ERROR, message); + final String body = String.format("%s: %s", message, exception.getMessage()); + respondWith(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, body); } - private static void respondWithError(RoutingContext context, HttpResponseStatus status, String message) { - context.response().setStatusCode(status.code()).end(message); + private static void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { + HttpUtil.executeSafely(routingContext, Endpoint.event, + response -> response + .setStatusCode(status.code()) + .end(body)); } /** diff --git a/src/main/java/org/prebid/server/handler/OptoutHandler.java b/src/main/java/org/prebid/server/handler/OptoutHandler.java index 8d5261f94f5..35cb2748070 100644 --- a/src/main/java/org/prebid/server/handler/OptoutHandler.java +++ b/src/main/java/org/prebid/server/handler/OptoutHandler.java @@ -1,6 +1,7 @@ package org.prebid.server.handler; import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.http.Cookie; import io.vertx.core.logging.Logger; @@ -9,7 +10,9 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.cookie.UidsCookieService; +import org.prebid.server.model.Endpoint; import org.prebid.server.optout.GoogleRecaptchaVerifier; +import org.prebid.server.optout.model.RecaptchaResponse; import org.prebid.server.util.HttpUtil; import java.net.MalformedURLException; @@ -39,54 +42,59 @@ public OptoutHandler(GoogleRecaptchaVerifier googleRecaptchaVerifier, UidsCookie } @Override - public void handle(RoutingContext context) { - final String recaptcha = getRequestParam(context, RECAPTCHA_PARAM); + public void handle(RoutingContext routingContext) { + final String recaptcha = getRequestParam(routingContext, RECAPTCHA_PARAM); if (StringUtils.isBlank(recaptcha)) { - sendRedirect(context); + respondWithRedirect(routingContext); return; } googleRecaptchaVerifier.verify(recaptcha) - .setHandler(result -> { - if (result.failed()) { - sendUnauthorized(context, result.cause()); - } else { - final boolean optout = isOptout(context); - sendResponse(context, optCookie(optout, context), optUrl(optout)); - } - }); + .setHandler(result -> handleVerification(routingContext, result)); } - private void sendRedirect(RoutingContext context) { - context.response() - .putHeader(HttpUtil.LOCATION_HEADER, optoutRedirectUrl) - .setStatusCode(HttpResponseStatus.MOVED_PERMANENTLY.code()) - .end(); + private void handleVerification(RoutingContext routingContext, AsyncResult result) { + if (result.failed()) { + respondWithUnauthorized(routingContext, result.cause()); + } else { + final boolean optout = isOptout(routingContext); + respondWithRedirectAndCookie(routingContext, optCookie(optout, routingContext), optUrl(optout)); + } + } + + private void respondWithRedirect(RoutingContext routingContext) { + HttpUtil.executeSafely(routingContext, Endpoint.optout, + response -> response + .setStatusCode(HttpResponseStatus.MOVED_PERMANENTLY.code()) + .putHeader(HttpUtil.LOCATION_HEADER, optoutRedirectUrl) + .end()); } - private void sendUnauthorized(RoutingContext context, Throwable cause) { - logger.warn("Opt Out failed optout", cause); - context.response() - .setStatusCode(HttpResponseStatus.UNAUTHORIZED.code()) - .end(); + private void respondWithUnauthorized(RoutingContext routingContext, Throwable exception) { + logger.warn("Opt Out failed optout", exception); + HttpUtil.executeSafely(routingContext, Endpoint.optout, + response -> response + .setStatusCode(HttpResponseStatus.UNAUTHORIZED.code()) + .end()); } - private void sendResponse(RoutingContext context, Cookie cookie, String url) { - context.response() - .putHeader(HttpUtil.LOCATION_HEADER, url) - .putHeader(HttpUtil.SET_COOKIE_HEADER, HttpUtil.toSetCookieHeaderValue(cookie)) - .setStatusCode(HttpResponseStatus.MOVED_PERMANENTLY.code()) - .end(); + private void respondWithRedirectAndCookie(RoutingContext routingContext, Cookie cookie, String url) { + HttpUtil.executeSafely(routingContext, Endpoint.optout, + response -> response + .setStatusCode(HttpResponseStatus.MOVED_PERMANENTLY.code()) + .putHeader(HttpUtil.LOCATION_HEADER, url) + .putHeader(HttpUtil.SET_COOKIE_HEADER, HttpUtil.toSetCookieHeaderValue(cookie)) + .end()); } - private static boolean isOptout(RoutingContext context) { - final String optoutValue = getRequestParam(context, OPTOUT_PARAM); + private static boolean isOptout(RoutingContext routingContext) { + final String optoutValue = getRequestParam(routingContext, OPTOUT_PARAM); return StringUtils.isNotEmpty(optoutValue); } - private Cookie optCookie(boolean optout, RoutingContext context) { + private Cookie optCookie(boolean optout, RoutingContext routingContext) { final UidsCookie uidsCookie = uidsCookieService - .parseFromRequest(context) + .parseFromRequest(routingContext) .updateOptout(optout); return uidsCookieService.toCookie(uidsCookie); } @@ -95,9 +103,9 @@ private String optUrl(boolean optout) { return optout ? optoutUrl : optinUrl; } - private static String getRequestParam(RoutingContext context, String paramName) { - final String recaptcha = context.request().getFormAttribute(paramName); - return StringUtils.isNotEmpty(recaptcha) ? recaptcha : context.request().getParam(paramName); + private static String getRequestParam(RoutingContext routingContext, String paramName) { + final String recaptcha = routingContext.request().getFormAttribute(paramName); + return StringUtils.isNotEmpty(recaptcha) ? recaptcha : routingContext.request().getParam(paramName); } public static String getOptoutRedirectUrl(String externalUrl) { diff --git a/src/main/java/org/prebid/server/handler/SettingsCacheNotificationHandler.java b/src/main/java/org/prebid/server/handler/SettingsCacheNotificationHandler.java index 4573967a90f..6c6674795b7 100644 --- a/src/main/java/org/prebid/server/handler/SettingsCacheNotificationHandler.java +++ b/src/main/java/org/prebid/server/handler/SettingsCacheNotificationHandler.java @@ -20,33 +20,36 @@ public class SettingsCacheNotificationHandler implements Handler private final CacheNotificationListener cacheNotificationListener; private final JacksonMapper mapper; + private final String endpoint; - public SettingsCacheNotificationHandler(CacheNotificationListener cacheNotificationListener, JacksonMapper mapper) { + public SettingsCacheNotificationHandler(CacheNotificationListener cacheNotificationListener, JacksonMapper mapper, + String endpoint) { this.cacheNotificationListener = Objects.requireNonNull(cacheNotificationListener); this.mapper = Objects.requireNonNull(mapper); + this.endpoint = Objects.requireNonNull(endpoint); } @Override - public void handle(RoutingContext context) { - switch (context.request().method()) { + public void handle(RoutingContext routingContext) { + switch (routingContext.request().method()) { case POST: - doSave(context); + doSave(routingContext); break; case DELETE: - doInvalidate(context); + doInvalidate(routingContext); break; default: - doFail(context); + doFail(routingContext); } } /** - * Propagates updating settings cache + * Propagates updating settings cache. */ - private void doSave(RoutingContext context) { - final Buffer body = context.getBody(); + private void doSave(RoutingContext routingContext) { + final Buffer body = routingContext.getBody(); if (body == null) { - HttpUtil.respondWith(context, HttpResponseStatus.BAD_REQUEST, "Missing update data."); + respondWithBadRequest(routingContext, "Missing update data."); return; } @@ -54,21 +57,21 @@ private void doSave(RoutingContext context) { try { request = mapper.decodeValue(body, UpdateSettingsCacheRequest.class); } catch (DecodeException e) { - HttpUtil.respondWith(context, HttpResponseStatus.BAD_REQUEST, "Invalid update."); + respondWithBadRequest(routingContext, "Invalid update."); return; } cacheNotificationListener.save(request.getRequests(), request.getImps()); - HttpUtil.respondWith(context, HttpResponseStatus.OK, null); + respondWith(routingContext, HttpResponseStatus.OK); } /** - * Propagates invalidating settings cache + * Propagates invalidating settings cache. */ - private void doInvalidate(RoutingContext context) { - final Buffer body = context.getBody(); + private void doInvalidate(RoutingContext routingContext) { + final Buffer body = routingContext.getBody(); if (body == null) { - HttpUtil.respondWith(context, HttpResponseStatus.BAD_REQUEST, "Missing invalidation data."); + respondWithBadRequest(routingContext, "Missing invalidation data."); return; } @@ -76,18 +79,32 @@ private void doInvalidate(RoutingContext context) { try { request = mapper.decodeValue(body, InvalidateSettingsCacheRequest.class); } catch (DecodeException e) { - HttpUtil.respondWith(context, HttpResponseStatus.BAD_REQUEST, "Invalid invalidation."); + respondWithBadRequest(routingContext, "Invalid invalidation."); return; } cacheNotificationListener.invalidate(request.getRequests(), request.getImps()); - HttpUtil.respondWith(context, HttpResponseStatus.OK, null); + respondWith(routingContext, HttpResponseStatus.OK); } /** - * Makes failure response in case of unexpected request + * Makes failure response in case of unexpected request. */ - private void doFail(RoutingContext context) { - HttpUtil.respondWith(context, HttpResponseStatus.METHOD_NOT_ALLOWED, null); + private void doFail(RoutingContext routingContext) { + respondWith(routingContext, HttpResponseStatus.METHOD_NOT_ALLOWED); + } + + private void respondWithBadRequest(RoutingContext routingContext, String body) { + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end(body)); + } + + private void respondWith(RoutingContext routingContext, HttpResponseStatus status) { + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(status.code()) + .end()); } } diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index 1419f2a6aa1..79808537ddd 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -5,24 +5,32 @@ import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.http.Cookie; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerRequest; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.SetuidEvent; import org.prebid.server.auction.PrivacyEnforcementService; +import org.prebid.server.auction.model.SetuidContext; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.UsersyncUtil; import org.prebid.server.bidder.Usersyncer; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.exception.UnauthorizedUidsException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.metric.Metrics; +import org.prebid.server.model.Endpoint; import org.prebid.server.privacy.gdpr.TcfDefinerService; +import org.prebid.server.privacy.gdpr.model.HostVendorTcfResponse; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; +import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.gdpr.model.TcfResponse; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; @@ -31,7 +39,6 @@ import java.util.Collections; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; public class SetuidHandler implements Handler { @@ -40,10 +47,9 @@ public class SetuidHandler implements Handler { private static final String BIDDER_PARAM = "bidder"; private static final String UID_PARAM = "uid"; - private static final String FORMAT_PARAM = "format"; - private static final String IMG_FORMAT_PARAM = "img"; private static final String PIXEL_FILE_PATH = "static/tracking-pixel.png"; private static final String ACCOUNT_PARAM = "account"; + private static final int UNAVAILABLE_FOR_LEGAL_REASONS = 451; private final long defaultTimeout; private final UidsCookieService uidsCookieService; @@ -51,10 +57,10 @@ public class SetuidHandler implements Handler { private final PrivacyEnforcementService privacyEnforcementService; private final TcfDefinerService tcfDefinerService; private final Integer gdprHostVendorId; - private final AnalyticsReporter analyticsReporter; + private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; private final TimeoutFactory timeoutFactory; - private final Set activeCookieFamilyNames; + private final Map cookieNameToSyncType; public SetuidHandler(long defaultTimeout, UidsCookieService uidsCookieService, @@ -63,7 +69,7 @@ public SetuidHandler(long defaultTimeout, PrivacyEnforcementService privacyEnforcementService, TcfDefinerService tcfDefinerService, Integer gdprHostVendorId, - AnalyticsReporter analyticsReporter, + AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, TimeoutFactory timeoutFactory) { @@ -72,50 +78,53 @@ public SetuidHandler(long defaultTimeout, this.applicationSettings = Objects.requireNonNull(applicationSettings); this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); this.tcfDefinerService = Objects.requireNonNull(tcfDefinerService); - this.gdprHostVendorId = gdprHostVendorId; - this.analyticsReporter = Objects.requireNonNull(analyticsReporter); + this.gdprHostVendorId = validateHostVendorId(gdprHostVendorId); + this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - activeCookieFamilyNames = bidderCatalog.names().stream() + cookieNameToSyncType = bidderCatalog.names().stream() .filter(bidderCatalog::isActive) .map(bidderCatalog::usersyncerByName) - .map(Usersyncer::getCookieFamilyName) - .collect(Collectors.toSet()); + .distinct() // built-in aliases looks like bidders with the same usersyncers + .collect(Collectors.toMap(Usersyncer::getCookieFamilyName, SetuidHandler::preferredUserSyncType)); } - @Override - public void handle(RoutingContext context) { - final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(context); - if (!uidsCookie.allowsSync()) { - final int status = HttpResponseStatus.UNAUTHORIZED.code(); - respondWith(context, status, null); - metrics.updateUserSyncOptoutMetric(); - analyticsReporter.processEvent(SetuidEvent.error(status)); - return; + private static Integer validateHostVendorId(Integer gdprHostVendorId) { + if (gdprHostVendorId == null) { + logger.warn("gdpr.host-vendor-id not specified. Will skip host company GDPR checks"); } + return gdprHostVendorId; + } - final String cookieName = context.request().getParam(BIDDER_PARAM); - final boolean isCookieNameBlank = StringUtils.isBlank(cookieName); - if (isCookieNameBlank || !activeCookieFamilyNames.contains(cookieName)) { - final int status = HttpResponseStatus.BAD_REQUEST.code(); - final String body = "\"bidder\" query param is "; - respondWith(context, status, body + (isCookieNameBlank ? "required" : "invalid")); - metrics.updateUserSyncBadRequestMetric(); - analyticsReporter.processEvent(SetuidEvent.error(status)); - return; - } + private static String preferredUserSyncType(Usersyncer usersyncer) { + return usersyncer.getPrimaryMethod().getType(); + } + + @Override + public void handle(RoutingContext routingContext) { + toSetuidContext(routingContext) + .setHandler(setuidContextResult -> handleSetuidContextResult(setuidContextResult, routingContext)); + } - final Set vendorIds = Collections.singleton(gdprHostVendorId); - final String requestAccount = context.request().getParam(ACCOUNT_PARAM); + private Future toSetuidContext(RoutingContext routingContext) { + final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(routingContext); + final HttpServerRequest httpRequest = routingContext.request(); + final String cookieName = httpRequest.getParam(BIDDER_PARAM); + final String requestAccount = httpRequest.getParam(ACCOUNT_PARAM); final Timeout timeout = timeoutFactory.create(defaultTimeout); - accountById(requestAccount, timeout) - .compose(account -> privacyEnforcementService.contextFromSetuidRequest( - context.request(), account, timeout)) - .compose(privacyContext -> tcfDefinerService.resultForVendorIds( - vendorIds, privacyContext.getTcfContext())) - .setHandler(asyncResult -> handleResult(asyncResult, context, uidsCookie, cookieName)); + return accountById(requestAccount, timeout) + .compose(account -> privacyEnforcementService.contextFromSetuidRequest(httpRequest, account, timeout) + .map(privacyContext -> SetuidContext.builder() + .routingContext(routingContext) + .uidsCookie(uidsCookie) + .timeout(timeout) + .account(account) + .cookieName(cookieName) + .syncType(cookieNameToSyncType.get(cookieName)) + .privacyContext(privacyContext) + .build())); } private Future accountById(String accountId, Timeout timeout) { @@ -125,61 +134,116 @@ private Future accountById(String accountId, Timeout timeout) { .otherwise(Account.empty(accountId)); } - private void handleResult(AsyncResult> asyncResult, RoutingContext context, - UidsCookie uidsCookie, String bidder) { - if (asyncResult.failed()) { - respondWithError(context, bidder, asyncResult.cause()); - } else { - // allow cookie only if user is not in GDPR scope or vendor passed GDPR check - final TcfResponse tcfResponse = asyncResult.result(); + private void handleSetuidContextResult(AsyncResult setuidContextResult, + RoutingContext routingContext) { + if (setuidContextResult.succeeded()) { + final SetuidContext setuidContext = setuidContextResult.result(); + final String bidder = setuidContext.getCookieName(); + final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); + + try { + validateSetuidContext(setuidContext, bidder); + } catch (InvalidRequestException | UnauthorizedUidsException e) { + handleErrors(e, routingContext, tcfContext); + return; + } - final boolean notInGdprScope = BooleanUtils.isFalse(tcfResponse.getUserInGdprScope()); + isAllowedForHostVendorId(tcfContext) + .setHandler(hostTcfResponseResult -> respondByTcfResponse(hostTcfResponseResult, setuidContext)); + } else { + final Throwable error = setuidContextResult.cause(); + handleErrors(error, routingContext, null); + } + } - final Map vendorIdToAction = tcfResponse.getActions(); - final PrivacyEnforcementAction privacyEnforcementAction = vendorIdToAction != null - ? vendorIdToAction.get(gdprHostVendorId) - : null; - final boolean blockPixelSync = privacyEnforcementAction == null - || privacyEnforcementAction.isBlockPixelSync(); + private void validateSetuidContext(SetuidContext setuidContext, String bidder) { + final String cookieName = setuidContext.getCookieName(); + final boolean isCookieNameBlank = StringUtils.isBlank(cookieName); + if (isCookieNameBlank || !cookieNameToSyncType.containsKey(cookieName)) { + final String cookieNameError = isCookieNameBlank ? "required" : "invalid"; + throw new InvalidRequestException(String.format("\"bidder\" query param is %s", cookieNameError)); + } - final boolean allowedCookie = notInGdprScope || !blockPixelSync; + final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); + if (StringUtils.equals(tcfContext.getGdpr(), "1") && BooleanUtils.isFalse(tcfContext.getIsConsentValid())) { + metrics.updateUserSyncTcfInvalidMetric(bidder); + throw new InvalidRequestException("Consent string is invalid"); + } - if (allowedCookie) { - respondWithCookie(context, bidder, uidsCookie); - } else { - respondWithoutCookie(context, HttpResponseStatus.OK.code(), - "The gdpr_consent param prevents cookies from being saved", bidder); - } + final UidsCookie uidsCookie = setuidContext.getUidsCookie(); + if (!uidsCookie.allowsSync()) { + throw new UnauthorizedUidsException("Sync is not allowed for this uids"); } } - private void respondWithError(RoutingContext context, String bidder, Throwable exception) { - final int status; - final String body; + /** + * If host vendor id is null, host allowed to setuid. + */ + private Future isAllowedForHostVendorId(TcfContext tcfContext) { + return gdprHostVendorId == null + ? Future.succeededFuture(HostVendorTcfResponse.allowedVendor()) + : tcfDefinerService.resultForVendorIds(Collections.singleton(gdprHostVendorId), tcfContext) + .map(this::toHostVendorTcfResponse); + } - if (exception instanceof InvalidRequestException) { - status = HttpResponseStatus.BAD_REQUEST.code(); - body = String.format("GDPR processing failed with error: %s", exception.getMessage()); - } else { - status = HttpResponseStatus.INTERNAL_SERVER_ERROR.code(); - body = "Unexpected GDPR processing error"; - logger.warn(body, exception); - } + private HostVendorTcfResponse toHostVendorTcfResponse(TcfResponse tcfResponse) { + return HostVendorTcfResponse.of(tcfResponse.getUserInGdprScope(), tcfResponse.getCountry(), + isSetuidAllowed(tcfResponse)); + } + + private boolean isSetuidAllowed(TcfResponse hostTcfResponseToSetuidContext) { + // allow cookie only if user is not in GDPR scope or vendor passed GDPR check + final boolean notInGdprScope = BooleanUtils.isFalse(hostTcfResponseToSetuidContext.getUserInGdprScope()); + + final Map vendorIdToAction = hostTcfResponseToSetuidContext.getActions(); + final PrivacyEnforcementAction hostPrivacyAction = vendorIdToAction != null + ? vendorIdToAction.get(gdprHostVendorId) + : null; + final boolean blockPixelSync = hostPrivacyAction == null || hostPrivacyAction.isBlockPixelSync(); - respondWithoutCookie(context, status, body, bidder); + return notInGdprScope || !blockPixelSync; } - private void respondWithoutCookie(RoutingContext context, int status, String body, String bidder) { - respondWith(context, status, body); - metrics.updateUserSyncTcfBlockedMetric(bidder); - analyticsReporter.processEvent(SetuidEvent.error(status)); + private void respondByTcfResponse(AsyncResult hostTcfResponseResult, + SetuidContext setuidContext) { + final String bidderCookieName = setuidContext.getCookieName(); + final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); + final RoutingContext routingContext = setuidContext.getRoutingContext(); + + if (hostTcfResponseResult.succeeded()) { + final HostVendorTcfResponse hostTcfResponse = hostTcfResponseResult.result(); + if (hostTcfResponse.isVendorAllowed()) { + respondWithCookie(setuidContext); + } else { + metrics.updateUserSyncTcfBlockedMetric(bidderCookieName); + + final HttpResponseStatus status = new HttpResponseStatus(UNAVAILABLE_FOR_LEGAL_REASONS, + "Unavailable for legal reasons"); + + HttpUtil.executeSafely(routingContext, Endpoint.setuid, + response -> response + .setStatusCode(status.code()) + .setStatusMessage(status.reasonPhrase()) + .end("The gdpr_consent param prevents cookies from being saved")); + + analyticsDelegator.processEvent(SetuidEvent.error(status.code()), tcfContext); + } + + } else { + final Throwable error = hostTcfResponseResult.cause(); + metrics.updateUserSyncTcfBlockedMetric(bidderCookieName); + handleErrors(error, routingContext, tcfContext); + } } - private void respondWithCookie(RoutingContext context, String bidder, UidsCookie uidsCookie) { - final String uid = context.request().getParam(UID_PARAM); + private void respondWithCookie(SetuidContext setuidContext) { + final RoutingContext routingContext = setuidContext.getRoutingContext(); + final String uid = routingContext.request().getParam(UID_PARAM); final UidsCookie updatedUidsCookie; boolean successfullyUpdated = false; + final String bidder = setuidContext.getCookieName(); + final UidsCookie uidsCookie = setuidContext.getUidsCookie(); if (StringUtils.isBlank(uid)) { updatedUidsCookie = uidsCookie.deleteUid(bidder); } else if (UidsCookie.isFacebookSentinel(bidder, uid)) { @@ -193,42 +257,71 @@ private void respondWithCookie(RoutingContext context, String bidder, UidsCookie } final Cookie cookie = uidsCookieService.toCookie(updatedUidsCookie); - addCookie(context, cookie); + addCookie(routingContext, cookie); - final int status = HttpResponseStatus.OK.code(); + final HttpResponseStatus status = HttpResponseStatus.OK; - // Send pixel file to response if "format=img" - final String format = context.request().getParam(FORMAT_PARAM); - if (StringUtils.equals(format, IMG_FORMAT_PARAM)) { - context.response().sendFile(PIXEL_FILE_PATH); + final String format = routingContext.request().getParam(UsersyncUtil.FORMAT_PARAMETER); + if (shouldRespondWithPixel(format, setuidContext.getSyncType())) { + HttpUtil.executeSafely(routingContext, Endpoint.setuid, + response -> response + .sendFile(PIXEL_FILE_PATH)); } else { - respondWith(context, status, null); + HttpUtil.executeSafely(routingContext, Endpoint.setuid, + response -> response + .setStatusCode(status.code()) + .putHeader(HttpHeaders.CONTENT_LENGTH, "0") + .putHeader(HttpHeaders.CONTENT_TYPE, HttpHeaders.TEXT_HTML) + .end()); } - analyticsReporter.processEvent(SetuidEvent.builder() - .status(status) + final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); + analyticsDelegator.processEvent(SetuidEvent.builder() + .status(status.code()) .bidder(bidder) .uid(uid) .success(successfullyUpdated) - .build()); + .build(), tcfContext); } - private void addCookie(RoutingContext context, Cookie cookie) { - context.response().headers().add(HttpUtil.SET_COOKIE_HEADER, HttpUtil.toSetCookieHeaderValue(cookie)); + private boolean shouldRespondWithPixel(String format, String syncType) { + return StringUtils.equals(format, UsersyncUtil.IMG_FORMAT) + || (!StringUtils.equals(format, UsersyncUtil.BLANK_FORMAT) + && StringUtils.equals(syncType, Usersyncer.UsersyncMethod.REDIRECT_TYPE)); } - private static void respondWith(RoutingContext context, int status, String body) { - // don't send the response if client has gone - if (context.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - return; + private void handleErrors(Throwable error, RoutingContext routingContext, TcfContext tcfContext) { + final String message = error.getMessage(); + final HttpResponseStatus status; + final String body; + if (error instanceof InvalidRequestException) { + metrics.updateUserSyncBadRequestMetric(); + status = HttpResponseStatus.BAD_REQUEST; + body = String.format("Invalid request format: %s", message); + } else if (error instanceof UnauthorizedUidsException) { + metrics.updateUserSyncOptoutMetric(); + status = HttpResponseStatus.UNAUTHORIZED; + body = String.format("Unauthorized: %s", message); + } else { + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + body = String.format("Unexpected setuid processing error: %s", message); + logger.warn(body, error); } - context.response().setStatusCode(status); - if (body != null) { - context.response().end(body); + HttpUtil.executeSafely(routingContext, Endpoint.setuid, + response -> response + .setStatusCode(status.code()) + .end(body)); + + final SetuidEvent setuidEvent = SetuidEvent.error(status.code()); + if (tcfContext == null) { + analyticsDelegator.processEvent(setuidEvent); } else { - context.response().end(); + analyticsDelegator.processEvent(setuidEvent, tcfContext); } } + + private void addCookie(RoutingContext routingContext, Cookie cookie) { + routingContext.response().headers().add(HttpUtil.SET_COOKIE_HEADER, HttpUtil.toSetCookieHeaderValue(cookie)); + } } diff --git a/src/main/java/org/prebid/server/handler/StatusHandler.java b/src/main/java/org/prebid/server/handler/StatusHandler.java index 80fdf13e053..1c6acab5ea7 100644 --- a/src/main/java/org/prebid/server/handler/StatusHandler.java +++ b/src/main/java/org/prebid/server/handler/StatusHandler.java @@ -1,13 +1,15 @@ package org.prebid.server.handler; +import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.health.HealthChecker; +import org.prebid.server.health.model.StatusResponse; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.model.Endpoint; +import org.prebid.server.util.HttpUtil; import java.util.List; import java.util.Objects; @@ -16,8 +18,6 @@ public class StatusHandler implements Handler { - private static final Logger logger = LoggerFactory.getLogger(StatusHandler.class); - private final List healthCheckers; private final JacksonMapper mapper; @@ -27,21 +27,20 @@ public StatusHandler(List healthCheckers, JacksonMapper mapper) { } @Override - public void handle(RoutingContext context) { - // don't send the response if client has gone - if (context.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - return; - } - + public void handle(RoutingContext routingContext) { if (CollectionUtils.isEmpty(healthCheckers)) { - context.response() - .setStatusCode(HttpResponseStatus.NO_CONTENT.code()) - .end(); + HttpUtil.executeSafely(routingContext, Endpoint.status, + response -> response + .setStatusCode(HttpResponseStatus.NO_CONTENT.code()) + .end()); } else { - context.response() - .end(mapper.encode(new TreeMap<>(healthCheckers.stream() - .collect(Collectors.toMap(HealthChecker::name, HealthChecker::status))))); + final TreeMap nameToStatus = new TreeMap<>(healthCheckers.stream() + .collect(Collectors.toMap(HealthChecker::name, HealthChecker::status))); + + HttpUtil.executeSafely(routingContext, Endpoint.status, + response -> response + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .end(mapper.encode(nameToStatus))); } } } diff --git a/src/main/java/org/prebid/server/handler/TracerLogHandler.java b/src/main/java/org/prebid/server/handler/TracerLogHandler.java new file mode 100644 index 00000000000..a4cd450b84c --- /dev/null +++ b/src/main/java/org/prebid/server/handler/TracerLogHandler.java @@ -0,0 +1,72 @@ +package org.prebid.server.handler; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.ext.web.RoutingContext; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.log.CriteriaManager; + +import java.util.Objects; + +public class TracerLogHandler implements Handler { + + private static final String ACCOUNT_PARAMETER = "account"; + private static final String LINE_ITEM_PARAMETER = "lineItemId"; + private static final String BIDDER_CODE_PARAMETER = "bidderCode"; + private static final String LOG_LEVEL_PARAMETER = "level"; + private static final String DURATION_IN_SECONDS = "duration"; + + private CriteriaManager criteriaManager; + + public TracerLogHandler(CriteriaManager criteriaManager) { + this.criteriaManager = Objects.requireNonNull(criteriaManager); + } + + @Override + public void handle(RoutingContext routingContext) { + final MultiMap parameters = routingContext.request().params(); + final String accountId = parameters.get(ACCOUNT_PARAMETER); + final String bidderCode = parameters.get(BIDDER_CODE_PARAMETER); + final String lineItemId = parameters.get(LINE_ITEM_PARAMETER); + + if (StringUtils.isBlank(accountId) && StringUtils.isBlank(lineItemId) && StringUtils.isBlank(bidderCode)) { + routingContext.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end("At least one parameter should ne defined: account, bidderCode, lineItemId"); + return; + } + + final Integer duration; + final String loggerLevel = parameters.get(LOG_LEVEL_PARAMETER); + try { + duration = parseDuration(parameters.get(DURATION_IN_SECONDS)); + } catch (InvalidRequestException e) { + routingContext.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end(e.getMessage()); + return; + } + + try { + criteriaManager.addCriteria(accountId, bidderCode, lineItemId, loggerLevel, duration); + } catch (IllegalArgumentException e) { + routingContext.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end(String.format("Invalid parameter: %s", e.getMessage())); + return; + } + + routingContext.response().end(); + } + + private static int parseDuration(String rawDuration) { + if (rawDuration == null) { + throw new InvalidRequestException("duration parameter should be defined"); + } + try { + return Integer.parseInt(rawDuration); + } catch (NumberFormatException e) { + throw new InvalidRequestException( + String.format("duration parameter should be defined as integer, but was %s", rawDuration)); + } + + } +} diff --git a/src/main/java/org/prebid/server/handler/VersionHandler.java b/src/main/java/org/prebid/server/handler/VersionHandler.java index f6dfc4182e5..eeeecae349a 100644 --- a/src/main/java/org/prebid/server/handler/VersionHandler.java +++ b/src/main/java/org/prebid/server/handler/VersionHandler.java @@ -1,6 +1,5 @@ package org.prebid.server.handler; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; @@ -11,11 +10,9 @@ import lombok.Value; import org.apache.commons.lang3.StringUtils; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.util.ResourceUtil; +import org.prebid.server.util.HttpUtil; -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.Objects; /** * Handles HTTP request for pbs project version. @@ -24,68 +21,41 @@ public class VersionHandler implements Handler { private static final Logger logger = LoggerFactory.getLogger(VersionHandler.class); - private static final String UNDEFINED = "undefined"; + private final String endpoint; private final String revisionResponseBody; - private VersionHandler(String revisionResponseBody) { - this.revisionResponseBody = revisionResponseBody; + public VersionHandler(String version, String commitHash, JacksonMapper mapper, String endpoint) { + this.endpoint = Objects.requireNonNull(endpoint); + this.revisionResponseBody = createRevisionResponseBody(version, commitHash, mapper); } - public static VersionHandler create(String revisionFilePath, JacksonMapper mapper) { - Revision revision; + private static String createRevisionResponseBody(String version, String commitHash, JacksonMapper mapper) { try { - revision = mapper.mapper().readValue(ResourceUtil.readFromClasspath(revisionFilePath), Revision.class); - } catch (IllegalArgumentException | IOException e) { - logger.error("Was not able to read revision file {0}. Reason: {1}", revisionFilePath, e.getMessage()); - revision = Revision.EMPTY; - } - return new VersionHandler(createRevisionResponseBody(revision, mapper)); - } - - private static String createRevisionResponseBody(Revision revision, JacksonMapper mapper) { - try { - return mapper.mapper().writeValueAsString(RevisionResponse.of( - revision.commitHash != null ? revision.commitHash : UNDEFINED, - revision.pbsVersion != null ? extractVersion(revision.pbsVersion) : UNDEFINED)); + return mapper.mapper().writeValueAsString(RevisionResponse.of(commitHash, version)); } catch (JsonProcessingException e) { logger.error("/version Critical error when trying to marshal revision response: %s", e.getMessage()); return null; } } - private static String extractVersion(String buildVersion) { - final Pattern versionPattern = Pattern.compile("\\d+\\.\\d+\\.\\d"); - final Matcher versionMatcher = versionPattern.matcher(buildVersion); - - return versionMatcher.lookingAt() ? versionMatcher.group() : null; - } - /** * Responds with commit revision. */ @Override - public void handle(RoutingContext context) { + public void handle(RoutingContext routingContext) { if (StringUtils.isNotBlank(revisionResponseBody)) { - context.response().end(revisionResponseBody); + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .end(revisionResponseBody)); } else { - context.response().setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()).end(); + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) + .end()); } } - @AllArgsConstructor(staticName = "of") - @Value - private static class Revision { - - private static final Revision EMPTY = Revision.of(null, null); - - @JsonProperty("git.commit.id") - String commitHash; - - @JsonProperty("git.build.version") - String pbsVersion; - } - @AllArgsConstructor(staticName = "of") @Value private static class RevisionResponse { diff --git a/src/main/java/org/prebid/server/handler/VtrackHandler.java b/src/main/java/org/prebid/server/handler/VtrackHandler.java index 3ed16761d4f..ca657c4bd37 100644 --- a/src/main/java/org/prebid/server/handler/VtrackHandler.java +++ b/src/main/java/org/prebid/server/handler/VtrackHandler.java @@ -1,5 +1,7 @@ package org.prebid.server.handler; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; import io.vertx.core.Future; @@ -22,10 +24,13 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.model.Endpoint; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountEventsConfig; +import org.prebid.server.util.HttpUtil; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; @@ -37,9 +42,11 @@ public class VtrackHandler implements Handler { private static final String ACCOUNT_PARAMETER = "a"; private static final String INTEGRATION_PARAMETER = "int"; + private static final String TYPE_XML = "xml"; private final long defaultTimeout; private final boolean allowUnknownBidder; + private final boolean modifyVastForUnknownBidder; private final ApplicationSettings applicationSettings; private final BidderCatalog bidderCatalog; private final CacheService cacheService; @@ -48,6 +55,7 @@ public class VtrackHandler implements Handler { public VtrackHandler(long defaultTimeout, boolean allowUnknownBidder, + boolean modifyVastForUnknownBidder, ApplicationSettings applicationSettings, BidderCatalog bidderCatalog, CacheService cacheService, @@ -56,6 +64,7 @@ public VtrackHandler(long defaultTimeout, this.defaultTimeout = defaultTimeout; this.allowUnknownBidder = allowUnknownBidder; + this.modifyVastForUnknownBidder = modifyVastForUnknownBidder; this.applicationSettings = Objects.requireNonNull(applicationSettings); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.cacheService = Objects.requireNonNull(cacheService); @@ -64,27 +73,28 @@ public VtrackHandler(long defaultTimeout, } @Override - public void handle(RoutingContext context) { + public void handle(RoutingContext routingContext) { final String accountId; final List vtrackPuts; final String integration; try { - accountId = accountId(context); - vtrackPuts = vtrackPuts(context); - integration = integration(context); + accountId = accountId(routingContext); + vtrackPuts = vtrackPuts(routingContext); + integration = integration(routingContext); } catch (IllegalArgumentException e) { - respondWithBadRequest(context, e.getMessage()); + respondWith(routingContext, HttpResponseStatus.BAD_REQUEST, e.getMessage()); return; } final Timeout timeout = timeoutFactory.create(defaultTimeout); applicationSettings.getAccountById(accountId, timeout) .recover(exception -> handleAccountExceptionOrFallback(exception, accountId)) - .setHandler(async -> handleAccountResult(async, context, vtrackPuts, accountId, integration, timeout)); + .setHandler(async -> handleAccountResult(async, routingContext, vtrackPuts, accountId, integration, + timeout)); } - private static String accountId(RoutingContext context) { - final String accountId = context.request().getParam(ACCOUNT_PARAMETER); + private static String accountId(RoutingContext routingContext) { + final String accountId = routingContext.request().getParam(ACCOUNT_PARAMETER); if (StringUtils.isEmpty(accountId)) { throw new IllegalArgumentException( String.format("Account '%s' is required query parameter and can't be empty", ACCOUNT_PARAMETER)); @@ -92,8 +102,8 @@ private static String accountId(RoutingContext context) { return accountId; } - private List vtrackPuts(RoutingContext context) { - final Buffer body = context.getBody(); + private List vtrackPuts(RoutingContext routingContext) { + final Buffer body = routingContext.getBody(); if (body == null || body.length() == 0) { throw new IllegalArgumentException("Incoming request has no body"); } @@ -107,50 +117,77 @@ private List vtrackPuts(RoutingContext context) { final List putObjects = ListUtils.emptyIfNull(bidCacheRequest.getPuts()); for (PutObject putObject : putObjects) { - if (StringUtils.isEmpty(putObject.getBidid())) { - throw new IllegalArgumentException("'bidid' is required field and can't be empty"); - } - if (StringUtils.isEmpty(putObject.getBidder())) { - throw new IllegalArgumentException("'bidder' is required field and can't be empty"); - } + validatePutObject(putObject); } return putObjects; } - public static String integration(RoutingContext context) { - EventUtil.validateIntegration(context); - return context.request().getParam(INTEGRATION_PARAMETER); + private static void validatePutObject(PutObject putObject) { + if (StringUtils.isEmpty(putObject.getBidid())) { + throw new IllegalArgumentException("'bidid' is required field and can't be empty"); + } + + if (StringUtils.isEmpty(putObject.getBidder())) { + throw new IllegalArgumentException("'bidder' is required field and can't be empty"); + } + + if (!StringUtils.equals(putObject.getType(), TYPE_XML)) { + throw new IllegalArgumentException("vtrack only accepts type xml"); + } + + final JsonNode value = putObject.getValue(); + final String valueAsString = value != null ? value.asText() : null; + if (!StringUtils.containsIgnoreCase(valueAsString, " handleAccountExceptionOrFallback(Throwable exception, String accountId) { - if (exception instanceof PreBidException) { - return Future.succeededFuture(Account.builder().id(accountId).eventsEnabled(false).build()); - } - return Future.failedFuture(exception); + return exception instanceof PreBidException + ? Future.succeededFuture(Account.builder() + .id(accountId) + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(false)) + .build()) + .build()) + : Future.failedFuture(exception); } private void handleAccountResult(AsyncResult asyncAccount, - RoutingContext context, + RoutingContext routingContext, List vtrackPuts, String accountId, String integration, Timeout timeout) { if (asyncAccount.failed()) { - respondWithServerError(context, "Error occurred while fetching account", asyncAccount.cause()); + respondWithServerError(routingContext, "Error occurred while fetching account", asyncAccount.cause()); } else { // insert impression tracking if account allows events and bidder allows VAST modification - final Set biddersAllowingVastUpdate = Objects.equals(asyncAccount.result().getEventsEnabled(), true) - ? biddersAllowingVastUpdate(vtrackPuts) - : Collections.emptySet(); - cacheService.cachePutObjects(vtrackPuts, biddersAllowingVastUpdate, accountId, integration, timeout) - .setHandler(asyncCache -> handleCacheResult(asyncCache, context)); + final Account account = asyncAccount.result(); + final Boolean isEventEnabled = accountEventsEnabled(asyncAccount.result()); + final Set allowedBidders = biddersAllowingVastUpdate(vtrackPuts); + cacheService.cachePutObjects(vtrackPuts, isEventEnabled, allowedBidders, accountId, integration, timeout) + .setHandler(asyncCache -> handleCacheResult(asyncCache, routingContext)); } } + private static Boolean accountEventsEnabled(Account account) { + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + final AccountEventsConfig accountEventsConfig = + accountAuctionConfig != null ? accountAuctionConfig.getEvents() : null; + + return accountEventsConfig != null ? accountEventsConfig.getEnabled() : null; + } + /** * Returns list of bidders that allow VAST XML modification. */ @@ -165,38 +202,33 @@ private boolean isAllowVastForBidder(String bidderName) { if (bidderCatalog.isValidName(bidderName)) { return bidderCatalog.isModifyingVastXmlAllowed(bidderName); } else { - return allowUnknownBidder; + return allowUnknownBidder && modifyVastForUnknownBidder; } } - private void handleCacheResult(AsyncResult async, RoutingContext context) { + private void handleCacheResult(AsyncResult async, RoutingContext routingContext) { if (async.failed()) { - respondWithServerError(context, "Error occurred while sending request to cache", async.cause()); + respondWithServerError(routingContext, "Error occurred while sending request to cache", async.cause()); } else { try { - respondWith(context, HttpResponseStatus.OK, mapper.encode(async.result())); + respondWith(routingContext, HttpResponseStatus.OK, mapper.encode(async.result())); } catch (EncodeException e) { - respondWithServerError(context, "Error occurred while encoding response", e); + respondWithServerError(routingContext, "Error occurred while encoding response", e); } } } - private static void respondWithBadRequest(RoutingContext context, String message) { - respondWith(context, HttpResponseStatus.BAD_REQUEST, message); - } - - private static void respondWithServerError(RoutingContext context, String message, Throwable exception) { - logger.warn(message, exception); - respondWith(context, HttpResponseStatus.INTERNAL_SERVER_ERROR, + private static void respondWithServerError(RoutingContext routingContext, String message, Throwable exception) { + logger.error(message, exception); + respondWith(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, String.format("%s: %s", message, exception.getMessage())); } - private static void respondWith(RoutingContext context, HttpResponseStatus status, String body) { - // don't send the response if client has gone - if (context.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - return; - } - context.response().setStatusCode(status.code()).end(body); + private static void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { + HttpUtil.executeSafely(routingContext, Endpoint.vtrack, + response -> response + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .setStatusCode(status.code()) + .end(body)); } } diff --git a/src/main/java/org/prebid/server/handler/info/BidderDetailsHandler.java b/src/main/java/org/prebid/server/handler/info/BidderDetailsHandler.java index de5e4a5aec7..331f9b5ba48 100644 --- a/src/main/java/org/prebid/server/handler/info/BidderDetailsHandler.java +++ b/src/main/java/org/prebid/server/handler/info/BidderDetailsHandler.java @@ -1,14 +1,17 @@ package org.prebid.server.handler.info; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import lombok.Value; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.model.Endpoint; import org.prebid.server.proto.response.BidderInfo; import org.prebid.server.util.HttpUtil; @@ -22,6 +25,8 @@ public class BidderDetailsHandler implements Handler { + private static final Logger logger = LoggerFactory.getLogger(BidderDetailsHandler.class); + private static final String BIDDER_NAME_PARAM = "bidderName"; private static final String ALL_PARAM_VALUE = "all"; @@ -31,91 +36,79 @@ public class BidderDetailsHandler implements Handler { public BidderDetailsHandler(BidderCatalog bidderCatalog, JacksonMapper mapper) { validateAliases(Objects.requireNonNull(bidderCatalog)); this.mapper = Objects.requireNonNull(mapper); - bidderInfos = createBidderInfos(bidderCatalog); + this.bidderInfos = createBidderInfos(bidderCatalog); } private static void validateAliases(BidderCatalog bidderCatalog) { - if (bidderCatalog.aliases().contains(ALL_PARAM_VALUE)) { + if (bidderCatalog.names().contains(ALL_PARAM_VALUE)) { throw new IllegalArgumentException( - String.format("The '%s' bidder has '%s' alias configured which is unacceptable.", - bidderCatalog.nameByAlias(ALL_PARAM_VALUE), ALL_PARAM_VALUE)); + String.format("There is '%s' bidder or alias configured which is unacceptable.", ALL_PARAM_VALUE)); } } - /** - * Returns a {@link Map} with bidder name (or alias, or "all" keyword) as a key - * and json-encoded string of {@link BidderInfoResponseModel} as a value. - */ private Map createBidderInfos(BidderCatalog bidderCatalog) { final Map nameToInfo = bidderCatalog.names().stream() - .filter(bidderCatalog::isActive) .collect(Collectors.toMap(Function.identity(), name -> bidderNode(bidderCatalog, name))); - final Map aliasToInfo = bidderCatalog.aliases().stream() - .filter(alias -> bidderCatalog.isActive(bidderCatalog.nameByAlias(alias))) - .collect(Collectors.toMap(Function.identity(), alias -> aliasNode(bidderCatalog, alias))); - - final Map allToInfos = Collections.singletonMap( - ALL_PARAM_VALUE, allInfos(nameToInfo, aliasToInfo)); + final Map allToInfos = Collections.singletonMap(ALL_PARAM_VALUE, allInfos(nameToInfo)); - return Stream.of(nameToInfo, aliasToInfo, allToInfos) + return Stream.of(nameToInfo, allToInfos) .flatMap(map -> map.entrySet().stream()) .collect(Collectors.toMap(Map.Entry::getKey, map -> mapper.encode(map.getValue()))); } - /** - * Returns bidder info as {@link ObjectNode}. - */ private ObjectNode bidderNode(BidderCatalog bidderCatalog, String name) { final BidderInfo bidderInfo = bidderCatalog.bidderInfoByName(name); return mapper.mapper().valueToTree(BidderInfoResponseModel.from(bidderInfo)); } - /** - * Returns alias info as {@link ObjectNode}. - */ - private ObjectNode aliasNode(BidderCatalog bidderCatalog, String alias) { - final String name = bidderCatalog.nameByAlias(alias); - - final ObjectNode node = bidderNode(bidderCatalog, name); - node.set("aliasOf", new TextNode(name)); - return node; - } - - /** - * Returns a {@link Map} of all bidder's infos sorted by name (and alias) as {@link ObjectNode}. - */ - private ObjectNode allInfos(Map nameToInfo, Map aliasToInfo) { - final Map result = new TreeMap<>(); - result.putAll(nameToInfo); - result.putAll(aliasToInfo); - return mapper.mapper().valueToTree(result); + private ObjectNode allInfos(Map nameToInfo) { + return mapper.mapper().valueToTree(new TreeMap<>(nameToInfo)); } @Override - public void handle(RoutingContext context) { - final String bidderName = context.request().getParam(BIDDER_NAME_PARAM); + public void handle(RoutingContext routingContext) { + final String bidderName = routingContext.request().getParam(BIDDER_NAME_PARAM); + final String endpoint = String.format("%s/%s", Endpoint.info_bidders.value(), bidderName); if (bidderInfos.containsKey(bidderName)) { - context.response() - .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) - .end(bidderInfos.get(bidderName)); + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .end(bidderInfos.get(bidderName))); } else { - context.response() - .setStatusCode(HttpResponseStatus.NOT_FOUND.code()) - .end(); + HttpUtil.executeSafely(routingContext, endpoint, + response -> response + .setStatusCode(HttpResponseStatus.NOT_FOUND.code()) + .end()); } } - @Value + @Value(staticConstructor = "of") private static class BidderInfoResponseModel { + private static final String STATUS_ACTIVE = "ACTIVE"; + private static final String STATUS_DISABLED = "DISABLED"; + + String status; + + @JsonProperty("usesHttps") + boolean usesHttps; + BidderInfo.MaintainerInfo maintainer; BidderInfo.CapabilitiesInfo capabilities; - static BidderInfoResponseModel from(BidderInfo bidderInfo) { - return new BidderInfoResponseModel(bidderInfo.getMaintainer(), bidderInfo.getCapabilities()); + @JsonProperty("aliasOf") + String aliasOf; + + private static BidderInfoResponseModel from(BidderInfo bidderInfo) { + return of( + bidderInfo.isEnabled() ? STATUS_ACTIVE : STATUS_DISABLED, + bidderInfo.isUsesHttps(), + bidderInfo.getMaintainer(), + bidderInfo.getCapabilities(), + bidderInfo.getAliasOf()); } } } diff --git a/src/main/java/org/prebid/server/handler/info/BiddersHandler.java b/src/main/java/org/prebid/server/handler/info/BiddersHandler.java index 61241d65019..4039050b697 100644 --- a/src/main/java/org/prebid/server/handler/info/BiddersHandler.java +++ b/src/main/java/org/prebid/server/handler/info/BiddersHandler.java @@ -1,40 +1,68 @@ package org.prebid.server.handler.info; import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.model.Endpoint; import org.prebid.server.util.HttpUtil; import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; -import java.util.stream.Stream; public class BiddersHandler implements Handler { - private final String body; + private static final String ENABLED_ONLY_PARAM = "enabledonly"; + private final BidderCatalog bidderCatalog; + private final JacksonMapper mapper; public BiddersHandler(BidderCatalog bidderCatalog, JacksonMapper mapper) { - Objects.requireNonNull(bidderCatalog); - Objects.requireNonNull(mapper); - final Set bidderNamesAndAliases = Stream.concat( - bidderCatalog.names().stream() - .filter(bidderCatalog::isActive), - bidderCatalog.aliases().stream() - .filter(alias -> bidderCatalog.isActive(bidderCatalog.nameByAlias(alias)))) - .collect(Collectors.toCollection(TreeSet::new)); - - body = mapper.encode(bidderNamesAndAliases); + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + this.mapper = Objects.requireNonNull(mapper); } @Override - public void handle(RoutingContext context) { - context.response() - .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) - .end(body); + public void handle(RoutingContext routingContext) { + try { + respondWith(routingContext, HttpResponseStatus.OK, resolveBodyFromContext(routingContext)); + } catch (IllegalArgumentException e) { + respondWith(routingContext, HttpResponseStatus.BAD_REQUEST, e.getMessage()); + } + } + + private String resolveBodyFromContext(RoutingContext routingContext) { + final boolean enabledOnly = enabledOnlyFromQueryStringParams(routingContext); + + final Set bidderNamesAndAliases = enabledOnly + ? bidderCatalog.names().stream() + .filter(bidderCatalog::isActive) + .collect(Collectors.toSet()) + : bidderCatalog.names(); + + return mapper.encode(new TreeSet<>(bidderNamesAndAliases)); + } + + private static boolean enabledOnlyFromQueryStringParams(RoutingContext routingContext) { + final String enabledOnlyParam = routingContext.queryParams().get(ENABLED_ONLY_PARAM); + + if (!StringUtils.equalsAnyIgnoreCase(enabledOnlyParam, "true", "false")) { + throw new IllegalArgumentException("Invalid value for 'enabledonly' query param, must be of boolean type"); + } + + return Boolean.parseBoolean(enabledOnlyParam); + } + + private static void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { + HttpUtil.executeSafely(routingContext, Endpoint.info_bidders, + response -> response + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .setStatusCode(status.code()) + .end(body)); } } diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java index ad5b50fb092..701a384c05c 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java @@ -1,6 +1,5 @@ package org.prebid.server.handler.openrtb2; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; @@ -19,15 +18,15 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.AmpEvent; import org.prebid.server.analytics.model.HttpContext; -import org.prebid.server.auction.AmpRequestFactory; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.Tuple2; import org.prebid.server.auction.model.Tuple3; +import org.prebid.server.auction.requestfactory.AmpRequestFactory; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.BlacklistedAccountException; @@ -40,14 +39,18 @@ import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.model.Endpoint; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; +import org.prebid.server.proto.openrtb.ext.response.ExtModules; import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; import org.prebid.server.proto.response.AmpResponse; +import org.prebid.server.proto.response.ExtAmpVideoPrebid; +import org.prebid.server.proto.response.ExtAmpVideoResponse; import org.prebid.server.util.HttpUtil; import java.time.Clock; @@ -65,17 +68,12 @@ public class AmpHandler implements Handler { private static final Logger logger = LoggerFactory.getLogger(AmpHandler.class); private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); - private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = - new TypeReference>() { - }; - private static final TypeReference EXT_BID_RESPONSE_TYPE_REFERENCE = - new TypeReference() { - }; + public static final String PREBID_EXT = "prebid"; private static final MetricName REQUEST_TYPE_METRIC = MetricName.amp; private final AmpRequestFactory ampRequestFactory; private final ExchangeService exchangeService; - private final AnalyticsReporter analyticsReporter; + private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; private final Clock clock; private final BidderCatalog bidderCatalog; @@ -86,7 +84,7 @@ public class AmpHandler implements Handler { public AmpHandler(AmpRequestFactory ampRequestFactory, ExchangeService exchangeService, - AnalyticsReporter analyticsReporter, + AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, Clock clock, BidderCatalog bidderCatalog, @@ -97,7 +95,7 @@ public AmpHandler(AmpRequestFactory ampRequestFactory, this.ampRequestFactory = Objects.requireNonNull(ampRequestFactory); this.exchangeService = Objects.requireNonNull(exchangeService); - this.analyticsReporter = Objects.requireNonNull(analyticsReporter); + this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); @@ -115,16 +113,13 @@ public void handle(RoutingContext routingContext) { // more accurately if we note the real start time, and use it to compute the auction timeout. final long startTime = clock.millis(); - final boolean isSafari = HttpUtil.isSafari(routingContext.request().headers().get(HttpUtil.USER_AGENT_HEADER)); - metrics.updateSafariRequestsMetric(isSafari); - final AmpEvent.AmpEventBuilder ampEventBuilder = AmpEvent.builder() .httpContext(HttpContext.from(routingContext)); ampRequestFactory.fromRequest(routingContext, startTime) .map(context -> addToEvent(context, ampEventBuilder::auctionContext, context)) - .map(context -> updateAppAndNoCookieAndImpsMetrics(context, isSafari)) + .map(this::updateAppAndNoCookieAndImpsMetrics) .compose(context -> exchangeService.holdAuction(context) .map(bidResponse -> Tuple2.of(bidResponse, context))) @@ -135,7 +130,7 @@ public void handle(RoutingContext routingContext) { Tuple3.of( result.getLeft(), result.getRight(), - toAmpResponse(result.getRight().getBidRequest(), result.getLeft()))) + toAmpResponse(result.getRight(), result.getLeft()))) .compose((Tuple3 result) -> ampResponsePostProcessor.postProcess( @@ -155,20 +150,22 @@ private static R addToEvent(T field, Consumer consumer, R result) { return result; } - private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context, boolean isSafari) { - final BidRequest bidRequest = context.getBidRequest(); - final UidsCookie uidsCookie = context.getUidsCookie(); + private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context) { + if (!context.isRequestRejected()) { + final BidRequest bidRequest = context.getBidRequest(); + final UidsCookie uidsCookie = context.getUidsCookie(); - final List imps = bidRequest.getImp(); - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(bidRequest.getApp() != null, uidsCookie.hasLiveUids(), - isSafari, imps.size()); + final List imps = bidRequest.getImp(); + metrics.updateAppAndNoCookieAndImpsRequestedMetrics(bidRequest.getApp() != null, uidsCookie.hasLiveUids(), + imps.size()); - metrics.updateImpTypesMetrics(imps); + metrics.updateImpTypesMetrics(imps); + } return context; } - private AmpResponse toAmpResponse(BidRequest bidRequest, BidResponse bidResponse) { + private AmpResponse toAmpResponse(AuctionContext auctionContext, BidResponse bidResponse) { // Fetch targeting information from response bids final List seatBids = bidResponse.getSeatbid(); @@ -184,48 +181,49 @@ private AmpResponse toAmpResponse(BidRequest bidRequest, BidResponse bidResponse final ExtResponseDebug extResponseDebug; final Map> errors; // Fetch debug and errors information from response if requested - if (isDebugEnabled(bidRequest)) { - final ExtBidResponse extBidResponse = extResponseFrom(bidResponse); + if (auctionContext.getDebugContext().isDebugEnabled()) { + final ExtBidResponse extBidResponse = bidResponse.getExt(); - extResponseDebug = extResponseDebugFrom(extBidResponse); - errors = errorsFrom(extBidResponse); + extResponseDebug = extBidResponse != null ? extBidResponse.getDebug() : null; + errors = extBidResponse != null ? extBidResponse.getErrors() : null; } else { extResponseDebug = null; errors = null; } - return AmpResponse.of(targeting, extResponseDebug, errors); + return AmpResponse.of(targeting, extResponseDebug, errors, extResponseFrom(bidResponse)); } private Map targetingFrom(Bid bid, String bidder) { - final ExtPrebid extBid; + final ObjectNode bidExt = bid.getExt(); + if (bidExt == null || !bidExt.hasNonNull(PREBID_EXT)) { + return Collections.emptyMap(); + } + + final ExtBidPrebid extBidPrebid; try { - extBid = mapper.mapper().convertValue(bid.getExt(), EXT_PREBID_TYPE_REFERENCE); + extBidPrebid = mapper.mapper().convertValue(bidExt.get(PREBID_EXT), ExtBidPrebid.class); } catch (IllegalArgumentException e) { throw new PreBidException( String.format("Critical error while unpacking AMP targets: %s", e.getMessage()), e); } - if (extBid != null) { - final ExtBidPrebid extBidPrebid = extBid.getPrebid(); - - // Need to extract the targeting parameters from the response, as those are all that - // go in the AMP response - final Map targeting = extBidPrebid != null ? extBidPrebid.getTargeting() : null; - if (targeting != null && targeting.keySet().stream() - .anyMatch(key -> key != null && key.startsWith("hb_cache_id"))) { + // Need to extract the targeting parameters from the response, as those are all that + // go in the AMP response + final Map targeting = extBidPrebid != null ? extBidPrebid.getTargeting() : null; + if (targeting != null && targeting.keySet().stream() + .anyMatch(key -> key != null && key.startsWith("hb_cache_id"))) { - return enrichWithCustomTargeting(targeting, extBid, bidder); - } + return enrichWithCustomTargeting(targeting, bidExt, bidder); } return Collections.emptyMap(); } private Map enrichWithCustomTargeting( - Map targeting, ExtPrebid extBid, String bidder) { + Map targeting, ObjectNode bidExt, String bidder) { - final Map customTargeting = customTargetingFrom(extBid.getBidder(), bidder); + final Map customTargeting = customTargetingFrom(bidExt, bidder); if (!customTargeting.isEmpty()) { final Map enrichedTargeting = new HashMap<>(targeting); enrichedTargeting.putAll(customTargeting); @@ -244,33 +242,12 @@ private Map customTargetingFrom(ObjectNode extBidBidder, String } } - /** - * Determines debug flag from {@link BidRequest}. - */ - private boolean isDebugEnabled(BidRequest bidRequest) { - if (Objects.equals(bidRequest.getTest(), 1)) { - return true; - } - final ExtRequest extRequest = bidRequest.getExt(); - final ExtRequestPrebid extRequestPrebid = extRequest != null ? extRequest.getPrebid() : null; - return extRequestPrebid != null && Objects.equals(extRequestPrebid.getDebug(), 1); - } + private static ExtAmpVideoResponse extResponseFrom(BidResponse bidResponse) { + final ExtBidResponse ext = bidResponse.getExt(); + final ExtBidResponsePrebid extPrebid = ext != null ? ext.getPrebid() : null; + final ExtModules extModules = extPrebid != null ? extPrebid.getModules() : null; - private ExtBidResponse extResponseFrom(BidResponse bidResponse) { - try { - return mapper.mapper().convertValue(bidResponse.getExt(), EXT_BID_RESPONSE_TYPE_REFERENCE); - } catch (IllegalArgumentException e) { - throw new PreBidException( - String.format("Critical error while unpacking AMP bid response: %s", e.getMessage()), e); - } - } - - private static ExtResponseDebug extResponseDebugFrom(ExtBidResponse extBidResponse) { - return extBidResponse != null ? extBidResponse.getDebug() : null; - } - - private static Map> errorsFrom(ExtBidResponse extBidResponse) { - return extBidResponse != null ? extBidResponse.getErrors() : null; + return extModules != null ? ExtAmpVideoResponse.of(ExtAmpVideoPrebid.of(extModules)) : null; } private void handleResult(AsyncResult> responseResult, @@ -283,7 +260,7 @@ private void handleResult(AsyncResult errorMessages; - final int status; + final HttpResponseStatus status; final String body; final String origin = originFrom(routingContext); @@ -298,7 +275,7 @@ private void handleResult(AsyncResult ampSourceOrigin = context.queryParam("__amp_source_origin"); + final List ampSourceOrigin = routingContext.queryParam("__amp_source_origin"); if (CollectionUtils.isNotEmpty(ampSourceOrigin)) { origin = ampSourceOrigin.get(0); } if (origin == null) { // Just to be safe - origin = ObjectUtils.defaultIfNull(context.request().headers().get("Origin"), StringUtils.EMPTY); + origin = ObjectUtils.defaultIfNull(routingContext.request().headers().get("Origin"), StringUtils.EMPTY); } return origin; } - private void respondWith(RoutingContext context, int status, String body, long startTime, - MetricName metricRequestStatus, AmpEvent event) { - // don't send the response if client has gone - if (context.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); - } else { - context.response() - .exceptionHandler(this::handleResponseException) - .setStatusCode(status) - .end(body); + private void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body, long startTime, + MetricName metricRequestStatus, AmpEvent event, TcfContext tcfContext) { + + final boolean responseSent = HttpUtil.executeSafely(routingContext, Endpoint.openrtb2_amp, + response -> response + .exceptionHandler(this::handleResponseException) + .setStatusCode(status.code()) + .end(body)); - metrics.updateRequestTimeMetric(clock.millis() - startTime); + if (responseSent) { + metrics.updateRequestTimeMetric(MetricName.request_time, clock.millis() - startTime); metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, metricRequestStatus); - analyticsReporter.processEvent(event); + analyticsDelegator.processEvent(event, tcfContext); + } else { + metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); } } diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java index 6e0b6e553ee..453bff5d776 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java @@ -10,13 +10,13 @@ import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.model.HttpContext; -import org.prebid.server.auction.AuctionRequestFactory; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.auction.requestfactory.AuctionRequestFactory; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.BlacklistedAccountException; import org.prebid.server.exception.BlacklistedAppException; @@ -27,6 +27,9 @@ import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; +import org.prebid.server.model.Endpoint; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.util.HttpUtil; import java.time.Clock; @@ -43,7 +46,7 @@ public class AuctionHandler implements Handler { private final AuctionRequestFactory auctionRequestFactory; private final ExchangeService exchangeService; - private final AnalyticsReporter analyticsReporter; + private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; private final Clock clock; private final HttpInteractionLogger httpInteractionLogger; @@ -51,7 +54,7 @@ public class AuctionHandler implements Handler { public AuctionHandler(AuctionRequestFactory auctionRequestFactory, ExchangeService exchangeService, - AnalyticsReporter analyticsReporter, + AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, Clock clock, HttpInteractionLogger httpInteractionLogger, @@ -59,7 +62,7 @@ public AuctionHandler(AuctionRequestFactory auctionRequestFactory, this.auctionRequestFactory = Objects.requireNonNull(auctionRequestFactory); this.exchangeService = Objects.requireNonNull(exchangeService); - this.analyticsReporter = Objects.requireNonNull(analyticsReporter); + this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); @@ -74,17 +77,13 @@ public void handle(RoutingContext routingContext) { // more accurately if we note the real start time, and use it to compute the auction timeout. final long startTime = clock.millis(); - final boolean isSafari = HttpUtil.isSafari(routingContext.request().headers().get(HttpUtil.USER_AGENT_HEADER)); - metrics.updateSafariRequestsMetric(isSafari); - final AuctionEvent.AuctionEventBuilder auctionEventBuilder = AuctionEvent.builder() .httpContext(HttpContext.from(routingContext)); auctionRequestFactory.fromRequest(routingContext, startTime) .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context)) - .map(context -> updateAppAndNoCookieAndImpsMetrics(context, isSafari)) - + .map(this::updateAppAndNoCookieAndImpsMetrics) .compose(context -> exchangeService.holdAuction(context) .map(bidResponse -> Tuple2.of(bidResponse, context))) @@ -97,15 +96,19 @@ private static R addToEvent(T field, Consumer consumer, R result) { return result; } - private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context, boolean isSafari) { - final BidRequest bidRequest = context.getBidRequest(); - final UidsCookie uidsCookie = context.getUidsCookie(); + private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context) { + if (!context.isRequestRejected()) { + final BidRequest bidRequest = context.getBidRequest(); + final UidsCookie uidsCookie = context.getUidsCookie(); - final List imps = bidRequest.getImp(); - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(bidRequest.getApp() != null, uidsCookie.hasLiveUids(), - isSafari, imps.size()); + final List imps = bidRequest.getImp(); + metrics.updateAppAndNoCookieAndImpsRequestedMetrics( + bidRequest.getApp() != null, + uidsCookie.hasLiveUids(), + imps.size()); - metrics.updateImpTypesMetrics(imps); + metrics.updateImpTypesMetrics(imps); + } return context; } @@ -122,14 +125,14 @@ private void handleResult(AsyncResult> respo final MetricName metricRequestStatus; final List errorMessages; - final int status; + final HttpResponseStatus status; final String body; if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); - status = HttpResponseStatus.OK.code(); + status = HttpResponseStatus.OK; routingContext.response().headers().add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); body = mapper.encode(responseResult.result().getLeft()); } else { @@ -142,19 +145,18 @@ private void handleResult(AsyncResult> respo .map(msg -> String.format("Invalid request format: %s", msg)) .collect(Collectors.toList()); final String message = String.join("\n", errorMessages); + final String referer = routingContext.request().headers().get(HttpUtil.REFERER_HEADER); + conditionalLogger.info(String.format("%s, Referer: %s", message, referer), 0.01); - conditionalLogger.info(String.format("%s, Referer: %s", message, - routingContext.request().headers().get(HttpUtil.REFERER_HEADER)), 100); - - status = HttpResponseStatus.BAD_REQUEST.code(); + status = HttpResponseStatus.BAD_REQUEST; body = message; } else if (exception instanceof UnauthorizedAccountException) { metricRequestStatus = MetricName.badinput; final String message = exception.getMessage(); - conditionalLogger.info(message, 100); + conditionalLogger.info(message, 0.01); errorMessages = Collections.singletonList(message); - status = HttpResponseStatus.UNAUTHORIZED.code(); + status = HttpResponseStatus.UNAUTHORIZED; body = message; final String accountId = ((UnauthorizedAccountException) exception).getAccountId(); metrics.updateAccountRequestRejectedMetrics(accountId); @@ -166,7 +168,7 @@ private void handleResult(AsyncResult> respo logger.debug(message); errorMessages = Collections.singletonList(message); - status = HttpResponseStatus.FORBIDDEN.code(); + status = HttpResponseStatus.FORBIDDEN; body = message; } else { metricRequestStatus = MetricName.err; @@ -175,32 +177,36 @@ private void handleResult(AsyncResult> respo final String message = exception.getMessage(); errorMessages = Collections.singletonList(message); - status = HttpResponseStatus.INTERNAL_SERVER_ERROR.code(); + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; body = String.format("Critical error while running the auction: %s", message); } } - final AuctionEvent auctionEvent = auctionEventBuilder.status(status).errors(errorMessages).build(); - respondWith(routingContext, status, body, startTime, requestType, metricRequestStatus, auctionEvent); + final AuctionEvent auctionEvent = auctionEventBuilder.status(status.code()).errors(errorMessages).build(); + final PrivacyContext privacyContext = auctionContext != null ? auctionContext.getPrivacyContext() : null; + final TcfContext tcfContext = privacyContext != null ? privacyContext.getTcfContext() : TcfContext.empty(); + respondWith(routingContext, status, body, startTime, requestType, metricRequestStatus, auctionEvent, + tcfContext); - httpInteractionLogger.maybeLogOpenrtb2Auction(auctionContext, routingContext, status, body); + httpInteractionLogger.maybeLogOpenrtb2Auction(auctionContext, routingContext, status.code(), body); } - private void respondWith(RoutingContext context, int status, String body, long startTime, MetricName requestType, - MetricName metricRequestStatus, AuctionEvent event) { - // don't send the response if client has gone - if (context.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - metrics.updateRequestTypeMetric(requestType, MetricName.networkerr); - } else { - context.response() - .exceptionHandler(throwable -> handleResponseException(throwable, requestType)) - .setStatusCode(status) - .end(body); + private void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body, long startTime, + MetricName requestType, MetricName metricRequestStatus, AuctionEvent event, + TcfContext tcfContext) { + + final boolean responseSent = HttpUtil.executeSafely(routingContext, Endpoint.openrtb2_auction, + response -> response + .exceptionHandler(throwable -> handleResponseException(throwable, requestType)) + .setStatusCode(status.code()) + .end(body)); - metrics.updateRequestTimeMetric(clock.millis() - startTime); + if (responseSent) { + metrics.updateRequestTimeMetric(MetricName.request_time, clock.millis() - startTime); metrics.updateRequestTypeMetric(requestType, metricRequestStatus); - analyticsReporter.processEvent(event); + analyticsDelegator.processEvent(event, tcfContext); + } else { + metrics.updateRequestTypeMetric(requestType, MetricName.networkerr); } } diff --git a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java index 36218834993..8e029d32cf8 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java @@ -7,18 +7,22 @@ import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.HttpContext; import org.prebid.server.analytics.model.VideoEvent; import org.prebid.server.auction.ExchangeService; -import org.prebid.server.auction.VideoRequestFactory; import org.prebid.server.auction.VideoResponseFactory; +import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.auction.requestfactory.VideoRequestFactory; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; +import org.prebid.server.model.Endpoint; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.response.VideoResponse; import org.prebid.server.util.HttpUtil; @@ -38,18 +42,18 @@ public class VideoHandler implements Handler { private final VideoRequestFactory videoRequestFactory; private final VideoResponseFactory videoResponseFactory; private final ExchangeService exchangeService; - private final AnalyticsReporter analyticsReporter; + private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; private final Clock clock; private final JacksonMapper mapper; public VideoHandler(VideoRequestFactory videoRequestFactory, VideoResponseFactory videoResponseFactory, - ExchangeService exchangeService, AnalyticsReporter analyticsReporter, Metrics metrics, + ExchangeService exchangeService, AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, Clock clock, JacksonMapper mapper) { this.videoRequestFactory = Objects.requireNonNull(videoRequestFactory); this.videoResponseFactory = Objects.requireNonNull(videoResponseFactory); this.exchangeService = Objects.requireNonNull(exchangeService); - this.analyticsReporter = Objects.requireNonNull(analyticsReporter); + this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); @@ -63,8 +67,6 @@ public void handle(RoutingContext routingContext) { // more accurately if we note the real start time, and use it to compute the auction timeout. final long startTime = clock.millis(); - final boolean isSafari = HttpUtil.isSafari(routingContext.request().headers().get(HttpUtil.USER_AGENT_HEADER)); - metrics.updateSafariRequestsMetric(isSafari); final VideoEvent.VideoEventBuilder videoEventBuilder = VideoEvent.builder() .httpContext(HttpContext.from(routingContext)); @@ -75,7 +77,7 @@ public void handle(RoutingContext routingContext) { .compose(contextToErrors -> exchangeService.holdAuction(contextToErrors.getData()) .map(bidResponse -> Tuple2.of(bidResponse, contextToErrors))) - .map(result -> videoResponseFactory.toVideoResponse(result.getRight().getData().getBidRequest(), + .map(result -> videoResponseFactory.toVideoResponse(result.getRight().getData(), result.getLeft(), result.getRight().getPodErrors())) .map(videoResponse -> addToEvent(videoResponse, videoEventBuilder::bidResponse, videoResponse)) @@ -89,19 +91,19 @@ private static R addToEvent(T field, Consumer consumer, R result) { } private void handleResult(AsyncResult responseResult, VideoEvent.VideoEventBuilder videoEventBuilder, - RoutingContext context, long startTime) { + RoutingContext routingContext, long startTime) { final boolean responseSucceeded = responseResult.succeeded(); final MetricName metricRequestStatus; final List errorMessages; - final int status; + final HttpResponseStatus status; final String body; if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); - status = HttpResponseStatus.OK.code(); - context.response().headers().add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + status = HttpResponseStatus.OK; + routingContext.response().headers().add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); body = mapper.encode(responseResult.result()); } else { final Throwable exception = responseResult.cause(); @@ -110,7 +112,7 @@ private void handleResult(AsyncResult responseResult, VideoEvent. errorMessages = ((InvalidRequestException) exception).getMessages(); logger.info("Invalid request format: {0}", errorMessages); - status = HttpResponseStatus.BAD_REQUEST.code(); + status = HttpResponseStatus.BAD_REQUEST; body = errorMessages.stream() .map(msg -> String.format("Invalid request format: %s", msg)) .collect(Collectors.joining("\n")); @@ -118,10 +120,9 @@ private void handleResult(AsyncResult responseResult, VideoEvent. metricRequestStatus = MetricName.badinput; final String errorMessage = exception.getMessage(); logger.info("Unauthorized: {0}", errorMessage); - errorMessages = Collections.singletonList(errorMessage); - status = HttpResponseStatus.UNAUTHORIZED.code(); + status = HttpResponseStatus.UNAUTHORIZED; body = String.format("Unauthorised: %s", errorMessage); } else { metricRequestStatus = MetricName.err; @@ -130,29 +131,38 @@ private void handleResult(AsyncResult responseResult, VideoEvent. final String message = exception.getMessage(); errorMessages = Collections.singletonList(message); - status = HttpResponseStatus.INTERNAL_SERVER_ERROR.code(); + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; body = String.format("Critical error while running the auction: %s", message); } } - final VideoEvent videoEvent = videoEventBuilder.status(status).errors(errorMessages).build(); - respondWith(context, status, body, startTime, metricRequestStatus, videoEvent); - } - private void respondWith(RoutingContext context, int status, String body, long startTime, - MetricName metricRequestStatus, VideoEvent event) { - // don't send the response if client has gone - if (context.response().closed()) { - logger.warn("The client already closed connection, response will be skipped"); - metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); - } else { - context.response() - .exceptionHandler(this::handleResponseException) - .setStatusCode(status) - .end(body); + final VideoEvent videoEvent = videoEventBuilder.status(status.code()).errors(errorMessages).build(); + final AuctionContext auctionContext = videoEvent.getAuctionContext(); + final PrivacyContext privacyContext = auctionContext != null ? auctionContext.getPrivacyContext() : null; + final TcfContext tcfContext = privacyContext != null ? privacyContext.getTcfContext() : TcfContext.empty(); + respondWith(routingContext, status, body, startTime, metricRequestStatus, videoEvent, tcfContext); + } - metrics.updateRequestTimeMetric(clock.millis() - startTime); + private void respondWith(RoutingContext routingContext, + HttpResponseStatus status, + String body, + long startTime, + MetricName metricRequestStatus, + VideoEvent event, + TcfContext tcfContext) { + + final boolean responseSent = HttpUtil.executeSafely(routingContext, Endpoint.openrtb2_video, + response -> response + .exceptionHandler(this::handleResponseException) + .setStatusCode(status.code()) + .end(body)); + + if (responseSent) { + metrics.updateRequestTimeMetric(REQUEST_TYPE_METRIC, clock.millis() - startTime); metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, metricRequestStatus); - analyticsReporter.processEvent(event); + analyticsDelegator.processEvent(event, tcfContext); + } else { + metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); } } diff --git a/src/main/java/org/prebid/server/health/HealthMonitor.java b/src/main/java/org/prebid/server/health/HealthMonitor.java new file mode 100644 index 00000000000..3fc8a53ed2a --- /dev/null +++ b/src/main/java/org/prebid/server/health/HealthMonitor.java @@ -0,0 +1,38 @@ +package org.prebid.server.health; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.concurrent.atomic.LongAdder; + +/** + * Used to gather statistics and calculate the health index indicator. + */ +public class HealthMonitor { + + private final LongAdder totalCounter = new LongAdder(); + + private final LongAdder successCounter = new LongAdder(); + + /** + * Increments total number of requests. + */ + public void incTotal() { + totalCounter.increment(); + } + + /** + * Increments succeeded number of requests. + */ + public void incSuccess() { + successCounter.increment(); + } + + /** + * Returns value between 0.0 ... 1.0 where 1.0 is indicated 100% healthy. + */ + public BigDecimal calculateHealthIndex() { + final BigDecimal success = BigDecimal.valueOf(successCounter.sumThenReset()); + final BigDecimal total = BigDecimal.valueOf(totalCounter.sumThenReset()); + return total.longValue() == 0 ? BigDecimal.ONE : success.divide(total, 2, RoundingMode.HALF_EVEN); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/FailedException.java b/src/main/java/org/prebid/server/hooks/execution/FailedException.java new file mode 100644 index 00000000000..79e1b9fda06 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/FailedException.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.execution; + +class FailedException extends RuntimeException { + + FailedException(String message) { + super(message); + } + + FailedException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java new file mode 100644 index 00000000000..3133028dfbf --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java @@ -0,0 +1,194 @@ +package org.prebid.server.hooks.execution; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.hooks.execution.model.ExecutionGroup; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.log.ConditionalLogger; + +import java.time.Clock; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.function.Supplier; + +class GroupExecutor { + + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(GroupExecutor.class)); + + private final Vertx vertx; + private final Clock clock; + + private ExecutionGroup group; + private PAYLOAD initialPayload; + private Function> hookProvider; + private InvocationContextProvider invocationContextProvider; + private HookExecutionContext hookExecutionContext; + private boolean rejectAllowed; + + private GroupExecutor(Vertx vertx, Clock clock) { + this.vertx = vertx; + this.clock = clock; + } + + public static GroupExecutor create( + Vertx vertx, + Clock clock) { + + return new GroupExecutor<>(vertx, clock); + } + + public GroupExecutor withGroup(ExecutionGroup group) { + this.group = group; + return this; + } + + public GroupExecutor withInitialPayload(PAYLOAD initialPayload) { + this.initialPayload = initialPayload; + return this; + } + + public GroupExecutor withHookProvider(Function> hookProvider) { + this.hookProvider = hookProvider; + return this; + } + + public GroupExecutor withInvocationContextProvider( + InvocationContextProvider invocationContextProvider) { + + this.invocationContextProvider = invocationContextProvider; + return this; + } + + public GroupExecutor withHookExecutionContext(HookExecutionContext hookExecutionContext) { + this.hookExecutionContext = hookExecutionContext; + return this; + } + + public GroupExecutor withRejectAllowed(boolean rejectAllowed) { + this.rejectAllowed = rejectAllowed; + return this; + } + + public Future> execute() { + final GroupResult initialGroupResult = GroupResult.of(initialPayload, rejectAllowed); + Future> groupFuture = Future.succeededFuture(initialGroupResult); + + for (final HookId hookId : group.getHookSequence()) { + final Hook hook = hookProvider.apply(hookId); + + final long startTime = clock.millis(); + final Future> invocationResult = + executeHook(hook, group.getTimeout(), initialGroupResult, hookId); + + groupFuture = groupFuture.compose(groupResult -> + applyInvocationResult(invocationResult, hookId, startTime, groupResult)); + } + + return groupFuture.recover(GroupExecutor::restoreResultFromRejection); + } + + private Future> executeHook( + Hook hook, + Long timeout, + GroupResult groupResult, + HookId hookId) { + + if (hook == null) { + conditionalLogger.error(String.format("Hook implementation %s does not exist or disabled", hookId), 0.01d); + + return Future.failedFuture(new FailedException("Hook implementation does not exist or disabled")); + } + + return executeWithTimeout( + () -> hook.call( + groupResult.payload(), + invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId))), + timeout); + } + + private Future executeWithTimeout(Supplier> action, Long timeout) { + final Promise promise = Promise.promise(); + + final long timeoutTimerId = vertx.setTimer(timeout, id -> failWithTimeout(promise)); + + executeSafely(action) + .setHandler(result -> completeWithActionResult(promise, timeoutTimerId, result)); + + return promise.future(); + } + + private static void failWithTimeout(Promise promise) { + // no need for synchronization since timer is fired on the same event loop thread + if (!promise.future().isComplete()) { + promise.fail(new TimeoutException("Timed out while executing action")); + } + } + + private static Future executeSafely(Supplier> action) { + try { + final Future result = action.get(); + return result != null ? result : Future.failedFuture(new FailedException("Action returned null")); + } catch (Throwable e) { + return Future.failedFuture(new FailedException(e)); + } + } + + private void completeWithActionResult(Promise promise, long timeoutTimerId, AsyncResult result) { + vertx.cancelTimer(timeoutTimerId); + + // check is to avoid harmless exception if timeout exceeds before successful result becomes ready + if (!promise.future().isComplete()) { + promise.handle(result); + } + } + + private long executionTime(long startTime) { + return clock.millis() - startTime; + } + + private Future> applyInvocationResult( + Future> invocationResult, + HookId hookId, + long startTime, + GroupResult groupResult) { + + return invocationResult + .map(result -> { + saveModuleContext(hookId, result); + return groupResult.applyInvocationResult(result, hookId, executionTime(startTime)); + }) + .otherwise(throwable -> groupResult.applyFailure(throwable, hookId, executionTime(startTime))) + .compose(this::propagateRejection); + } + + private Object moduleContextFor(HookId hookId) { + return hookExecutionContext.getModuleContexts().get(hookId.getModuleCode()); + } + + private void saveModuleContext(HookId hookId, InvocationResult result) { + hookExecutionContext.getModuleContexts().put(hookId.getModuleCode(), result.moduleContext()); + } + + private Future> propagateRejection(GroupResult groupResult) { + return groupResult.shouldReject() + ? Future.failedFuture(new RejectedException(groupResult)) + : Future.succeededFuture(groupResult); + + } + + private static Future restoreResultFromRejection(Throwable throwable) { + if (throwable instanceof RejectedException) { + return Future.succeededFuture(((RejectedException) throwable).result()); + } + + return Future.failedFuture(throwable); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupResult.java b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java new file mode 100644 index 00000000000..b2505110250 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java @@ -0,0 +1,216 @@ +package org.prebid.server.hooks.execution; + +import io.vertx.core.logging.LoggerFactory; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +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.PayloadUpdate; +import org.prebid.server.log.ConditionalLogger; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; + +@Accessors(fluent = true) +@Getter +class GroupResult { + + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(GroupExecutor.class)); + + private static final double LOG_SAMPLING_RATE = 0.01d; + + private boolean shouldReject; + + private T payload; + + private final boolean rejectAllowed; + + private final List hookExecutionOutcomes = new ArrayList<>(); + + private GroupResult(T payload, boolean rejectAllowed) { + this.shouldReject = false; + this.payload = payload; + this.rejectAllowed = rejectAllowed; + } + + public static GroupResult of(T payload, boolean rejectAllowed) { + return new GroupResult<>(payload, rejectAllowed); + } + + public GroupResult applyInvocationResult(InvocationResult invocationResult, + HookId hookId, + long executionTime) { + + if (invocationResult.status() == InvocationStatus.success && invocationResult.action() != null) { + try { + applyAction(hookId, invocationResult.action(), invocationResult.payloadUpdate()); + } catch (Exception e) { + hookExecutionOutcomes.add(toExecutionOutcome(e, hookId, executionTime)); + + return this; + } + } + + hookExecutionOutcomes.add(toExecutionOutcome(invocationResult, hookId, executionTime)); + + return this; + } + + public GroupResult applyFailure(Throwable throwable, HookId hookId, long executionTime) { + hookExecutionOutcomes.add(toExecutionOutcome(throwable, hookId, executionTime)); + + return this; + } + + public GroupExecutionOutcome toGroupExecutionOutcome() { + return GroupExecutionOutcome.of(this.hookExecutionOutcomes()); + } + + private void applyAction(HookId hookId, InvocationAction action, PayloadUpdate payloadUpdate) { + switch (action) { + case reject: + applyReject(hookId); + break; + case update: + applyPayloadUpdate(hookId, payloadUpdate); + break; + case no_action: + break; + default: + throw new IllegalStateException( + String.format("Unknown invocation action %s", action)); + } + } + + private void applyReject(HookId hookId) { + if (!rejectAllowed) { + conditionalLogger.error( + String.format( + "Hook implementation %s requested to reject an entity on a stage that does not support " + + "rejection", + hookId), + LOG_SAMPLING_RATE); + + throw new RejectionNotSupportedException("Rejection is not supported during this stage"); + } + + shouldReject = true; + payload = null; + } + + private void applyPayloadUpdate(HookId hookId, PayloadUpdate payloadUpdate) { + if (payloadUpdate == null) { + conditionalLogger.error( + String.format( + "Hook implementation %s requested to update an entity but not provided a payload update", + hookId), + LOG_SAMPLING_RATE); + + throw new PayloadUpdateException("Payload update is missing in invocation result"); + } + + try { + payload = payloadUpdate.apply(payload); + } catch (Exception e) { + conditionalLogger.error( + String.format( + "Hook implementation %s requested to update an entity but payload update has thrown an " + + "exception: %s", + hookId, + e), + LOG_SAMPLING_RATE); + + throw new PayloadUpdateException(String.format("Payload update has thrown an exception: %s", e)); + } + } + + private static HookExecutionOutcome toExecutionOutcome(InvocationResult invocationResult, + HookId hookId, + long executionTime) { + + return HookExecutionOutcome.builder() + .hookId(hookId) + .executionTime(executionTime) + .status(toExecutionStatus(invocationResult.status())) + .message(invocationResult.message()) + .action(toExecutionAction(invocationResult.action())) + .errors(invocationResult.errors()) + .warnings(invocationResult.warnings()) + .debugMessages(invocationResult.debugMessages()) + .analyticsTags(invocationResult.analyticsTags()) + .build(); + } + + private static HookExecutionOutcome toExecutionOutcome(Throwable throwable, HookId hookId, long executionTime) { + return HookExecutionOutcome.builder() + .hookId(hookId) + .executionTime(executionTime) + .status(toFailureType(throwable)) + .message(throwable.getMessage()) + .build(); + } + + private static ExecutionStatus toFailureType(Throwable throwable) { + if (throwable instanceof TimeoutException) { + return ExecutionStatus.timeout; + } else if (throwable instanceof FailedException) { + return ExecutionStatus.invocation_failure; + } + + return ExecutionStatus.execution_failure; + } + + private static ExecutionStatus toExecutionStatus(InvocationStatus status) { + if (status == null) { + return null; + } + + switch (status) { + case success: + return ExecutionStatus.success; + case failure: + return ExecutionStatus.failure; + default: + throw new IllegalStateException(String.format("Unknown invocation status %s", status)); + } + } + + private static ExecutionAction toExecutionAction(InvocationAction action) { + if (action == null) { + return null; + } + + switch (action) { + case reject: + return ExecutionAction.reject; + case update: + return ExecutionAction.update; + case no_action: + return ExecutionAction.no_action; + default: + throw new IllegalStateException(String.format("Unknown invocation action %s", action)); + } + } + + private static class RejectionNotSupportedException extends RuntimeException { + + RejectionNotSupportedException(String message) { + super(message); + } + } + + private static class PayloadUpdateException extends RuntimeException { + + PayloadUpdateException(String message) { + super(message); + } + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java b/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java new file mode 100644 index 00000000000..3a6f56f6f39 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.execution; + +import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; +import java.util.Objects; + +/** + * Provides simple access to all {@link Hook}s registered in application. + */ +public class HookCatalog { + + private final Collection modules; + + public HookCatalog(Collection modules) { + this.modules = Objects.requireNonNull(modules); + } + + public > HOOK hookById( + String moduleCode, + String hookImplCode, + StageWithHookType stage) { + + Class clazz = stage.hookType(); + return modules.stream() + .filter(module -> Objects.equals(module.code(), moduleCode)) + .map(Module::hooks) + .flatMap(Collection::stream) + .filter(hook -> Objects.equals(hook.code(), hookImplCode)) + .filter(clazz::isInstance) + .map(clazz::cast) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java new file mode 100644 index 00000000000..d6d73bc47ee --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java @@ -0,0 +1,395 @@ +package org.prebid.server.hooks.execution; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; +import org.prebid.server.hooks.execution.model.ExecutionGroup; +import org.prebid.server.hooks.execution.model.ExecutionPlan; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionPlan; +import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.execution.v1.InvocationContextImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionInvocationContextImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderInvocationContextImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; +import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.Endpoint; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountHooksConfiguration; + +import java.time.Clock; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class HookStageExecutor { + + private static final String ENTITY_HTTP_REQUEST = "http-request"; + private static final String ENTITY_AUCTION_REQUEST = "auction-request"; + private static final String ENTITY_AUCTION_RESPONSE = "auction-response"; + + private final ExecutionPlan hostExecutionPlan; + private final ExecutionPlan defaultAccountExecutionPlan; + private final HookCatalog hookCatalog; + private final TimeoutFactory timeoutFactory; + private final Vertx vertx; + private final Clock clock; + + private HookStageExecutor(ExecutionPlan hostExecutionPlan, + ExecutionPlan defaultAccountExecutionPlan, + HookCatalog hookCatalog, + TimeoutFactory timeoutFactory, + Vertx vertx, + Clock clock) { + + this.hostExecutionPlan = hostExecutionPlan; + this.defaultAccountExecutionPlan = defaultAccountExecutionPlan; + this.hookCatalog = hookCatalog; + this.timeoutFactory = timeoutFactory; + this.vertx = vertx; + this.clock = clock; + } + + public static HookStageExecutor create(String hostExecutionPlan, + String defaultAccountExecutionPlan, + HookCatalog hookCatalog, + TimeoutFactory timeoutFactory, + Vertx vertx, + Clock clock, + JacksonMapper mapper) { + + return new HookStageExecutor( + parseAndValidateExecutionPlan( + hostExecutionPlan, + Objects.requireNonNull(mapper), + Objects.requireNonNull(hookCatalog)), + parseAndValidateExecutionPlan(defaultAccountExecutionPlan, mapper, hookCatalog), + hookCatalog, + Objects.requireNonNull(timeoutFactory), + Objects.requireNonNull(vertx), + Objects.requireNonNull(clock)); + } + + public Future> executeEntrypointStage( + CaseInsensitiveMultiMap queryParams, + CaseInsensitiveMultiMap headers, + String body, + HookExecutionContext context) { + + final Endpoint endpoint = context.getEndpoint(); + + return this + .stageExecutor(StageWithHookType.ENTRYPOINT, ENTITY_HTTP_REQUEST, context) + .withExecutionPlan(planForEntrypointStage(endpoint)) + .withInitialPayload(EntrypointPayloadImpl.of(queryParams, headers, body)) + .withInvocationContextProvider(invocationContextProvider(endpoint)) + .withRejectAllowed(true) + .execute(); + } + + public Future> executeRawAuctionRequestStage( + AuctionContext auctionContext) { + + final BidRequest bidRequest = auctionContext.getBidRequest(); + final Account account = auctionContext.getAccount(); + final HookExecutionContext context = auctionContext.getHookExecutionContext(); + + final Endpoint endpoint = context.getEndpoint(); + + return this + .stageExecutor( + StageWithHookType.RAW_AUCTION_REQUEST, ENTITY_AUCTION_REQUEST, context, account, endpoint) + .withInitialPayload(AuctionRequestPayloadImpl.of(bidRequest)) + .withInvocationContextProvider(auctionInvocationContextProvider(endpoint, auctionContext)) + .withRejectAllowed(true) + .execute(); + } + + public Future> executeProcessedAuctionRequestStage( + AuctionContext auctionContext) { + + final BidRequest bidRequest = auctionContext.getBidRequest(); + final Account account = auctionContext.getAccount(); + final HookExecutionContext context = auctionContext.getHookExecutionContext(); + + final Endpoint endpoint = context.getEndpoint(); + + return this + .stageExecutor( + StageWithHookType.PROCESSED_AUCTION_REQUEST, ENTITY_AUCTION_REQUEST, context, account, endpoint) + .withInitialPayload(AuctionRequestPayloadImpl.of(bidRequest)) + .withInvocationContextProvider(auctionInvocationContextProvider(endpoint, auctionContext)) + .withRejectAllowed(true) + .execute(); + } + + public Future> executeBidderRequestStage( + BidderRequest bidderRequest, AuctionContext auctionContext) { + + final Account account = auctionContext.getAccount(); + final HookExecutionContext context = auctionContext.getHookExecutionContext(); + + final String bidder = bidderRequest.getBidder(); + + final Endpoint endpoint = context.getEndpoint(); + + return this + .stageExecutor(StageWithHookType.BIDDER_REQUEST, bidder, context, account, endpoint) + .withInitialPayload(BidderRequestPayloadImpl.of(bidderRequest.getBidRequest())) + .withInvocationContextProvider(bidderInvocationContextProvider(endpoint, auctionContext, bidder)) + .withRejectAllowed(true) + .execute(); + } + + public Future> executeRawBidderResponseStage( + BidderResponse bidderResponse, + AuctionContext auctionContext) { + + final Account account = auctionContext.getAccount(); + final HookExecutionContext context = auctionContext.getHookExecutionContext(); + + final List bids = bidderResponse.getSeatBid().getBids(); + final String bidder = bidderResponse.getBidder(); + + final Endpoint endpoint = context.getEndpoint(); + + return this + .stageExecutor(StageWithHookType.RAW_BIDDER_RESPONSE, bidder, context, account, endpoint) + .withInitialPayload(BidderResponsePayloadImpl.of(bids)) + .withInvocationContextProvider(bidderInvocationContextProvider(endpoint, auctionContext, bidder)) + .withRejectAllowed(true) + .execute(); + } + + public Future> executeProcessedBidderResponseStage( + BidderResponse bidderResponse, + AuctionContext auctionContext) { + + final Account account = auctionContext.getAccount(); + final HookExecutionContext context = auctionContext.getHookExecutionContext(); + + final List bids = bidderResponse.getSeatBid().getBids(); + final String bidder = bidderResponse.getBidder(); + + final Endpoint endpoint = context.getEndpoint(); + + return this + .stageExecutor(StageWithHookType.PROCESSED_BIDDER_RESPONSE, bidder, context, account, endpoint) + .withInitialPayload(BidderResponsePayloadImpl.of(bids)) + .withInvocationContextProvider(bidderInvocationContextProvider(endpoint, auctionContext, bidder)) + .withRejectAllowed(true) + .execute(); + } + + public Future> executeAuctionResponseStage( + BidResponse bidResponse, + AuctionContext auctionContext) { + + final Account account = auctionContext.getAccount(); + final HookExecutionContext context = auctionContext.getHookExecutionContext(); + + final Endpoint endpoint = context.getEndpoint(); + + return this + .stageExecutor(StageWithHookType.AUCTION_RESPONSE, ENTITY_AUCTION_RESPONSE, context, account, endpoint) + .withInitialPayload(AuctionResponsePayloadImpl.of(bidResponse)) + .withInvocationContextProvider(auctionInvocationContextProvider(endpoint, auctionContext)) + .withRejectAllowed(false) + .execute(); + } + + private StageExecutor stageExecutor( + StageWithHookType> stage, + String entity, + HookExecutionContext context) { + + return StageExecutor.create(hookCatalog, vertx, clock) + .withStage(stage) + .withEntity(entity) + .withHookExecutionContext(context); + } + + private StageExecutor stageExecutor( + StageWithHookType> stage, + String entity, + HookExecutionContext context, + Account account, + Endpoint endpoint) { + + return this + .stageExecutor(stage, entity, context) + .withExecutionPlan(planForStage(account, endpoint, stage.stage())); + } + + private static ExecutionPlan parseAndValidateExecutionPlan( + String executionPlan, + JacksonMapper mapper, + HookCatalog hookCatalog) { + + return validateExecutionPlan(parseExecutionPlan(executionPlan, mapper), hookCatalog); + } + + private static ExecutionPlan validateExecutionPlan(ExecutionPlan plan, HookCatalog hookCatalog) { + plan.getEndpoints().values().stream() + .map(EndpointExecutionPlan::getStages) + .map(Map::entrySet) + .flatMap(Collection::stream) + .forEach(stageToPlan -> stageToPlan.getValue().getGroups().stream() + .map(ExecutionGroup::getHookSequence) + .flatMap(Collection::stream) + .forEach(hookId -> validateHookId(stageToPlan.getKey(), hookId, hookCatalog))); + + return plan; + } + + private static void validateHookId(Stage stage, HookId hookId, HookCatalog hookCatalog) { + final Hook hook = hookCatalog.hookById( + hookId.getModuleCode(), + hookId.getHookImplCode(), + StageWithHookType.forStage(stage)); + + if (hook == null) { + throw new IllegalArgumentException(String.format( + "Hooks execution plan contains unknown or disabled hook: stage=%s, hookId=%s", stage, hookId)); + } + } + + private static ExecutionPlan parseExecutionPlan(String executionPlan, JacksonMapper mapper) { + if (StringUtils.isBlank(executionPlan)) { + return ExecutionPlan.empty(); + } + + try { + return mapper.decodeValue(executionPlan, ExecutionPlan.class); + } catch (DecodeException e) { + throw new IllegalArgumentException("Hooks execution plan could not be parsed", e); + } + } + + private StageExecutionPlan planForEntrypointStage(Endpoint endpoint) { + return effectiveStagePlanFrom(ExecutionPlan.empty(), endpoint, Stage.entrypoint); + } + + private StageExecutionPlan planForStage(Account account, Endpoint endpoint, Stage stage) { + return effectiveStagePlanFrom(effectiveExecutionPlanFor(account), endpoint, stage); + } + + private StageExecutionPlan effectiveStagePlanFrom( + ExecutionPlan accountExecutionPlan, Endpoint endpoint, Stage stage) { + + final StageExecutionPlan hostStageExecutionPlan = stagePlanFrom(hostExecutionPlan, endpoint, stage); + final StageExecutionPlan accountStageExecutionPlan = stagePlanFrom(accountExecutionPlan, endpoint, stage); + + if (hostStageExecutionPlan.isEmpty()) { + return accountStageExecutionPlan; + } else if (accountStageExecutionPlan.isEmpty()) { + return hostStageExecutionPlan; + } + + final List combinedGroups = Stream.of(hostStageExecutionPlan, accountStageExecutionPlan) + .map(StageExecutionPlan::getGroups) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + + return StageExecutionPlan.of(combinedGroups); + } + + private static StageExecutionPlan stagePlanFrom(ExecutionPlan executionPlan, Endpoint endpoint, Stage stage) { + return executionPlan + .getEndpoints() + .getOrDefault(endpoint, EndpointExecutionPlan.empty()) + .getStages() + .getOrDefault(stage, StageExecutionPlan.empty()); + } + + private ExecutionPlan effectiveExecutionPlanFor(Account account) { + final AccountHooksConfiguration hooksAccountConfig = account.getHooks(); + final ExecutionPlan accountExecutionPlan = + hooksAccountConfig != null ? hooksAccountConfig.getExecutionPlan() : null; + + return accountExecutionPlan != null ? accountExecutionPlan : defaultAccountExecutionPlan; + } + + private InvocationContextProvider invocationContextProvider(Endpoint endpoint) { + return (timeout, hookId, moduleContext) -> invocationContext(endpoint, timeout); + } + + private InvocationContextProvider auctionInvocationContextProvider( + Endpoint endpoint, + AuctionContext auctionContext) { + + return (timeout, hookId, moduleContext) -> auctionInvocationContext( + endpoint, timeout, auctionContext, hookId, moduleContext); + } + + private InvocationContextProvider bidderInvocationContextProvider( + Endpoint endpoint, + AuctionContext auctionContext, + String bidder) { + + return (timeout, hookId, moduleContext) -> BidderInvocationContextImpl.of( + auctionInvocationContext(endpoint, timeout, auctionContext, hookId, moduleContext), + bidder); + } + + private InvocationContextImpl invocationContext(Endpoint endpoint, Long timeout) { + return InvocationContextImpl.of(createTimeout(timeout), endpoint); + } + + private AuctionInvocationContextImpl auctionInvocationContext(Endpoint endpoint, + Long timeout, + AuctionContext auctionContext, + HookId hookId, + Object moduleContext) { + + return AuctionInvocationContextImpl.of( + invocationContext(endpoint, timeout), + auctionContext.getDebugContext().isDebugEnabled(), + accountConfigFor(auctionContext.getAccount(), hookId), + moduleContext); + } + + private Timeout createTimeout(Long timeout) { + return timeoutFactory.create(timeout); + } + + private static ObjectNode accountConfigFor(Account account, HookId hookId) { + final AccountHooksConfiguration accountHooksConfiguration = account.getHooks(); + final Map modulesConfiguration = + accountHooksConfiguration != null ? accountHooksConfiguration.getModules() : Collections.emptyMap(); + + return modulesConfiguration != null ? modulesConfiguration.get(hookId.getModuleCode()) : null; + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/InvocationContextProvider.java b/src/main/java/org/prebid/server/hooks/execution/InvocationContextProvider.java new file mode 100644 index 00000000000..83c1404fcbe --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/InvocationContextProvider.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.execution; + +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.v1.InvocationContext; + +@FunctionalInterface +interface InvocationContextProvider { + + CONTEXT apply(Long timeout, HookId hookId, Object moduleContext); +} diff --git a/src/main/java/org/prebid/server/hooks/execution/RejectedException.java b/src/main/java/org/prebid/server/hooks/execution/RejectedException.java new file mode 100644 index 00000000000..edf220eb921 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/RejectedException.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.execution; + +class RejectedException extends RuntimeException { + + private final Object result; + + RejectedException(Object result) { + this.result = result; + } + + @SuppressWarnings("unchecked") + public T result() { + return (T) result; + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java new file mode 100644 index 00000000000..f4f4a8176de --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java @@ -0,0 +1,131 @@ +package org.prebid.server.hooks.execution; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.hooks.execution.model.ExecutionGroup; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.model.StageExecutionPlan; +import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.time.Clock; +import java.util.ArrayList; + +class StageExecutor { + + private final HookCatalog hookCatalog; + private final Vertx vertx; + private final Clock clock; + + private StageWithHookType> stage; + private String entity; + private StageExecutionPlan executionPlan; + private PAYLOAD initialPayload; + private InvocationContextProvider invocationContextProvider; + private HookExecutionContext hookExecutionContext; + private boolean rejectAllowed; + + private StageExecutor(HookCatalog hookCatalog, Vertx vertx, Clock clock) { + this.hookCatalog = hookCatalog; + this.vertx = vertx; + this.clock = clock; + } + + public static StageExecutor create( + HookCatalog hookCatalog, + Vertx vertx, + Clock clock) { + + return new StageExecutor<>(hookCatalog, vertx, clock); + } + + public StageExecutor withStage(StageWithHookType> stage) { + this.stage = stage; + return this; + } + + public StageExecutor withEntity(String entity) { + this.entity = entity; + return this; + } + + public StageExecutor withExecutionPlan(StageExecutionPlan executionPlan) { + this.executionPlan = executionPlan; + return this; + } + + public StageExecutor withInitialPayload(PAYLOAD initialPayload) { + this.initialPayload = initialPayload; + return this; + } + + public StageExecutor withInvocationContextProvider( + InvocationContextProvider invocationContextProvider) { + + this.invocationContextProvider = invocationContextProvider; + return this; + } + + public StageExecutor withHookExecutionContext(HookExecutionContext hookExecutionContext) { + this.hookExecutionContext = hookExecutionContext; + return this; + } + + public StageExecutor withRejectAllowed(boolean rejectAllowed) { + this.rejectAllowed = rejectAllowed; + return this; + } + + public Future> execute() { + Future> stageFuture = Future.succeededFuture(StageResult.of(initialPayload, entity)); + + for (final ExecutionGroup group : executionPlan.getGroups()) { + stageFuture = stageFuture.compose(stageResult -> + executeGroup(group, stageResult.payload()) + .map(stageResult::applyGroupResult) + .compose(StageExecutor::propagateRejection)); + } + + return stageFuture + .recover(StageExecutor::restoreResultFromRejection) + .map(this::toHookStageExecutionResult); + } + + private Future> executeGroup(ExecutionGroup group, PAYLOAD initialPayload) { + return GroupExecutor.create(vertx, clock) + .withGroup(group) + .withInitialPayload(initialPayload) + .withHookProvider( + hookId -> hookCatalog.hookById(hookId.getModuleCode(), hookId.getHookImplCode(), stage)) + .withInvocationContextProvider(invocationContextProvider) + .withHookExecutionContext(hookExecutionContext) + .withRejectAllowed(rejectAllowed) + .execute(); + } + + private static Future> propagateRejection(StageResult stageResult) { + return stageResult.shouldReject() + ? Future.failedFuture(new RejectedException(stageResult)) + : Future.succeededFuture(stageResult); + + } + + private static Future restoreResultFromRejection(Throwable throwable) { + if (throwable instanceof RejectedException) { + return Future.succeededFuture(((RejectedException) throwable).result()); + } + + return Future.failedFuture(throwable); + } + + private HookStageExecutionResult toHookStageExecutionResult(StageResult stageResult) { + hookExecutionContext.getStageOutcomes().computeIfAbsent(stage.stage(), key -> new ArrayList<>()) + .add(stageResult.toStageExecutionOutcome()); + + return stageResult.shouldReject() + ? HookStageExecutionResult.reject() + : HookStageExecutionResult.success(stageResult.payload()); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/StageResult.java b/src/main/java/org/prebid/server/hooks/execution/StageResult.java new file mode 100644 index 00000000000..11847608587 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/StageResult.java @@ -0,0 +1,51 @@ +package org.prebid.server.hooks.execution; + +import lombok.Getter; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Accessors(fluent = true) +@Getter +class StageResult { + + private boolean shouldReject; + + private T payload; + private final String entity; + + private final List> groupResults = new ArrayList<>(); + + private StageResult(T payload, String entity) { + this.shouldReject = false; + this.payload = payload; + this.entity = entity; + } + + public static StageResult of(T payload, String entity) { + return new StageResult<>(payload, entity); + } + + public StageResult applyGroupResult(GroupResult groupResult) { + groupResults.add(groupResult); + + shouldReject = groupResult.shouldReject(); + payload = groupResult.payload(); + + return this; + } + + public StageExecutionOutcome toStageExecutionOutcome() { + return StageExecutionOutcome.of(entity, groupExecutionOutcomes()); + } + + private List groupExecutionOutcomes() { + return groupResults.stream() + .map(GroupResult::toGroupExecutionOutcome) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/EndpointExecutionPlan.java b/src/main/java/org/prebid/server/hooks/execution/model/EndpointExecutionPlan.java new file mode 100644 index 00000000000..4c71daf4166 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/EndpointExecutionPlan.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.execution.model; + +import lombok.Value; + +import java.util.Collections; +import java.util.Map; + +@Value(staticConstructor = "of") +public class EndpointExecutionPlan { + + private static final EndpointExecutionPlan EMPTY = of(Collections.emptyMap()); + + Map stages; + + public static EndpointExecutionPlan empty() { + return EMPTY; + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java new file mode 100644 index 00000000000..5e13aa3f14c --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java @@ -0,0 +1,6 @@ +package org.prebid.server.hooks.execution.model; + +public enum ExecutionAction { + + no_action, update, reject +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionGroup.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionGroup.java new file mode 100644 index 00000000000..c268731a157 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionGroup.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.execution.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ExecutionGroup { + + Long timeout; + + @JsonProperty("hook-sequence") + List hookSequence; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java new file mode 100644 index 00000000000..5d0af8b8e23 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.execution.model; + +import lombok.Value; +import org.prebid.server.model.Endpoint; + +import java.util.Collections; +import java.util.Map; + +@Value(staticConstructor = "of") +public class ExecutionPlan { + + private static final ExecutionPlan EMPTY = of(Collections.emptyMap()); + + Map endpoints; + + public static ExecutionPlan empty() { + return EMPTY; + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionStatus.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionStatus.java new file mode 100644 index 00000000000..6e8b6b67e81 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionStatus.java @@ -0,0 +1,6 @@ +package org.prebid.server.hooks.execution.model; + +public enum ExecutionStatus { + + success, failure, timeout, invocation_failure, execution_failure +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/GroupExecutionOutcome.java b/src/main/java/org/prebid/server/hooks/execution/model/GroupExecutionOutcome.java new file mode 100644 index 00000000000..91bd0915d88 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/GroupExecutionOutcome.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.execution.model; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class GroupExecutionOutcome { + + List hooks; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/HookExecutionContext.java b/src/main/java/org/prebid/server/hooks/execution/model/HookExecutionContext.java new file mode 100644 index 00000000000..32129252d41 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/HookExecutionContext.java @@ -0,0 +1,23 @@ +package org.prebid.server.hooks.execution.model; + +import lombok.Value; +import org.prebid.server.model.Endpoint; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class HookExecutionContext { + + Endpoint endpoint; + + EnumMap> stageOutcomes; + + Map moduleContexts = new HashMap<>(); + + public static HookExecutionContext of(Endpoint endpoint) { + return of(endpoint, new EnumMap<>(Stage.class)); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/HookExecutionOutcome.java b/src/main/java/org/prebid/server/hooks/execution/model/HookExecutionOutcome.java new file mode 100644 index 00000000000..0596a25da69 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/HookExecutionOutcome.java @@ -0,0 +1,30 @@ +package org.prebid.server.hooks.execution.model; + +import lombok.Builder; +import lombok.Value; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.List; + +@Builder +@Value +public class HookExecutionOutcome { + + HookId hookId; + + Long executionTime; + + ExecutionStatus status; + + String message; + + ExecutionAction action; + + List errors; + + List warnings; + + List debugMessages; + + Tags analyticsTags; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/HookId.java b/src/main/java/org/prebid/server/hooks/execution/model/HookId.java new file mode 100644 index 00000000000..8d7db1da069 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/HookId.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.execution.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class HookId { + + @JsonProperty("module-code") + String moduleCode; + + @JsonProperty("hook-impl-code") + String hookImplCode; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/HookStageExecutionResult.java b/src/main/java/org/prebid/server/hooks/execution/model/HookStageExecutionResult.java new file mode 100644 index 00000000000..5e867add4ef --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/HookStageExecutionResult.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.execution.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class HookStageExecutionResult { + + boolean shouldReject; + + PAYLOAD payload; + + public static HookStageExecutionResult success(PAYLOAD payload) { + return of(false, payload); + } + + public static HookStageExecutionResult reject() { + return of(true, null); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/Stage.java b/src/main/java/org/prebid/server/hooks/execution/model/Stage.java new file mode 100644 index 00000000000..5e2f6301786 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/Stage.java @@ -0,0 +1,36 @@ +package org.prebid.server.hooks.execution.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.Arrays; + +public enum Stage { + + entrypoint, + raw_auction_request("raw-auction-request"), + processed_auction_request("processed-auction-request"), + bidder_request("bidder-request"), + raw_bidder_response("raw-bidder-response"), + processed_bidder_response("processed-bidder-response"), + auction_response("auction-response"); + + @JsonValue + private final String value; + + Stage() { + this.value = name(); + } + + Stage(String value) { + this.value = value; + } + + @JsonCreator + public static Stage fromString(String value) { + return Arrays.stream(values()) + .filter(stage -> stage.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown stage")); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/StageExecutionOutcome.java b/src/main/java/org/prebid/server/hooks/execution/model/StageExecutionOutcome.java new file mode 100644 index 00000000000..602cec40619 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/StageExecutionOutcome.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.execution.model; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class StageExecutionOutcome { + + String entity; + + List groups; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/StageExecutionPlan.java b/src/main/java/org/prebid/server/hooks/execution/model/StageExecutionPlan.java new file mode 100644 index 00000000000..f835705d129 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/StageExecutionPlan.java @@ -0,0 +1,23 @@ +package org.prebid.server.hooks.execution.model; + +import lombok.Value; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@Value(staticConstructor = "of") +public class StageExecutionPlan { + + private static final StageExecutionPlan EMPTY = of(Collections.emptyList()); + + List groups; + + public static StageExecutionPlan empty() { + return EMPTY; + } + + public boolean isEmpty() { + return Objects.equals(this, EMPTY); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java new file mode 100644 index 00000000000..9eb9fa78110 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java @@ -0,0 +1,54 @@ +package org.prebid.server.hooks.execution.model; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionResponseHook; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; +import org.prebid.server.hooks.v1.bidder.BidderRequestHook; +import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; +import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; +import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; + +public interface StageWithHookType> { + + StageWithHookType ENTRYPOINT = + new StageWithHookTypeImpl<>(Stage.entrypoint, EntrypointHook.class); + StageWithHookType RAW_AUCTION_REQUEST = + new StageWithHookTypeImpl<>(Stage.raw_auction_request, RawAuctionRequestHook.class); + StageWithHookType PROCESSED_AUCTION_REQUEST = + new StageWithHookTypeImpl<>(Stage.processed_auction_request, ProcessedAuctionRequestHook.class); + StageWithHookType BIDDER_REQUEST = + new StageWithHookTypeImpl<>(Stage.bidder_request, BidderRequestHook.class); + StageWithHookType RAW_BIDDER_RESPONSE = + new StageWithHookTypeImpl<>(Stage.raw_bidder_response, RawBidderResponseHook.class); + StageWithHookType PROCESSED_BIDDER_RESPONSE = + new StageWithHookTypeImpl<>(Stage.processed_bidder_response, ProcessedBidderResponseHook.class); + StageWithHookType AUCTION_RESPONSE = + new StageWithHookTypeImpl<>(Stage.auction_response, AuctionResponseHook.class); + + Stage stage(); + + Class hookType(); + + static StageWithHookType> forStage(Stage stage) { + switch (stage) { + case entrypoint: + return ENTRYPOINT; + case raw_auction_request: + return RAW_AUCTION_REQUEST; + case processed_auction_request: + return PROCESSED_AUCTION_REQUEST; + case bidder_request: + return BIDDER_REQUEST; + case raw_bidder_response: + return RAW_BIDDER_RESPONSE; + case processed_bidder_response: + return PROCESSED_BIDDER_RESPONSE; + case auction_response: + return AUCTION_RESPONSE; + default: + throw new IllegalStateException(String.format("Unknown stage %s", stage)); + } + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookTypeImpl.java b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookTypeImpl.java new file mode 100644 index 00000000000..1f4f7a7f521 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookTypeImpl.java @@ -0,0 +1,17 @@ +package org.prebid.server.hooks.execution.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +@AllArgsConstructor +@Getter +@Accessors(fluent = true) +class StageWithHookTypeImpl> implements StageWithHookType { + + private final Stage stage; + + private final Class hookType; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java new file mode 100644 index 00000000000..6ed23ef8980 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.execution.v1; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.execution.Timeout; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.model.Endpoint; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class InvocationContextImpl implements InvocationContext { + + Timeout timeout; + + Endpoint endpoint; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/auction/AuctionInvocationContextImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/auction/AuctionInvocationContextImpl.java new file mode 100644 index 00000000000..aad1e99c05a --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/auction/AuctionInvocationContextImpl.java @@ -0,0 +1,22 @@ +package org.prebid.server.hooks.execution.v1.auction; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; +import lombok.experimental.Accessors; +import lombok.experimental.Delegate; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class AuctionInvocationContextImpl implements AuctionInvocationContext { + + @Delegate + InvocationContext invocationContext; + + boolean debugEnabled; + + ObjectNode accountConfig; + + Object moduleContext; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/auction/AuctionRequestPayloadImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/auction/AuctionRequestPayloadImpl.java new file mode 100644 index 00000000000..4befb75fa94 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/auction/AuctionRequestPayloadImpl.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.execution.v1.auction; + +import com.iab.openrtb.request.BidRequest; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class AuctionRequestPayloadImpl implements AuctionRequestPayload { + + BidRequest bidRequest; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/auction/AuctionResponsePayloadImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/auction/AuctionResponsePayloadImpl.java new file mode 100644 index 00000000000..04cc50a2882 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/auction/AuctionResponsePayloadImpl.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.execution.v1.auction; + +import com.iab.openrtb.response.BidResponse; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class AuctionResponsePayloadImpl implements AuctionResponsePayload { + + BidResponse bidResponse; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/bidder/BidderInvocationContextImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/bidder/BidderInvocationContextImpl.java new file mode 100644 index 00000000000..e714f5b2fd6 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/bidder/BidderInvocationContextImpl.java @@ -0,0 +1,17 @@ +package org.prebid.server.hooks.execution.v1.bidder; + +import lombok.Value; +import lombok.experimental.Accessors; +import lombok.experimental.Delegate; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class BidderInvocationContextImpl implements BidderInvocationContext { + + @Delegate + AuctionInvocationContext auctionInvocationContext; + + String bidder; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/bidder/BidderRequestPayloadImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/bidder/BidderRequestPayloadImpl.java new file mode 100644 index 00000000000..549e78fc44b --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/bidder/BidderRequestPayloadImpl.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.execution.v1.bidder; + +import com.iab.openrtb.request.BidRequest; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class BidderRequestPayloadImpl implements BidderRequestPayload { + + BidRequest bidRequest; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/bidder/BidderResponsePayloadImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/bidder/BidderResponsePayloadImpl.java new file mode 100644 index 00000000000..40a6e65ebca --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/bidder/BidderResponsePayloadImpl.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.execution.v1.bidder; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; + +import java.util.List; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class BidderResponsePayloadImpl implements BidderResponsePayload { + + List bids; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/entrypoint/EntrypointPayloadImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/entrypoint/EntrypointPayloadImpl.java new file mode 100644 index 00000000000..536d61a2de1 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/entrypoint/EntrypointPayloadImpl.java @@ -0,0 +1,17 @@ +package org.prebid.server.hooks.execution.v1.entrypoint; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.model.CaseInsensitiveMultiMap; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class EntrypointPayloadImpl implements EntrypointPayload { + + CaseInsensitiveMultiMap queryParams; + + CaseInsensitiveMultiMap headers; + + String body; +} diff --git a/src/main/java/org/prebid/server/hooks/v1/Hook.java b/src/main/java/org/prebid/server/hooks/v1/Hook.java new file mode 100644 index 00000000000..83c24ddf23e --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/Hook.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.v1; + +import io.vertx.core.Future; + +public interface Hook { + + Future> call(PAYLOAD payload, CONTEXT invocationContext); + + String code(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java b/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java new file mode 100644 index 00000000000..29b22bf1b3d --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java @@ -0,0 +1,6 @@ +package org.prebid.server.hooks.v1; + +public enum InvocationAction { + + no_action, update, reject +} diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java b/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java new file mode 100644 index 00000000000..7c3b6c922d3 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.v1; + +import org.prebid.server.execution.Timeout; +import org.prebid.server.model.Endpoint; + +public interface InvocationContext { + + Timeout timeout(); + + Endpoint endpoint(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationResult.java b/src/main/java/org/prebid/server/hooks/v1/InvocationResult.java new file mode 100644 index 00000000000..979ff697316 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationResult.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.v1; + +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.List; + +public interface InvocationResult { + + InvocationStatus status(); + + String message(); + + InvocationAction action(); + + PayloadUpdate payloadUpdate(); + + List errors(); + + List warnings(); + + List debugMessages(); + + Object moduleContext(); + + Tags analyticsTags(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationStatus.java b/src/main/java/org/prebid/server/hooks/v1/InvocationStatus.java new file mode 100644 index 00000000000..52b51c28729 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationStatus.java @@ -0,0 +1,6 @@ +package org.prebid.server.hooks.v1; + +public enum InvocationStatus { + + success, failure +} diff --git a/src/main/java/org/prebid/server/hooks/v1/Module.java b/src/main/java/org/prebid/server/hooks/v1/Module.java new file mode 100644 index 00000000000..18b7ef06da3 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/Module.java @@ -0,0 +1,22 @@ +package org.prebid.server.hooks.v1; + +import java.util.Collection; + +/** + * Cares of the module identification among other modules and supplies a collection of available {@link Hook}s. + *

+ * This interface is used to keep knowledge of which {@link Hook} is belongs to certain {@link Module} + * while running execution plan. + */ +public interface Module { + + /** + * An identifier that should be unique among other available modules. + */ + String code(); + + /** + * Collection of hooks available through the module. + */ + Collection> hooks(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/PayloadUpdate.java b/src/main/java/org/prebid/server/hooks/v1/PayloadUpdate.java new file mode 100644 index 00000000000..bb1b016e3b9 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/PayloadUpdate.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.v1; + +import java.util.function.UnaryOperator; + +@FunctionalInterface +public interface PayloadUpdate extends UnaryOperator { + + static PayloadUpdate identity() { + return payload -> payload; + } +} diff --git a/src/main/java/org/prebid/server/hooks/v1/analytics/Activity.java b/src/main/java/org/prebid/server/hooks/v1/analytics/Activity.java new file mode 100644 index 00000000000..8f04bab7936 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/analytics/Activity.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.v1.analytics; + +import java.util.List; + +public interface Activity { + + String name(); + + String status(); + + List results(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/analytics/AppliedTo.java b/src/main/java/org/prebid/server/hooks/v1/analytics/AppliedTo.java new file mode 100644 index 00000000000..c1a926c776e --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/analytics/AppliedTo.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.v1.analytics; + +import java.util.List; + +public interface AppliedTo { + + List impIds(); + + List bidders(); + + boolean request(); + + boolean response(); + + List bidIds(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/analytics/Result.java b/src/main/java/org/prebid/server/hooks/v1/analytics/Result.java new file mode 100644 index 00000000000..c8a2f1c97df --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/analytics/Result.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.v1.analytics; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +public interface Result { + + String status(); + + ObjectNode values(); + + AppliedTo appliedTo(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/analytics/Tags.java b/src/main/java/org/prebid/server/hooks/v1/analytics/Tags.java new file mode 100644 index 00000000000..52f3a594d24 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/analytics/Tags.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.v1.analytics; + +import java.util.List; + +public interface Tags { + + List activities(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/auction/AuctionInvocationContext.java b/src/main/java/org/prebid/server/hooks/v1/auction/AuctionInvocationContext.java new file mode 100644 index 00000000000..137de12469c --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/auction/AuctionInvocationContext.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.v1.auction; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.prebid.server.hooks.v1.InvocationContext; + +public interface AuctionInvocationContext extends InvocationContext { + + Object moduleContext(); + + boolean debugEnabled(); + + ObjectNode accountConfig(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/auction/AuctionRequestPayload.java b/src/main/java/org/prebid/server/hooks/v1/auction/AuctionRequestPayload.java new file mode 100644 index 00000000000..8bd5c77aec2 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/auction/AuctionRequestPayload.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.v1.auction; + +import com.iab.openrtb.request.BidRequest; + +public interface AuctionRequestPayload { + + BidRequest bidRequest(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/auction/AuctionResponseHook.java b/src/main/java/org/prebid/server/hooks/v1/auction/AuctionResponseHook.java new file mode 100644 index 00000000000..5db7fa17ab1 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/auction/AuctionResponseHook.java @@ -0,0 +1,7 @@ +package org.prebid.server.hooks.v1.auction; + +import org.prebid.server.hooks.v1.Hook; + +public interface AuctionResponseHook extends Hook { + +} diff --git a/src/main/java/org/prebid/server/hooks/v1/auction/AuctionResponsePayload.java b/src/main/java/org/prebid/server/hooks/v1/auction/AuctionResponsePayload.java new file mode 100644 index 00000000000..851a44817a8 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/auction/AuctionResponsePayload.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.v1.auction; + +import com.iab.openrtb.response.BidResponse; + +public interface AuctionResponsePayload { + + BidResponse bidResponse(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/auction/ProcessedAuctionRequestHook.java b/src/main/java/org/prebid/server/hooks/v1/auction/ProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..21afd63af19 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/auction/ProcessedAuctionRequestHook.java @@ -0,0 +1,7 @@ +package org.prebid.server.hooks.v1.auction; + +import org.prebid.server.hooks.v1.Hook; + +public interface ProcessedAuctionRequestHook extends Hook { + +} diff --git a/src/main/java/org/prebid/server/hooks/v1/auction/RawAuctionRequestHook.java b/src/main/java/org/prebid/server/hooks/v1/auction/RawAuctionRequestHook.java new file mode 100644 index 00000000000..a563a6fc183 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/auction/RawAuctionRequestHook.java @@ -0,0 +1,7 @@ +package org.prebid.server.hooks.v1.auction; + +import org.prebid.server.hooks.v1.Hook; + +public interface RawAuctionRequestHook extends Hook { + +} diff --git a/src/main/java/org/prebid/server/hooks/v1/bidder/BidderInvocationContext.java b/src/main/java/org/prebid/server/hooks/v1/bidder/BidderInvocationContext.java new file mode 100644 index 00000000000..6d8d1872935 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/bidder/BidderInvocationContext.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.v1.bidder; + +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; + +public interface BidderInvocationContext extends AuctionInvocationContext { + + String bidder(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/bidder/BidderRequestHook.java b/src/main/java/org/prebid/server/hooks/v1/bidder/BidderRequestHook.java new file mode 100644 index 00000000000..5ca14a7f7cf --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/bidder/BidderRequestHook.java @@ -0,0 +1,7 @@ +package org.prebid.server.hooks.v1.bidder; + +import org.prebid.server.hooks.v1.Hook; + +public interface BidderRequestHook extends Hook { + +} diff --git a/src/main/java/org/prebid/server/hooks/v1/bidder/BidderRequestPayload.java b/src/main/java/org/prebid/server/hooks/v1/bidder/BidderRequestPayload.java new file mode 100644 index 00000000000..c4cafa3d89e --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/bidder/BidderRequestPayload.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.v1.bidder; + +import com.iab.openrtb.request.BidRequest; + +public interface BidderRequestPayload { + + BidRequest bidRequest(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/bidder/BidderResponsePayload.java b/src/main/java/org/prebid/server/hooks/v1/bidder/BidderResponsePayload.java new file mode 100644 index 00000000000..d485164389e --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/bidder/BidderResponsePayload.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.v1.bidder; + +import org.prebid.server.bidder.model.BidderBid; + +import java.util.List; + +public interface BidderResponsePayload { + + List bids(); +} diff --git a/src/main/java/org/prebid/server/hooks/v1/bidder/ProcessedBidderResponseHook.java b/src/main/java/org/prebid/server/hooks/v1/bidder/ProcessedBidderResponseHook.java new file mode 100644 index 00000000000..f0f03d18908 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/bidder/ProcessedBidderResponseHook.java @@ -0,0 +1,7 @@ +package org.prebid.server.hooks.v1.bidder; + +import org.prebid.server.hooks.v1.Hook; + +public interface ProcessedBidderResponseHook extends Hook { + +} diff --git a/src/main/java/org/prebid/server/hooks/v1/bidder/RawBidderResponseHook.java b/src/main/java/org/prebid/server/hooks/v1/bidder/RawBidderResponseHook.java new file mode 100644 index 00000000000..a47b63764c6 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/bidder/RawBidderResponseHook.java @@ -0,0 +1,7 @@ +package org.prebid.server.hooks.v1.bidder; + +import org.prebid.server.hooks.v1.Hook; + +public interface RawBidderResponseHook extends Hook { + +} diff --git a/src/main/java/org/prebid/server/hooks/v1/entrypoint/EntrypointHook.java b/src/main/java/org/prebid/server/hooks/v1/entrypoint/EntrypointHook.java new file mode 100644 index 00000000000..a8aaee16ecf --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/entrypoint/EntrypointHook.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.v1.entrypoint; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +public interface EntrypointHook extends Hook { + +} diff --git a/src/main/java/org/prebid/server/hooks/v1/entrypoint/EntrypointPayload.java b/src/main/java/org/prebid/server/hooks/v1/entrypoint/EntrypointPayload.java new file mode 100644 index 00000000000..d6dd6e8b61c --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/entrypoint/EntrypointPayload.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.v1.entrypoint; + +import org.prebid.server.model.CaseInsensitiveMultiMap; + +public interface EntrypointPayload { + + CaseInsensitiveMultiMap queryParams(); + + CaseInsensitiveMultiMap headers(); + + String body(); +} diff --git a/src/main/java/org/prebid/server/identity/IdGenerator.java b/src/main/java/org/prebid/server/identity/IdGenerator.java index 4b75fe47ebc..ea34457d98c 100644 --- a/src/main/java/org/prebid/server/identity/IdGenerator.java +++ b/src/main/java/org/prebid/server/identity/IdGenerator.java @@ -6,4 +6,6 @@ public interface IdGenerator { String generateId(); + + IdGeneratorType getType(); } diff --git a/src/main/java/org/prebid/server/identity/NoneIdGenerator.java b/src/main/java/org/prebid/server/identity/NoneIdGenerator.java index 5e458bf1875..14aaba41886 100644 --- a/src/main/java/org/prebid/server/identity/NoneIdGenerator.java +++ b/src/main/java/org/prebid/server/identity/NoneIdGenerator.java @@ -9,4 +9,9 @@ public class NoneIdGenerator implements IdGenerator { public String generateId() { return null; } + + @Override + public IdGeneratorType getType() { + return IdGeneratorType.none; + } } diff --git a/src/main/java/org/prebid/server/identity/UUIDIdGenerator.java b/src/main/java/org/prebid/server/identity/UUIDIdGenerator.java index 927c0255cc5..7f6e95ef371 100644 --- a/src/main/java/org/prebid/server/identity/UUIDIdGenerator.java +++ b/src/main/java/org/prebid/server/identity/UUIDIdGenerator.java @@ -11,4 +11,9 @@ public class UUIDIdGenerator implements IdGenerator { public String generateId() { return UUID.randomUUID().toString(); } + + @Override + public IdGeneratorType getType() { + return IdGeneratorType.uuid; + } } diff --git a/src/main/java/org/prebid/server/json/IntegerFlagDeserializer.java b/src/main/java/org/prebid/server/json/IntegerFlagDeserializer.java new file mode 100644 index 00000000000..26cf097f350 --- /dev/null +++ b/src/main/java/org/prebid/server/json/IntegerFlagDeserializer.java @@ -0,0 +1,36 @@ +package org.prebid.server.json; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import java.io.IOException; + +/** + * Deserialized json boolean FALSE to 0 and TRUE to 1. + */ +public class IntegerFlagDeserializer extends StdDeserializer { + + public IntegerFlagDeserializer() { + super(Integer.class); + } + + @Override + public Integer deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException { + switch (parser.getCurrentToken()) { + case VALUE_NUMBER_INT: + return parser.getValueAsInt(); + case VALUE_FALSE: + return 0; + case VALUE_TRUE: + return 1; + default: + ctxt.reportWrongTokenException(JsonToken.class, JsonToken.VALUE_NUMBER_INT, + String.format("Failed to parse field %s to Integer type with a reason: Expected type boolean" + + " or integer(`0` or `1`).", parser.getCurrentName())); + // the previous method should have thrown + throw new AssertionError(); + } + } +} diff --git a/src/main/java/org/prebid/server/json/JsonMerger.java b/src/main/java/org/prebid/server/json/JsonMerger.java new file mode 100644 index 00000000000..cd6b8aceee7 --- /dev/null +++ b/src/main/java/org/prebid/server/json/JsonMerger.java @@ -0,0 +1,78 @@ +package org.prebid.server.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jsonpatch.JsonPatchException; +import com.github.fge.jsonpatch.mergepatch.JsonMergePatch; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.exception.InvalidRequestException; + +import java.io.IOException; +import java.util.Objects; + +public class JsonMerger { + + private final JacksonMapper mapper; + + public JsonMerger(JacksonMapper mapper) { + this.mapper = Objects.requireNonNull(mapper); + } + + /** + * Merges passed object with json retrieved from stored data map by id + * and cast it to appropriate class. In case of any exception during merging, throws {@link InvalidRequestException} + * with reason message. + */ + public T merge(T originalObject, String storedData, String id, Class classToCast) { + final JsonNode originJsonNode = mapper.mapper().valueToTree(originalObject); + final JsonNode storedRequestJsonNode; + try { + storedRequestJsonNode = mapper.mapper().readTree(storedData); + } catch (IOException e) { + throw new InvalidRequestException( + String.format("Can't parse Json for stored request with id %s", id)); + } + try { + // Http request fields have higher priority and will override fields from stored requests + // in case they have different values + return mapper.mapper().treeToValue(JsonMergePatch.fromJson(originJsonNode).apply(storedRequestJsonNode), + classToCast); + } catch (JsonPatchException e) { + throw new InvalidRequestException(String.format( + "Couldn't create merge patch from origin object node for id %s: %s", id, e.getMessage())); + } catch (JsonProcessingException e) { + throw new InvalidRequestException( + String.format("Can't convert merging result for id %s: %s", id, e.getMessage())); + } + } + + public T merge(T originalObject, T mergingObject, Class classToCast) { + if (!ObjectUtils.allNotNull(originalObject, mergingObject)) { + return ObjectUtils.defaultIfNull(originalObject, mergingObject); + } + + final JsonNode originJsonNode = mapper.mapper().valueToTree(originalObject); + final JsonNode mergingObjectJsonNode = mapper.mapper().valueToTree(mergingObject); + try { + final JsonNode mergedNode = JsonMergePatch.fromJson(originJsonNode).apply(mergingObjectJsonNode); + return mapper.mapper().treeToValue(mergedNode, classToCast); + } catch (JsonPatchException e) { + throw new InvalidRequestException(String.format( + "Couldn't create merge patch for objects with class %s", classToCast.getName())); + } catch (JsonProcessingException e) { + throw new InvalidRequestException( + String.format("Can't convert merging result class %s", classToCast.getName())); + } + } + + /** + * Returns 'toNode' with merged properties from 'fromNode'. + */ + public JsonNode merge(JsonNode fromNode, JsonNode toNode) { + try { + return JsonMergePatch.fromJson(fromNode).apply(toNode); + } catch (JsonPatchException e) { + throw new InvalidRequestException("Couldn't create merge patch for json nodes"); + } + } +} diff --git a/src/main/java/org/prebid/server/json/LongAdderModule.java b/src/main/java/org/prebid/server/json/LongAdderModule.java new file mode 100644 index 00000000000..8f89dae8f82 --- /dev/null +++ b/src/main/java/org/prebid/server/json/LongAdderModule.java @@ -0,0 +1,39 @@ +package org.prebid.server.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import java.io.IOException; +import java.util.concurrent.atomic.LongAdder; + +class LongAdderModule extends SimpleModule { + + LongAdderModule() { + addSerializer(LongAdder.class, new LongAdderSerializer()); + addDeserializer(LongAdder.class, new LongAdderDeserializer()); + } + + private static class LongAdderSerializer extends JsonSerializer { + + @Override + public void serialize(LongAdder value, JsonGenerator generator, SerializerProvider provider) + throws IOException { + generator.writeNumber(value.longValue()); + } + } + + private static class LongAdderDeserializer extends JsonDeserializer { + + @Override + public LongAdder deserialize(JsonParser parser, DeserializationContext ctx) throws IOException { + final LongAdder result = new LongAdder(); + result.add(parser.getLongValue()); + return result; + } + } +} diff --git a/src/main/java/org/prebid/server/json/ObjectMapperProvider.java b/src/main/java/org/prebid/server/json/ObjectMapperProvider.java index 47d311225fa..fe135fca51d 100644 --- a/src/main/java/org/prebid/server/json/ObjectMapperProvider.java +++ b/src/main/java/org/prebid/server/json/ObjectMapperProvider.java @@ -22,7 +22,9 @@ public final class ObjectMapperProvider { .setSerializationInclusion(JsonInclude.Include.NON_NULL) .registerModule(new AfterburnerModule()) .registerModule(new ZonedDateTimeModule()) - .registerModule(new MissingJsonNodeModule()); + .registerModule(new MissingJsonNodeModule()) + .registerModule(new ZonedDateTimeModule()) + .registerModule(new LongAdderModule()); } private ObjectMapperProvider() { diff --git a/src/main/java/org/prebid/server/json/ZonedDateTimeModule.java b/src/main/java/org/prebid/server/json/ZonedDateTimeModule.java index bce4d90c26a..6a2b60bd649 100644 --- a/src/main/java/org/prebid/server/json/ZonedDateTimeModule.java +++ b/src/main/java/org/prebid/server/json/ZonedDateTimeModule.java @@ -14,7 +14,6 @@ import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; -@SuppressWarnings("serial") class ZonedDateTimeModule extends SimpleModule { // see https://stackoverflow.com/q/30090710 diff --git a/src/main/java/org/prebid/server/log/ConditionalLogger.java b/src/main/java/org/prebid/server/log/ConditionalLogger.java index d453a3b4865..cb9a884c8b7 100644 --- a/src/main/java/org/prebid/server/log/ConditionalLogger.java +++ b/src/main/java/org/prebid/server/log/ConditionalLogger.java @@ -7,6 +7,7 @@ import java.time.Instant; import java.util.Objects; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -19,8 +20,8 @@ public class ConditionalLogger { private final String key; private final Logger logger; - private ConcurrentMap messageToCount; - private ConcurrentMap messageToWait; + private final ConcurrentMap messageToCount; + private final ConcurrentMap messageToWait; public ConditionalLogger(String key, Logger logger) { this.key = key; // can be null @@ -43,6 +44,10 @@ public ConditionalLogger(Logger logger) { this(null, logger); } + public void infoWithKey(String key, String message, int limit) { + log(key, limit, logger -> logger.info(message)); + } + public void info(String message, int limit) { log(message, limit, logger -> logger.info(message)); } @@ -51,6 +56,16 @@ public void info(String message, long duration, TimeUnit unit) { log(message, duration, unit, logger -> logger.info(message)); } + public void info(String message, double samplingRate) { + if (samplingRate >= 1.0d || ThreadLocalRandom.current().nextDouble() < samplingRate) { + logger.warn(message); + } + } + + public void errorWithKey(String key, String message, int limit) { + log(key, limit, logger -> logger.error(message)); + } + public void error(String message, int limit) { log(message, limit, logger -> logger.error(message)); } @@ -59,6 +74,12 @@ public void error(String message, long duration, TimeUnit unit) { log(message, duration, unit, logger -> logger.error(message)); } + public void error(String message, double samplingRate) { + if (samplingRate >= 1.0d || ThreadLocalRandom.current().nextDouble() < samplingRate) { + logger.error(message); + } + } + public void debug(String message, int limit) { log(message, limit, logger -> logger.debug(message)); } @@ -75,6 +96,12 @@ public void warn(String message, long duration, TimeUnit unit) { log(message, duration, unit, logger -> logger.warn(message)); } + public void warn(String message, double samplingRate) { + if (samplingRate >= 1.0d || ThreadLocalRandom.current().nextDouble() < samplingRate) { + logger.warn(message); + } + } + /** * Calls {@link Consumer} if the given limit for specified key is not exceeded. */ diff --git a/src/main/java/org/prebid/server/log/Criteria.java b/src/main/java/org/prebid/server/log/Criteria.java new file mode 100644 index 00000000000..ff8f2a5c410 --- /dev/null +++ b/src/main/java/org/prebid/server/log/Criteria.java @@ -0,0 +1,80 @@ +package org.prebid.server.log; + +import io.vertx.core.logging.Logger; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Value +@Builder +@AllArgsConstructor +public class Criteria { + + private static final String TAG_SEPARATOR = "-"; + private static final String TAGGED_MESSAGE_PATTERN = "[%s]: %s"; + private static final String TAGGED_RESPONSE_PATTERN = "[%s]: %s - %s"; + public static final String BID_RESPONSE = "BidResponse"; + public static final String RESOLVED_BID_REQUEST = "Resolved BidRequest"; + + String account; + + String bidder; + + String lineItemId; + + String tag; + + BiConsumer loggerLevel; + + public static Criteria create(String account, String bidder, String lineItemId, + BiConsumer loggerLevel) { + return new Criteria(account, bidder, lineItemId, makeTag(account, bidder, lineItemId), loggerLevel); + } + + public void log(Criteria criteria, Logger logger, Object message, Consumer defaultLogger) { + if (isMatched(criteria)) { + loggerLevel.accept(logger, String.format(TAGGED_MESSAGE_PATTERN, tag, message)); + } else { + defaultLogger.accept(message); + } + } + + public void logResponse(String bidResponse, Logger logger) { + if (isMatchedToString(bidResponse)) { + loggerLevel.accept(logger, String.format(TAGGED_RESPONSE_PATTERN, tag, BID_RESPONSE, bidResponse)); + } + } + + public void logResponseAndRequest(String bidResponse, String bidRequest, Logger logger) { + if (isMatchedToString(bidResponse + bidRequest)) { + loggerLevel.accept(logger, String.format(TAGGED_RESPONSE_PATTERN, tag, BID_RESPONSE, bidResponse)); + loggerLevel.accept(logger, String.format(TAGGED_RESPONSE_PATTERN, tag, RESOLVED_BID_REQUEST, bidRequest)); + } + } + + private boolean isMatched(Criteria criteria) { + return criteria != null + && ((account == null || account.equals(criteria.account)) + && (bidder == null || bidder.equals(criteria.bidder)) + && (lineItemId == null || lineItemId.equals(criteria.lineItemId))); + } + + private boolean isMatchedToString(String value) { + return (account == null || value.contains(account)) + && (bidder == null || value.contains(bidder)) + && (lineItemId == null || value.contains(lineItemId)); + } + + private static String makeTag(String account, String bidder, String lineItemId) { + return Stream.of(account, bidder, lineItemId) + .filter(Objects::nonNull) + .collect(Collectors.joining(TAG_SEPARATOR)); + } + +} diff --git a/src/main/java/org/prebid/server/log/CriteriaLogManager.java b/src/main/java/org/prebid/server/log/CriteriaLogManager.java new file mode 100644 index 00000000000..53dd6f21973 --- /dev/null +++ b/src/main/java/org/prebid/server/log/CriteriaLogManager.java @@ -0,0 +1,80 @@ +package org.prebid.server.log; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import io.vertx.core.impl.ConcurrentHashSet; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.json.EncodeException; +import org.prebid.server.json.JacksonMapper; + +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +public class CriteriaLogManager { + + private static final Logger logger = LoggerFactory.getLogger(CriteriaLogManager.class); + + private final Set criterias = new ConcurrentHashSet<>(); + + private final JacksonMapper mapper; + + public CriteriaLogManager(JacksonMapper mapper) { + this.mapper = Objects.requireNonNull(mapper); + } + + public void log(Logger logger, Criteria criteria, Object message, Consumer defaultLogger) { + if (criterias.isEmpty()) { + defaultLogger.accept(message); + } + criterias.forEach(cr -> cr.log(criteria, logger, message, defaultLogger)); + } + + public void log(Logger logger, String account, Object message, Consumer defaultLogger) { + log(logger, Criteria.builder().account(account).build(), message, defaultLogger); + } + + public void log(Logger logger, String account, String bidder, String lineItemId, Object message, + Consumer defaultLogger) { + log(logger, Criteria.builder().account(account).bidder(bidder).lineItemId(lineItemId).build(), + message, defaultLogger); + } + + public BidResponse traceResponse(Logger logger, BidResponse bidResponse, BidRequest bidRequest, + boolean debugEnabled) { + if (criterias.isEmpty()) { + return bidResponse; + } + + final String jsonBidResponse; + final String jsonBidRequest; + try { + jsonBidResponse = mapper.encode(bidResponse); + jsonBidRequest = debugEnabled ? null : mapper.encode(bidRequest); + } catch (EncodeException e) { + CriteriaLogManager.logger.warn("Failed to parse bidResponse or bidRequest to json string: {0}", e); + return bidResponse; + } + + if (debugEnabled) { + criterias.forEach(criteria -> criteria.logResponse(jsonBidResponse, logger)); + } else { + criterias.forEach(criteria -> criteria.logResponseAndRequest(jsonBidResponse, jsonBidRequest, logger)); + } + + return bidResponse; + } + + public void removeCriteria(Criteria criteria) { + criterias.remove(criteria); + } + + public void addCriteria(Criteria criteria) { + criterias.add(criteria); + } + + public void removeAllCriteria() { + criterias.clear(); + } +} diff --git a/src/main/java/org/prebid/server/log/CriteriaManager.java b/src/main/java/org/prebid/server/log/CriteriaManager.java new file mode 100644 index 00000000000..90934519c8d --- /dev/null +++ b/src/main/java/org/prebid/server/log/CriteriaManager.java @@ -0,0 +1,80 @@ +package org.prebid.server.log; + +import io.vertx.core.Vertx; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.deals.model.LogCriteriaFilter; + +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +public class CriteriaManager { + + private static final long MAX_CRITERIA_DURATION = 300000L; + + private static final Logger logger = LoggerFactory.getLogger(CriteriaManager.class); + + private final CriteriaLogManager criteriaLogManager; + private final Vertx vertx; + + public CriteriaManager(CriteriaLogManager criteriaLogManager, Vertx vertx) { + this.criteriaLogManager = criteriaLogManager; + this.vertx = vertx; + } + + public void addCriteria(String accountId, String bidderCode, String lineItemId, String loggerLevel, + Integer durationMillis) { + final Criteria criteria = Criteria.create(accountId, bidderCode, lineItemId, resolveLogLevel(loggerLevel)); + criteriaLogManager.addCriteria(criteria); + vertx.setTimer(limitDuration(durationMillis), ignored -> criteriaLogManager.removeCriteria(criteria)); + } + + public void addCriteria(LogCriteriaFilter filter, Long durationSeconds) { + if (filter != null) { + final Criteria criteria = Criteria.create(filter.getAccountId(), filter.getBidderCode(), + filter.getLineItemId(), Logger::error); + criteriaLogManager.addCriteria(criteria); + logger.info("Logger was updated with new criteria {0}", criteria); + vertx.setTimer(limitDuration(TimeUnit.SECONDS.toMillis(durationSeconds)), + ignored -> criteriaLogManager.removeCriteria(criteria)); + } + } + + public void stop() { + criteriaLogManager.removeAllCriteria(); + } + + private long limitDuration(long durationMillis) { + return Math.min(durationMillis, MAX_CRITERIA_DURATION); + } + + private BiConsumer resolveLogLevel(String rawLogLevel) { + final LogLevel logLevel; + try { + logLevel = LogLevel.valueOf(rawLogLevel.toLowerCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(String.format("Invalid LoggingLevel: %s", rawLogLevel)); + } + + switch (logLevel) { + case info: + return Logger::info; + case warn: + return Logger::warn; + case trace: + return Logger::trace; + case error: + return Logger::error; + case fatal: + return Logger::fatal; + case debug: + return Logger::debug; + default: + throw new IllegalArgumentException(String.format("Unknown LoggingLevel: %s", logLevel)); + } + } + + private enum LogLevel { + info, warn, trace, error, fatal, debug + } +} diff --git a/src/main/java/org/prebid/server/log/HttpInteractionLogger.java b/src/main/java/org/prebid/server/log/HttpInteractionLogger.java index 9dce8abd643..585b5a45268 100644 --- a/src/main/java/org/prebid/server/log/HttpInteractionLogger.java +++ b/src/main/java/org/prebid/server/log/HttpInteractionLogger.java @@ -1,5 +1,6 @@ package org.prebid.server.log; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; @@ -17,6 +18,7 @@ import org.prebid.server.settings.model.Account; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -27,10 +29,11 @@ public class HttpInteractionLogger { private static final Logger logger = LoggerFactory.getLogger(HTTP_INTERACTION_LOGGER_NAME); private final JacksonMapper mapper; + private final AtomicReference specWithCounter = new AtomicReference<>(); public HttpInteractionLogger(JacksonMapper mapper) { - this.mapper = mapper; + this.mapper = Objects.requireNonNull(mapper); } public void setSpec(HttpLogSpec spec) { @@ -46,7 +49,7 @@ public void maybeLogOpenrtb2Auction(AuctionContext auctionContext, logger.info( "Requested URL: \"{0}\", request body: \"{1}\", response status: \"{2}\", response body: \"{3}\"", routingContext.request().uri(), - routingContext.getBody().toString(), + toOneLineString(routingContext.getBodyAsString()), statusCode, responseBody); @@ -54,6 +57,14 @@ public void maybeLogOpenrtb2Auction(AuctionContext auctionContext, } } + private String toOneLineString(String value) { + try { + return mapper.encode(mapper.mapper().readTree(value)); + } catch (JsonProcessingException e) { + return String.format("Not parseable JSON passed: %s", value.replaceAll("[\r\n]+", " ")); + } + } + public void maybeLogOpenrtb2Amp(AuctionContext auctionContext, RoutingContext routingContext, int statusCode, diff --git a/src/main/java/org/prebid/server/metric/AccountMetrics.java b/src/main/java/org/prebid/server/metric/AccountMetrics.java index 2dcfbba217a..2cedd43d7ca 100644 --- a/src/main/java/org/prebid/server/metric/AccountMetrics.java +++ b/src/main/java/org/prebid/server/metric/AccountMetrics.java @@ -12,26 +12,28 @@ */ class AccountMetrics extends UpdatableMetrics { - private final Function adapterMetricsCreator; // not thread-safe maps are intentionally used here because it's harmless in this particular case - eventually // this all boils down to metrics lookup by underlying metric registry and that operation is guaranteed to be // thread-safe - private final Map adapterMetrics; private final Function requestTypeMetricsCreator; private final Map requestTypeMetrics; + private final AdapterMetrics adapterMetrics; private final RequestMetrics requestsMetrics; private final CacheMetrics cacheMetrics; + private final ResponseMetrics responseMetrics; + private final HooksMetrics hooksMetrics; AccountMetrics(MetricRegistry metricRegistry, CounterType counterType, String account) { super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), nameCreator(createPrefix(Objects.requireNonNull(account)))); - adapterMetricsCreator = adapterType -> new AdapterMetrics(metricRegistry, counterType, account, adapterType); - adapterMetrics = new HashMap<>(); requestTypeMetricsCreator = requestType -> new RequestTypeMetrics(metricRegistry, counterType, createPrefix(account), requestType); + adapterMetrics = new AdapterMetrics(metricRegistry, counterType, createPrefix(account)); requestTypeMetrics = new HashMap<>(); requestsMetrics = new RequestMetrics(metricRegistry, counterType, createPrefix(account)); cacheMetrics = new CacheMetrics(metricRegistry, counterType, createPrefix(account)); + responseMetrics = new ResponseMetrics(metricRegistry, counterType, createPrefix(account)); + hooksMetrics = new HooksMetrics(metricRegistry, counterType, createPrefix(account)); } private static String createPrefix(String account) { @@ -42,8 +44,8 @@ private static Function nameCreator(String prefix) { return metricName -> String.format("%s.%s", prefix, metricName.toString()); } - AdapterMetrics forAdapter(String adapterType) { - return adapterMetrics.computeIfAbsent(adapterType, adapterMetricsCreator); + AdapterMetrics adapter() { + return adapterMetrics; } RequestTypeMetrics requestType(MetricName requestType) { @@ -57,4 +59,12 @@ RequestMetrics requests() { CacheMetrics cache() { return cacheMetrics; } + + ResponseMetrics response() { + return responseMetrics; + } + + HooksMetrics hooks() { + return hooksMetrics; + } } diff --git a/src/main/java/org/prebid/server/metric/AdapterMetrics.java b/src/main/java/org/prebid/server/metric/AdapterMetrics.java index 80b406ec94a..96c3621e431 100644 --- a/src/main/java/org/prebid/server/metric/AdapterMetrics.java +++ b/src/main/java/org/prebid/server/metric/AdapterMetrics.java @@ -12,61 +12,27 @@ */ class AdapterMetrics extends UpdatableMetrics { - private final Function requestTypeMetricsCreator; - private final Map requestTypeMetrics; - private final RequestMetrics requestMetrics; - private final Function bidTypeMetricsCreator; - private final Map bidTypeMetrics; + private final Function adapterMetricsCreator; + private final Map adapterMetrics; - AdapterMetrics(MetricRegistry metricRegistry, CounterType counterType, String adapterType) { + AdapterMetrics(MetricRegistry metricRegistry, CounterType counterType, String accountPrefix) { super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), - nameCreator(createAdapterPrefix(Objects.requireNonNull(adapterType)))); + nameCreator(createAdapterSuffix(Objects.requireNonNull(accountPrefix)))); - bidTypeMetricsCreator = bidType -> - new BidTypeMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType), bidType); - requestTypeMetricsCreator = requestType -> - new RequestTypeMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType), requestType); - requestTypeMetrics = new HashMap<>(); - requestMetrics = new RequestMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType)); - bidTypeMetrics = new HashMap<>(); + adapterMetrics = new HashMap<>(); + adapterMetricsCreator = adapterType -> new AdapterTypeMetrics(metricRegistry, counterType, + createAdapterSuffix(Objects.requireNonNull(accountPrefix)), adapterType); } - AdapterMetrics(MetricRegistry metricRegistry, CounterType counterType, String account, String adapterType) { - super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), - nameCreator(createAccountAdapterPrefix(Objects.requireNonNull(account), - Objects.requireNonNull(adapterType)))); - - requestMetrics = new RequestMetrics(metricRegistry, counterType, - createAccountAdapterPrefix(account, adapterType)); - - // not used for account.adapter metrics - requestTypeMetricsCreator = null; - requestTypeMetrics = null; - bidTypeMetricsCreator = null; - bidTypeMetrics = null; - } - - private static String createAdapterPrefix(String adapterType) { - return String.format("adapter.%s", adapterType); - } - - private static String createAccountAdapterPrefix(String account, String adapterType) { - return String.format("account.%s.%s", account, adapterType); + private static String createAdapterSuffix(String prefix) { + return String.format("%s.adapter", prefix); } private static Function nameCreator(String prefix) { return metricName -> String.format("%s.%s", prefix, metricName.toString()); } - RequestTypeMetrics requestType(MetricName requestType) { - return requestTypeMetrics.computeIfAbsent(requestType, requestTypeMetricsCreator); - } - - RequestMetrics request() { - return requestMetrics; - } - - BidTypeMetrics forBidType(String bidType) { - return bidTypeMetrics.computeIfAbsent(bidType, bidTypeMetricsCreator); + AdapterTypeMetrics forAdapter(String adapterType) { + return adapterMetrics.computeIfAbsent(adapterType, adapterMetricsCreator); } } diff --git a/src/main/java/org/prebid/server/metric/AdapterTypeMetrics.java b/src/main/java/org/prebid/server/metric/AdapterTypeMetrics.java new file mode 100644 index 00000000000..5c4bf1ae1d2 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/AdapterTypeMetrics.java @@ -0,0 +1,82 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * AdapterType metrics support. + */ +class AdapterTypeMetrics extends UpdatableMetrics { + + private final Function requestTypeMetricsCreator; + private final Map requestTypeMetrics; + private final RequestMetrics requestMetrics; + private final Function bidTypeMetricsCreator; + private final Map bidTypeMetrics; + private final ResponseMetrics responseMetrics; + + AdapterTypeMetrics(MetricRegistry metricRegistry, CounterType counterType, String adapterType) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createAdapterPrefix(Objects.requireNonNull(adapterType)))); + + bidTypeMetricsCreator = bidType -> + new BidTypeMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType), bidType); + requestTypeMetricsCreator = requestType -> + new RequestTypeMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType), requestType); + requestTypeMetrics = new HashMap<>(); + requestMetrics = new RequestMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType)); + bidTypeMetrics = new HashMap<>(); + responseMetrics = new ResponseMetrics(metricRegistry, counterType, createAdapterPrefix(adapterType)); + } + + AdapterTypeMetrics(MetricRegistry metricRegistry, + CounterType counterType, + String accountAdapterPrefix, + String adapterType) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createAdapterPrefix(Objects.requireNonNull(accountAdapterPrefix), + Objects.requireNonNull(adapterType)))); + + requestMetrics = new RequestMetrics(metricRegistry, counterType, + createAdapterPrefix(accountAdapterPrefix, adapterType)); + + // not used for account.adapter.adapters metrics + requestTypeMetricsCreator = null; + requestTypeMetrics = null; + bidTypeMetricsCreator = null; + bidTypeMetrics = null; + responseMetrics = null; + } + + private static String createAdapterPrefix(String adapterType) { + return String.format("adapter.%s", adapterType); + } + + private static String createAdapterPrefix(String adapterPrefix, String adapterType) { + return String.format("%s.%s", adapterPrefix, adapterType); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + RequestTypeMetrics requestType(MetricName requestType) { + return requestTypeMetrics.computeIfAbsent(requestType, requestTypeMetricsCreator); + } + + RequestMetrics request() { + return requestMetrics; + } + + BidTypeMetrics forBidType(String bidType) { + return bidTypeMetrics.computeIfAbsent(bidType, bidTypeMetricsCreator); + } + + ResponseMetrics response() { + return responseMetrics; + } +} diff --git a/src/main/java/org/prebid/server/metric/AnalyticsReporterMetrics.java b/src/main/java/org/prebid/server/metric/AnalyticsReporterMetrics.java new file mode 100644 index 00000000000..a939c10be74 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/AnalyticsReporterMetrics.java @@ -0,0 +1,38 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * AnalyticsReporter metrics support. + */ +class AnalyticsReporterMetrics extends UpdatableMetrics { + + private final Function eventTypeMetricsCreator; + private final Map eventTypeMetrics; + + AnalyticsReporterMetrics(MetricRegistry metricRegistry, CounterType counterType, String analyticCode) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createAdapterPrefix(Objects.requireNonNull(analyticCode)))); + + eventTypeMetricsCreator = eventType -> + new EventTypeMetrics(metricRegistry, counterType, createAdapterPrefix(analyticCode), eventType); + eventTypeMetrics = new HashMap<>(); + } + + private static String createAdapterPrefix(String reporterName) { + return String.format("analytics.%s", reporterName); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + EventTypeMetrics forEventType(MetricName eventType) { + return eventTypeMetrics.computeIfAbsent(eventType, eventTypeMetricsCreator); + } +} diff --git a/src/main/java/org/prebid/server/metric/BidderCardinalityMetrics.java b/src/main/java/org/prebid/server/metric/BidderCardinalityMetrics.java new file mode 100644 index 00000000000..dfac861aa6c --- /dev/null +++ b/src/main/java/org/prebid/server/metric/BidderCardinalityMetrics.java @@ -0,0 +1,17 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +public class BidderCardinalityMetrics extends UpdatableMetrics { + + BidderCardinalityMetrics(MetricRegistry metricRegistry, CounterType counterType, Integer cardinality) { + super(metricRegistry, counterType, nameCreator(Objects.requireNonNull(cardinality))); + } + + private static Function nameCreator(Integer cardinality) { + return metricName -> String.format("bidder-cardinality.%d.%s", cardinality, metricName.toString()); + } +} diff --git a/src/main/java/org/prebid/server/metric/CircuitBreakerMetrics.java b/src/main/java/org/prebid/server/metric/CircuitBreakerMetrics.java index 0d71eaf6602..f6a7afcb1d2 100644 --- a/src/main/java/org/prebid/server/metric/CircuitBreakerMetrics.java +++ b/src/main/java/org/prebid/server/metric/CircuitBreakerMetrics.java @@ -2,6 +2,8 @@ import com.codahale.metrics.MetricRegistry; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.function.Function; @@ -10,14 +12,85 @@ */ class CircuitBreakerMetrics extends UpdatableMetrics { - CircuitBreakerMetrics(MetricRegistry metricRegistry, CounterType counterType, String id) { + private static final String SUFFIX = ".count"; + + private final Function namedCircuitBreakerMetricsCreator; + private final Map namedCircuitBreakerMetrics; + + CircuitBreakerMetrics(MetricRegistry metricRegistry, CounterType counterType, MetricName type) { super( Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), - nameCreator(Objects.requireNonNull(id))); + nameCreator(createPrefix(Objects.requireNonNull(type)))); + + namedCircuitBreakerMetricsCreator = + name -> new NamedCircuitBreakerMetrics(metricRegistry, counterType, createPrefix(type), name); + namedCircuitBreakerMetrics = new HashMap<>(); + } + + NamedCircuitBreakerMetrics forName(String name) { + return namedCircuitBreakerMetrics.computeIfAbsent(name, namedCircuitBreakerMetricsCreator); + } + + private static String createPrefix(MetricName type) { + return String.format("circuit-breaker.%s", type.toString()); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s%s", prefix, metricName.toString(), SUFFIX); } - private static Function nameCreator(String id) { - return metricName -> String.format("%s.%s", metricName.toString(), id); + @Override + void incCounter(MetricName metricName) { + throw new UnsupportedOperationException(); + } + + @Override + void incCounter(MetricName metricName, long value) { + throw new UnsupportedOperationException(); + } + + @Override + void updateTimer(MetricName metricName, long millis) { + throw new UnsupportedOperationException(); + } + + @Override + void updateHistogram(MetricName metricName, long value) { + throw new UnsupportedOperationException(); + } + + static class NamedCircuitBreakerMetrics extends UpdatableMetrics { + + NamedCircuitBreakerMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix, String name) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(Objects.requireNonNull(prefix), Objects.requireNonNull(name))); + } + + private static Function nameCreator(String prefix, String name) { + return metricName -> String.format("%s.named.%s.%s%s", prefix, name, metricName.toString(), SUFFIX); + } + + @Override + void incCounter(MetricName metricName) { + throw new UnsupportedOperationException(); + } + + @Override + void incCounter(MetricName metricName, long value) { + throw new UnsupportedOperationException(); + } + + @Override + void updateTimer(MetricName metricName, long millis) { + throw new UnsupportedOperationException(); + } + + @Override + void updateHistogram(MetricName metricName, long value) { + throw new UnsupportedOperationException(); + } } } diff --git a/src/main/java/org/prebid/server/metric/CurrencyRatesMetrics.java b/src/main/java/org/prebid/server/metric/CurrencyRatesMetrics.java new file mode 100644 index 00000000000..b85eba57d85 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/CurrencyRatesMetrics.java @@ -0,0 +1,42 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Circuit breaker metrics support. + */ +class CurrencyRatesMetrics extends UpdatableMetrics { + + private static final String SUFFIX = ".count"; + + CurrencyRatesMetrics(MetricRegistry metricRegistry, CounterType counterType) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), nameCreator()); + } + + private static Function nameCreator() { + return metricName -> String.format("currency-rates.%s%s", metricName.toString(), SUFFIX); + } + + @Override + void incCounter(MetricName metricName) { + throw new UnsupportedOperationException(); + } + + @Override + void incCounter(MetricName metricName, long value) { + throw new UnsupportedOperationException(); + } + + @Override + void updateTimer(MetricName metricName, long millis) { + throw new UnsupportedOperationException(); + } + + @Override + void updateHistogram(MetricName metricName, long value) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/org/prebid/server/metric/EventTypeMetrics.java b/src/main/java/org/prebid/server/metric/EventTypeMetrics.java new file mode 100644 index 00000000000..057d1541b34 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/EventTypeMetrics.java @@ -0,0 +1,19 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.function.Function; + +/** + * Metrics for reporting on certain event type + */ +public class EventTypeMetrics extends UpdatableMetrics { + + EventTypeMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix, MetricName eventType) { + super(metricRegistry, counterType, nameCreator(prefix, eventType)); + } + + private static Function nameCreator(String prefix, MetricName eventType) { + return metricName -> String.format("%s.%s.%s", prefix, eventType, metricName.toString()); + } +} diff --git a/src/main/java/org/prebid/server/metric/HookImplMetrics.java b/src/main/java/org/prebid/server/metric/HookImplMetrics.java new file mode 100644 index 00000000000..b5e488e9222 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/HookImplMetrics.java @@ -0,0 +1,32 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +class HookImplMetrics extends UpdatableMetrics { + + private final HookSuccessMetrics successMetrics; + + HookImplMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix, String hookImplCode) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix), Objects.requireNonNull(hookImplCode)))); + + successMetrics = new HookSuccessMetrics(metricRegistry, counterType, createPrefix(prefix, hookImplCode)); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix, String stage) { + return String.format("%s.hook.%s", prefix, stage); + } + + HookSuccessMetrics success() { + return successMetrics; + } +} diff --git a/src/main/java/org/prebid/server/metric/HookSuccessMetrics.java b/src/main/java/org/prebid/server/metric/HookSuccessMetrics.java new file mode 100644 index 00000000000..64daddd9069 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/HookSuccessMetrics.java @@ -0,0 +1,24 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +class HookSuccessMetrics extends UpdatableMetrics { + + HookSuccessMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix)))); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix) { + return String.format("%s.success", prefix); + } +} diff --git a/src/main/java/org/prebid/server/metric/HooksMetrics.java b/src/main/java/org/prebid/server/metric/HooksMetrics.java new file mode 100644 index 00000000000..6703f11dbd0 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/HooksMetrics.java @@ -0,0 +1,55 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +class HooksMetrics extends UpdatableMetrics { + + // not thread-safe maps are intentionally used here because it's harmless in this particular case - eventually + // this all boils down to metrics lookup by underlying metric registry and that operation is guaranteed to be + // thread-safe + private final Function moduleMetricsCreator; + private final Map moduleMetrics; + + HooksMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix)))); + + moduleMetricsCreator = moduleCode -> + new ModuleMetrics(metricRegistry, counterType, createPrefix(prefix), moduleCode); + moduleMetrics = new HashMap<>(); + } + + HooksMetrics(MetricRegistry metricRegistry, CounterType counterType) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(createPrefix())); + + moduleMetricsCreator = moduleCode -> + new ModuleMetrics(metricRegistry, counterType, createPrefix(), moduleCode); + moduleMetrics = new HashMap<>(); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix) { + return String.format("%s.%s", prefix, createPrefix()); + } + + private static String createPrefix() { + return "modules"; + } + + ModuleMetrics module(String moduleCode) { + return moduleMetrics.computeIfAbsent(moduleCode, moduleMetricsCreator); + } +} diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index a6280b99678..16bc385a246 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -5,28 +5,26 @@ public enum MetricName { // connection connection_accept_errors, + // circuit breaker + db, + geo, + http, + opened, + existing, + // database - db_circuitbreaker_opened, - db_circuitbreaker_closed, db_query_time, - // http client - httpclient_circuitbreaker_opened, - httpclient_circuitbreaker_closed, - // geo location geolocation_requests, + geolocation_request_time, geolocation_successful, geolocation_fail, - geolocation_circuitbreaker_opened, - geolocation_circuitbreaker_closed, // auction requests, app_requests, no_cookie_requests, - safari_requests, - safari_no_cookie_requests, request_time, prices, imps_requested, @@ -43,10 +41,21 @@ public enum MetricName { openrtb2app("openrtb2-app"), amp, video, - legacy, + cookiesync, + setuid, + + // event types + event_auction("auction"), + event_amp("amp"), + event_video("video"), + event_notification("event"), + event_cookie_sync("cookie_sync"), + event_setuid("setuid"), + event_unknown("unknown"), // request and adapter statuses ok, + failed, nobid, gotbids, badinput, @@ -59,6 +68,9 @@ public enum MetricName { err, networkerr, + // bids validation + warn, + // cookie sync cookie_sync_requests, opt_outs, @@ -97,8 +109,57 @@ public enum MetricName { // cache creative_size, - //account.*.requests. - rejected; + // account.*.requests. + rejected, + + // currency rates + stale, + + // settings cache + stored_request("stored-request"), + amp_stored_request("amp-stored-request"), + account, + initialize, + update, + hit, + miss, + + // hooks + call, + success, + noop, + reject, + unknown, + failure, + execution_error("execution-error"), + duration, + + // win notifications + win_notifications, + win_requests, + win_request_preparation_failed, + win_request_time, + win_request_failed, + win_request_successful, + + // user details + user_details_requests, + user_details_request_preparation_failed, + user_details_request_time, + user_details_request_failed, + user_details_request_successful, + + // pg + planner_lineitems_received, + planner_requests, + planner_request_failed, + planner_request_successful, + planner_request_time, + delivery_requests, + delivery_request_failed, + delivery_request_successful, + delivery_request_time; + private final String name; diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 506068ad606..d26ff843a3c 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -2,7 +2,9 @@ import com.codahale.metrics.MetricRegistry; import com.iab.openrtb.request.Imp; -import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.metric.model.AccountMetricsVerbosityLevel; import java.util.ArrayList; @@ -12,7 +14,9 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.BooleanSupplier; import java.util.function.Function; +import java.util.function.LongSupplier; import java.util.stream.Collectors; /** @@ -20,64 +24,96 @@ */ public class Metrics extends UpdatableMetrics { - private static final String METRICS_UNKNOWN_BIDDER = "UNKNOWN"; + private static final String ALL_REQUEST_BIDDERS = "all"; private final AccountMetricsVerbosity accountMetricsVerbosity; - private final BidderCatalog bidderCatalog; private final Function requestMetricsCreator; private final Function accountMetricsCreator; - private final Function adapterMetricsCreator; - private final Function circuitBreakerMetricsCreator; + private final Function adapterMetricsCreator; + private final Function analyticMetricsCreator; + private final Function bidderCardinalityMetricsCreator; + private final Function circuitBreakerMetricsCreator; + private final Function settingsCacheMetricsCreator; // not thread-safe maps are intentionally used here because it's harmless in this particular case - eventually // this all boils down to metrics lookup by underlying metric registry and that operation is guaranteed to be // thread-safe private final Map requestMetrics; private final Map accountMetrics; - private final Map adapterMetrics; + private final Map adapterMetrics; + private final Map analyticMetrics; + private final Map bidderCardinailtyMetrics; private final UserSyncMetrics userSyncMetrics; private final CookieSyncMetrics cookieSyncMetrics; private final PrivacyMetrics privacyMetrics; - private final Map circuitBreakerMetrics; + private final Map circuitBreakerMetrics; private final CacheMetrics cacheMetrics; - - public Metrics(MetricRegistry metricRegistry, CounterType counterType, AccountMetricsVerbosity - accountMetricsVerbosity, BidderCatalog bidderCatalog) { + private final TimeoutNotificationMetrics timeoutNotificationMetrics; + private final CurrencyRatesMetrics currencyRatesMetrics; + private final Map settingsCacheMetrics; + private final HooksMetrics hooksMetrics; + private final PgMetrics pgMetrics; + + public Metrics(MetricRegistry metricRegistry, CounterType counterType, + AccountMetricsVerbosity accountMetricsVerbosity) { super(metricRegistry, counterType, MetricName::toString); this.accountMetricsVerbosity = Objects.requireNonNull(accountMetricsVerbosity); - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); requestMetricsCreator = requestType -> new RequestStatusMetrics(metricRegistry, counterType, requestType); accountMetricsCreator = account -> new AccountMetrics(metricRegistry, counterType, account); - adapterMetricsCreator = adapterType -> new AdapterMetrics(metricRegistry, counterType, adapterType); - circuitBreakerMetricsCreator = id -> new CircuitBreakerMetrics(metricRegistry, counterType, id); + adapterMetricsCreator = adapterType -> new AdapterTypeMetrics(metricRegistry, counterType, adapterType); + bidderCardinalityMetricsCreator = cardinality -> new BidderCardinalityMetrics( + metricRegistry, counterType, cardinality); + analyticMetricsCreator = analyticCode -> new AnalyticsReporterMetrics( + metricRegistry, counterType, analyticCode); + circuitBreakerMetricsCreator = type -> new CircuitBreakerMetrics(metricRegistry, counterType, type); + settingsCacheMetricsCreator = type -> new SettingsCacheMetrics(metricRegistry, counterType, type); requestMetrics = new EnumMap<>(MetricName.class); accountMetrics = new HashMap<>(); adapterMetrics = new HashMap<>(); + analyticMetrics = new HashMap<>(); + bidderCardinailtyMetrics = new HashMap<>(); userSyncMetrics = new UserSyncMetrics(metricRegistry, counterType); cookieSyncMetrics = new CookieSyncMetrics(metricRegistry, counterType); privacyMetrics = new PrivacyMetrics(metricRegistry, counterType); circuitBreakerMetrics = new HashMap<>(); cacheMetrics = new CacheMetrics(metricRegistry, counterType); + timeoutNotificationMetrics = new TimeoutNotificationMetrics(metricRegistry, counterType); + currencyRatesMetrics = new CurrencyRatesMetrics(metricRegistry, counterType); + settingsCacheMetrics = new HashMap<>(); + hooksMetrics = new HooksMetrics(metricRegistry, counterType); + pgMetrics = new PgMetrics(metricRegistry, counterType); } RequestStatusMetrics forRequestType(MetricName requestType) { return requestMetrics.computeIfAbsent(requestType, requestMetricsCreator); } + BidderCardinalityMetrics forBidderCardinality(int cardinality) { + return bidderCardinailtyMetrics.computeIfAbsent(cardinality, bidderCardinalityMetricsCreator); + } + AccountMetrics forAccount(String account) { return accountMetrics.computeIfAbsent(account, accountMetricsCreator); } - AdapterMetrics forAdapter(String adapterType) { + AdapterTypeMetrics forAdapter(String adapterType) { return adapterMetrics.computeIfAbsent(adapterType, adapterMetricsCreator); } + AnalyticsReporterMetrics forAnalyticReporter(String analyticCode) { + return analyticMetrics.computeIfAbsent(analyticCode, analyticMetricsCreator); + } + UserSyncMetrics userSync() { return userSyncMetrics; } + PgMetrics pgMetrics() { + return pgMetrics; + } + CookieSyncMetrics cookieSync() { return cookieSyncMetrics; } @@ -86,34 +122,46 @@ PrivacyMetrics privacy() { return privacyMetrics; } - CircuitBreakerMetrics forCircuitBreaker(String id) { - return circuitBreakerMetrics.computeIfAbsent(id, circuitBreakerMetricsCreator); + CircuitBreakerMetrics forCircuitBreakerType(MetricName type) { + return circuitBreakerMetrics.computeIfAbsent(type, circuitBreakerMetricsCreator); } CacheMetrics cache() { return cacheMetrics; } - public void updateSafariRequestsMetric(boolean isSafari) { - if (isSafari) { - incCounter(MetricName.safari_requests); - } + CurrencyRatesMetrics currencyRates() { + return currencyRatesMetrics; } - public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean liveUidsPresent, boolean isSafari, - int numImps) { + SettingsCacheMetrics forSettingsCacheType(MetricName type) { + return settingsCacheMetrics.computeIfAbsent(type, settingsCacheMetricsCreator); + } + + HooksMetrics hooks() { + return hooksMetrics; + } + + public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean liveUidsPresent, int numImps) { if (isApp) { incCounter(MetricName.app_requests); } else if (!liveUidsPresent) { incCounter(MetricName.no_cookie_requests); - if (isSafari) { - incCounter(MetricName.safari_no_cookie_requests); - } } incCounter(MetricName.imps_requested, numImps); } - public void updateImpTypesMetrics(Map countPerMediaType) { + public void updateImpTypesMetrics(List imps) { + + final Map mediaTypeToCount = imps.stream() + .map(Metrics::getPresentMediaTypes) + .flatMap(Collection::stream) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + + updateImpTypesMetrics(mediaTypeToCount); + } + + void updateImpTypesMetrics(Map countPerMediaType) { for (Map.Entry mediaTypeCount : countPerMediaType.entrySet()) { switch (mediaTypeCount.getKey()) { case "banner": @@ -135,16 +183,6 @@ public void updateImpTypesMetrics(Map countPerMediaType) { } } - public void updateImpTypesMetrics(List imps) { - - final Map mediaTypeToCount = imps.stream() - .map(Metrics::getPresentMediaTypes) - .flatMap(Collection::stream) - .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); - - updateImpTypesMetrics(mediaTypeToCount); - } - private static List getPresentMediaTypes(Imp imp) { final List impMediaTypes = new ArrayList<>(); @@ -164,14 +202,18 @@ private static List getPresentMediaTypes(Imp imp) { return impMediaTypes; } - public void updateRequestTimeMetric(long millis) { - updateTimer(MetricName.request_time, millis); + public void updateRequestTimeMetric(MetricName requestType, long millis) { + updateTimer(requestType, millis); } public void updateRequestTypeMetric(MetricName requestType, MetricName requestStatus) { forRequestType(requestType).incCounter(requestStatus); } + public void updateRequestBidderCardinalityMetric(int bidderCardinality) { + forBidderCardinality(bidderCardinality).incCounter(MetricName.requests); + } + public void updateAccountRequestMetrics(String accountId, MetricName requestType) { final AccountMetricsVerbosityLevel verbosityLevel = accountMetricsVerbosity.forAccount(accountId); if (verbosityLevel.isAtLeast(AccountMetricsVerbosityLevel.basic)) { @@ -190,59 +232,71 @@ public void updateAccountRequestRejectedMetrics(String accountId) { } public void updateAdapterRequestTypeAndNoCookieMetrics(String bidder, MetricName requestType, boolean noCookie) { - final AdapterMetrics adapterMetrics = forAdapter(resolveMetricsBidderName(bidder)); + final AdapterTypeMetrics adapterTypeMetrics = forAdapter(bidder); - adapterMetrics.requestType(requestType).incCounter(MetricName.requests); + adapterTypeMetrics.requestType(requestType).incCounter(MetricName.requests); if (noCookie) { - adapterMetrics.incCounter(MetricName.no_cookie_requests); + adapterTypeMetrics.incCounter(MetricName.no_cookie_requests); } } public void updateAdapterResponseTime(String bidder, String accountId, int responseTime) { - final String metricsBidderName = resolveMetricsBidderName(bidder); - final AdapterMetrics adapterMetrics = forAdapter(metricsBidderName); - adapterMetrics.updateTimer(MetricName.request_time, responseTime); + final AdapterTypeMetrics adapterTypeMetrics = forAdapter(bidder); + adapterTypeMetrics.updateTimer(MetricName.request_time, responseTime); if (accountMetricsVerbosity.forAccount(accountId).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { - final AdapterMetrics accountAdapterMetrics = forAccount(accountId).forAdapter(metricsBidderName); + final AdapterTypeMetrics accountAdapterMetrics = + forAccount(accountId).adapter().forAdapter(bidder); accountAdapterMetrics.updateTimer(MetricName.request_time, responseTime); } } public void updateAdapterRequestNobidMetrics(String bidder, String accountId) { - final String metricsBidderName = resolveMetricsBidderName(bidder); - forAdapter(metricsBidderName).request().incCounter(MetricName.nobid); + forAdapter(bidder).request().incCounter(MetricName.nobid); if (accountMetricsVerbosity.forAccount(accountId).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { - forAccount(accountId).forAdapter(metricsBidderName).request().incCounter(MetricName.nobid); + forAccount(accountId).adapter().forAdapter(bidder).request().incCounter(MetricName.nobid); } } public void updateAdapterRequestGotbidsMetrics(String bidder, String accountId) { - final String metricsBidderName = resolveMetricsBidderName(bidder); - forAdapter(metricsBidderName).request().incCounter(MetricName.gotbids); + forAdapter(bidder).request().incCounter(MetricName.gotbids); if (accountMetricsVerbosity.forAccount(accountId).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { - forAccount(accountId).forAdapter(metricsBidderName).request().incCounter(MetricName.gotbids); + forAccount(accountId).adapter().forAdapter(bidder).request().incCounter(MetricName.gotbids); } } public void updateAdapterBidMetrics(String bidder, String accountId, long cpm, boolean isAdm, String bidType) { - final String metricsBidderName = resolveMetricsBidderName(bidder); - final AdapterMetrics adapterMetrics = forAdapter(metricsBidderName); - adapterMetrics.updateHistogram(MetricName.prices, cpm); - adapterMetrics.incCounter(MetricName.bids_received); - adapterMetrics.forBidType(bidType) + final AdapterTypeMetrics adapterTypeMetrics = forAdapter(bidder); + adapterTypeMetrics.updateHistogram(MetricName.prices, cpm); + adapterTypeMetrics.incCounter(MetricName.bids_received); + adapterTypeMetrics.forBidType(bidType) .incCounter(isAdm ? MetricName.adm_bids_received : MetricName.nurl_bids_received); if (accountMetricsVerbosity.forAccount(accountId).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { - final AdapterMetrics accountAdapterMetrics = forAccount(accountId).forAdapter(metricsBidderName); + final AdapterTypeMetrics accountAdapterMetrics = + forAccount(accountId).adapter().forAdapter(bidder); accountAdapterMetrics.updateHistogram(MetricName.prices, cpm); accountAdapterMetrics.incCounter(MetricName.bids_received); } } public void updateAdapterRequestErrorMetric(String bidder, MetricName errorMetric) { - forAdapter(resolveMetricsBidderName(bidder)).request().incCounter(errorMetric); + forAdapter(bidder).request().incCounter(errorMetric); + } + + public void updateAnalyticEventMetric(String analyticCode, MetricName eventType, MetricName result) { + forAnalyticReporter(analyticCode).forEventType(eventType).incCounter(result); + } + + public void updateSizeValidationMetrics(String bidder, String accountId, MetricName type) { + forAdapter(bidder).response().validation().size().incCounter(type); + forAccount(accountId).response().validation().size().incCounter(type); + } + + public void updateSecureValidationMetrics(String bidder, String accountId, MetricName type) { + forAdapter(bidder).response().validation().secure().incCounter(type); + forAccount(accountId).response().validation().secure().incCounter(type); } public void updateUserSyncOptoutMetric() { @@ -261,6 +315,14 @@ public void updateUserSyncTcfBlockedMetric(String bidder) { userSync().forBidder(bidder).tcf().incCounter(MetricName.blocked); } + public void updateUserSyncTcfInvalidMetric(String bidder) { + userSync().forBidder(bidder).tcf().incCounter(MetricName.invalid); + } + + public void updateUserSyncTcfInvalidMetric() { + updateUserSyncTcfInvalidMetric(ALL_REQUEST_BIDDERS); + } + public void updateCookieSyncRequestMetric() { incCounter(MetricName.cookie_sync_requests); } @@ -274,30 +336,30 @@ public void updateCookieSyncMatchesMetric(String bidder) { } public void updateCookieSyncTcfBlockedMetric(String bidder) { - cookieSync().forBidder(resolveMetricsBidderName(bidder)).tcf().incCounter(MetricName.blocked); + cookieSync().forBidder(bidder).tcf().incCounter(MetricName.blocked); } public void updateAuctionTcfMetrics(String bidder, MetricName requestType, - boolean useridRemoved, + boolean userIdRemoved, boolean geoMasked, - boolean requestBlocked, - boolean analyticsBlocked) { + boolean analyticsBlocked, + boolean requestBlocked) { - final TcfMetrics tcf = forAdapter(resolveMetricsBidderName(bidder)).requestType(requestType).tcf(); + final TcfMetrics tcf = forAdapter(bidder).requestType(requestType).tcf(); - if (useridRemoved) { + if (userIdRemoved) { tcf.incCounter(MetricName.userid_removed); } if (geoMasked) { tcf.incCounter(MetricName.geo_masked); } - if (requestBlocked) { - tcf.incCounter(MetricName.request_blocked); - } if (analyticsBlocked) { tcf.incCounter(MetricName.analytics_blocked); } + if (requestBlocked) { + tcf.incCounter(MetricName.request_blocked); + } } public void updatePrivacyCoppaMetric() { @@ -325,8 +387,13 @@ public void updatePrivacyTcfInvalidMetric() { privacy().tcf().incCounter(MetricName.invalid); } + public void updatePrivacyTcfRequestsMetric(int version) { + final UpdatableMetrics versionMetrics = privacy().tcf().fromVersion(version); + versionMetrics.incCounter(MetricName.requests); + } + public void updatePrivacyTcfGeoMetric(int version, Boolean inEea) { - final UpdatableMetrics versionMetrics = version == 2 ? privacy().tcf().v2() : privacy().tcf().v1(); + final UpdatableMetrics versionMetrics = privacy().tcf().fromVersion(version); final MetricName metricName = inEea == null ? MetricName.unknown_geo @@ -353,8 +420,7 @@ public void updatePrivacyTcfVendorListFallbackMetric(int version) { private void updatePrivacyTcfVendorListMetric(int version, MetricName metricName) { final TcfMetrics tcfMetrics = privacy().tcf(); - final TcfMetrics.TcfVersionMetrics tcfVersionMetrics = version == 2 ? tcfMetrics.v2() : tcfMetrics.v1(); - tcfVersionMetrics.vendorList().incCounter(metricName); + tcfMetrics.fromVersion(version).vendorList().incCounter(metricName); } public void updateConnectionAcceptErrors() { @@ -365,22 +431,77 @@ public void updateDatabaseQueryTimeMetric(long millis) { updateTimer(MetricName.db_query_time, millis); } - public void updateDatabaseCircuitBreakerMetric(boolean opened) { - if (opened) { - incCounter(MetricName.db_circuitbreaker_opened); + public void createDatabaseCircuitBreakerGauge(BooleanSupplier stateSupplier) { + forCircuitBreakerType(MetricName.db) + .createGauge(MetricName.opened, () -> stateSupplier.getAsBoolean() ? 1 : 0); + } + + public void createHttpClientCircuitBreakerGauge(String name, BooleanSupplier stateSupplier) { + forCircuitBreakerType(MetricName.http) + .forName(name) + .createGauge(MetricName.opened, () -> stateSupplier.getAsBoolean() ? 1 : 0); + } + + public void removeHttpClientCircuitBreakerGauge(String name) { + forCircuitBreakerType(MetricName.http).forName(name).removeMetric(MetricName.opened); + } + + public void createHttpClientCircuitBreakerNumberGauge(LongSupplier numberSupplier) { + forCircuitBreakerType(MetricName.http).createGauge(MetricName.existing, numberSupplier); + } + + public void updatePlannerRequestMetric(boolean successful) { + pgMetrics().incCounter(MetricName.planner_requests); + if (successful) { + pgMetrics().incCounter(MetricName.planner_request_successful); + } else { + pgMetrics().incCounter(MetricName.planner_request_failed); + } + } + + public void updateDeliveryRequestMetric(boolean successful) { + pgMetrics().incCounter(MetricName.delivery_requests); + if (successful) { + pgMetrics().incCounter(MetricName.delivery_request_successful); + } else { + pgMetrics().incCounter(MetricName.delivery_request_failed); + } + } + + public void updateWinEventRequestMetric(boolean successful) { + incCounter(MetricName.win_requests); + if (successful) { + incCounter(MetricName.win_request_successful); } else { - incCounter(MetricName.db_circuitbreaker_closed); + incCounter(MetricName.win_request_failed); } } - public void updateHttpClientCircuitBreakerMetric(String id, boolean opened) { - if (opened) { - forCircuitBreaker(id).incCounter(MetricName.httpclient_circuitbreaker_opened); + public void updateUserDetailsRequestMetric(boolean successful) { + incCounter(MetricName.user_details_requests); + if (successful) { + incCounter(MetricName.user_details_request_successful); } else { - forCircuitBreaker(id).incCounter(MetricName.httpclient_circuitbreaker_closed); + incCounter(MetricName.user_details_request_failed); } } + public void updateWinRequestTime(long millis) { + updateTimer(MetricName.win_request_time, millis); + } + + public void updateLineItemsNumberMetric(long count) { + pgMetrics().incCounter(MetricName.planner_lineitems_received, count); + } + + public void updatePlannerRequestTime(long millis) { + pgMetrics().updateTimer(MetricName.planner_request_time, millis); + } + + public void updateDeliveryRequestTime(long millis) { + pgMetrics().updateTimer(MetricName.delivery_request_time, millis); + } + public void updateGeoLocationMetric(boolean successful) { incCounter(MetricName.geolocation_requests); if (successful) { @@ -390,12 +511,9 @@ public void updateGeoLocationMetric(boolean successful) { } } - public void updateGeoLocationCircuitBreakerMetric(boolean opened) { - if (opened) { - incCounter(MetricName.geolocation_circuitbreaker_opened); - } else { - incCounter(MetricName.geolocation_circuitbreaker_closed); - } + public void createGeoLocationCircuitBreakerGauge(BooleanSupplier stateSupplier) { + forCircuitBreakerType(MetricName.geo) + .createGauge(MetricName.opened, () -> stateSupplier.getAsBoolean() ? 1 : 0); } public void updateStoredRequestMetric(boolean found) { @@ -429,7 +547,109 @@ public void updateCacheCreativeSize(String accountId, int creativeSize) { forAccount(accountId).cache().updateHistogram(MetricName.creative_size, creativeSize); } - private String resolveMetricsBidderName(String bidder) { - return bidderCatalog.isValidName(bidder) ? bidder : METRICS_UNKNOWN_BIDDER; + public void updateTimeoutNotificationMetric(boolean success) { + if (success) { + timeoutNotificationMetrics.incCounter(MetricName.ok); + } else { + timeoutNotificationMetrics.incCounter(MetricName.failed); + } + } + + public void createCurrencyRatesGauge(BooleanSupplier stateSupplier) { + currencyRates().createGauge(MetricName.stale, () -> stateSupplier.getAsBoolean() ? 1 : 0); + } + + public void updateSettingsCacheRefreshTime(MetricName cacheType, MetricName refreshType, long timeElapsed) { + forSettingsCacheType(cacheType).forRefreshType(refreshType).updateTimer(MetricName.db_query_time, timeElapsed); + } + + public void updateSettingsCacheRefreshErrorMetric(MetricName cacheType, MetricName refreshType) { + forSettingsCacheType(cacheType).forRefreshType(refreshType).incCounter(MetricName.err); + } + + public void updateSettingsCacheEventMetric(MetricName cacheType, MetricName event) { + forSettingsCacheType(cacheType).incCounter(event); + } + + public void updateHooksMetrics( + String moduleCode, + Stage stage, + String hookImplCode, + ExecutionStatus status, + Long executionTime, + ExecutionAction action) { + + final HookImplMetrics hookImplMetrics = hooks().module(moduleCode).stage(stage).hookImpl(hookImplCode); + + hookImplMetrics.incCounter(MetricName.call); + if (status == ExecutionStatus.success) { + hookImplMetrics.success().incCounter(HookMetricMapper.fromAction(action)); + } else { + hookImplMetrics.incCounter(HookMetricMapper.fromStatus(status)); + } + hookImplMetrics.updateTimer(MetricName.duration, executionTime); + } + + public void updateAccountHooksMetrics( + String accountId, + String moduleCode, + ExecutionStatus status, + ExecutionAction action) { + + if (accountMetricsVerbosity.forAccount(accountId).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { + final ModuleMetrics accountModuleMetrics = forAccount(accountId).hooks().module(moduleCode); + + accountModuleMetrics.incCounter(MetricName.call); + if (status == ExecutionStatus.success) { + accountModuleMetrics.success().incCounter(HookMetricMapper.fromAction(action)); + } else { + accountModuleMetrics.incCounter(MetricName.failure); + } + } + } + + public void updateAccountModuleDurationMetric(String accountId, String moduleCode, Long executionTime) { + if (accountMetricsVerbosity.forAccount(accountId).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { + forAccount(accountId).hooks().module(moduleCode).updateTimer(MetricName.duration, executionTime); + } + } + + private static class HookMetricMapper { + + private static final EnumMap STATUS_TO_METRIC = + new EnumMap<>(ExecutionStatus.class); + private static final EnumMap ACTION_TO_METRIC = + new EnumMap<>(ExecutionAction.class); + + static { + STATUS_TO_METRIC.put(ExecutionStatus.failure, MetricName.failure); + STATUS_TO_METRIC.put(ExecutionStatus.timeout, MetricName.timeout); + STATUS_TO_METRIC.put(ExecutionStatus.invocation_failure, MetricName.execution_error); + STATUS_TO_METRIC.put(ExecutionStatus.execution_failure, MetricName.execution_error); + + ACTION_TO_METRIC.put(ExecutionAction.no_action, MetricName.noop); + ACTION_TO_METRIC.put(ExecutionAction.update, MetricName.update); + ACTION_TO_METRIC.put(ExecutionAction.reject, MetricName.reject); + } + + static MetricName fromStatus(ExecutionStatus status) { + return STATUS_TO_METRIC.getOrDefault(status, MetricName.unknown); + } + + static MetricName fromAction(ExecutionAction action) { + return ACTION_TO_METRIC.getOrDefault(action, MetricName.unknown); + } + } + + public void updateWinNotificationMetric() { + incCounter(MetricName.win_notifications); + } + + public void updateWinRequestPreparationFailed() { + incCounter(MetricName.win_request_preparation_failed); + } + + public void updateUserDetailsRequestPreparationFailed() { + incCounter(MetricName.user_details_request_preparation_failed); } } diff --git a/src/main/java/org/prebid/server/metric/ModuleMetrics.java b/src/main/java/org/prebid/server/metric/ModuleMetrics.java new file mode 100644 index 00000000000..8e9ccc8b594 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/ModuleMetrics.java @@ -0,0 +1,49 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; +import org.prebid.server.hooks.execution.model.Stage; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +class ModuleMetrics extends UpdatableMetrics { + + // not thread-safe maps are intentionally used here because it's harmless in this particular case - eventually + // this all boils down to metrics lookup by underlying metric registry and that operation is guaranteed to be + // thread-safe + private final Function stageMetricsCreator; + private final Map stageMetrics; + + private final HookSuccessMetrics successMetrics; + + ModuleMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix, String moduleCode) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix), Objects.requireNonNull(moduleCode)))); + + stageMetricsCreator = stage -> + new StageMetrics(metricRegistry, counterType, createPrefix(prefix, moduleCode), stage); + stageMetrics = new HashMap<>(); + + successMetrics = new HookSuccessMetrics(metricRegistry, counterType, createPrefix(prefix, moduleCode)); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix, String moduleCode) { + return String.format("%s.module.%s", prefix, moduleCode); + } + + StageMetrics stage(Stage stage) { + return stageMetrics.computeIfAbsent(stage, stageMetricsCreator); + } + + HookSuccessMetrics success() { + return successMetrics; + } +} diff --git a/src/main/java/org/prebid/server/metric/PgMetrics.java b/src/main/java/org/prebid/server/metric/PgMetrics.java new file mode 100644 index 00000000000..2b6ef3fad9c --- /dev/null +++ b/src/main/java/org/prebid/server/metric/PgMetrics.java @@ -0,0 +1,10 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +public class PgMetrics extends UpdatableMetrics { + + PgMetrics(MetricRegistry metricRegistry, CounterType counterType) { + super(metricRegistry, counterType, metricName -> String.format("pg.%s", metricName.toString())); + } +} diff --git a/src/main/java/org/prebid/server/metric/ResponseMetrics.java b/src/main/java/org/prebid/server/metric/ResponseMetrics.java new file mode 100644 index 00000000000..ba7e93fae81 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/ResponseMetrics.java @@ -0,0 +1,33 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Request metrics support. + */ +class ResponseMetrics extends UpdatableMetrics { + + private final ValidationMetrics validationMetrics; + + ResponseMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix)))); + + validationMetrics = new ValidationMetrics(metricRegistry, counterType, createPrefix(prefix)); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix) { + return String.format("%s.response", prefix); + } + + ValidationMetrics validation() { + return validationMetrics; + } +} diff --git a/src/main/java/org/prebid/server/metric/SettingsCacheMetrics.java b/src/main/java/org/prebid/server/metric/SettingsCacheMetrics.java new file mode 100644 index 00000000000..a78bf2223d0 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/SettingsCacheMetrics.java @@ -0,0 +1,58 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * Settings cache metrics support. + */ +class SettingsCacheMetrics extends UpdatableMetrics { + + private final Function refreshSettingsCacheMetricsCreator; + private final Map refreshSettingsCacheMetrics; + + SettingsCacheMetrics(MetricRegistry metricRegistry, CounterType counterType, MetricName type) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(type)))); + + refreshSettingsCacheMetricsCreator = refreshType -> + new RefreshSettingsCacheMetrics(metricRegistry, counterType, createPrefix(type), refreshType); + refreshSettingsCacheMetrics = new HashMap<>(); + } + + RefreshSettingsCacheMetrics forRefreshType(MetricName refreshType) { + return refreshSettingsCacheMetrics.computeIfAbsent(refreshType, refreshSettingsCacheMetricsCreator); + } + + private static String createPrefix(MetricName type) { + return String.format("settings.cache.%s", type.toString()); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + static class RefreshSettingsCacheMetrics extends UpdatableMetrics { + + RefreshSettingsCacheMetrics(MetricRegistry metricRegistry, + CounterType counterType, + String prefix, + MetricName type) { + + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix), Objects.requireNonNull(type)))); + } + + private static String createPrefix(String prefix, MetricName type) { + return String.format("%s.refresh.%s", prefix, type.toString()); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + } +} diff --git a/src/main/java/org/prebid/server/metric/SpecificValidationMetrics.java b/src/main/java/org/prebid/server/metric/SpecificValidationMetrics.java new file mode 100644 index 00000000000..4a1bc289860 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/SpecificValidationMetrics.java @@ -0,0 +1,27 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Request metrics support. + */ +class SpecificValidationMetrics extends UpdatableMetrics { + + SpecificValidationMetrics( + MetricRegistry metricRegistry, CounterType counterType, String prefix, String validation) { + + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix), Objects.requireNonNull(validation)))); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix, String validation) { + return String.format("%s.%s", prefix, validation); + } +} diff --git a/src/main/java/org/prebid/server/metric/StageMetrics.java b/src/main/java/org/prebid/server/metric/StageMetrics.java new file mode 100644 index 00000000000..d5bbdcb50ca --- /dev/null +++ b/src/main/java/org/prebid/server/metric/StageMetrics.java @@ -0,0 +1,56 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; +import org.prebid.server.hooks.execution.model.Stage; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +class StageMetrics extends UpdatableMetrics { + + private static final EnumMap STAGE_TO_METRIC = new EnumMap<>(Stage.class); + + static { + STAGE_TO_METRIC.put(Stage.entrypoint, "entrypoint"); + STAGE_TO_METRIC.put(Stage.raw_auction_request, "rawauction"); + STAGE_TO_METRIC.put(Stage.processed_auction_request, "procauction"); + STAGE_TO_METRIC.put(Stage.bidder_request, "bidrequest"); + STAGE_TO_METRIC.put(Stage.raw_bidder_response, "rawbidresponse"); + STAGE_TO_METRIC.put(Stage.processed_bidder_response, "procbidresponse"); + STAGE_TO_METRIC.put(Stage.auction_response, "auctionresponse"); + } + + private static final String UNKNOWN_STAGE = "unknown"; + + // not thread-safe maps are intentionally used here because it's harmless in this particular case - eventually + // this all boils down to metrics lookup by underlying metric registry and that operation is guaranteed to be + // thread-safe + private final Function hookImplMetricsCreator; + private final Map hookImplMetrics; + + StageMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix, Stage stage) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix), Objects.requireNonNull(stage)))); + + hookImplMetricsCreator = hookImplCode -> + new HookImplMetrics(metricRegistry, counterType, createPrefix(prefix, stage), hookImplCode); + hookImplMetrics = new HashMap<>(); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix, Stage stage) { + return String.format("%s.stage.%s", prefix, STAGE_TO_METRIC.getOrDefault(stage, UNKNOWN_STAGE)); + } + + HookImplMetrics hookImpl(String hookImpl) { + return hookImplMetrics.computeIfAbsent(hookImpl, hookImplMetricsCreator); + } +} diff --git a/src/main/java/org/prebid/server/metric/TcfMetrics.java b/src/main/java/org/prebid/server/metric/TcfMetrics.java index 1bd01b43bba..b29416f061e 100644 --- a/src/main/java/org/prebid/server/metric/TcfMetrics.java +++ b/src/main/java/org/prebid/server/metric/TcfMetrics.java @@ -1,6 +1,7 @@ package org.prebid.server.metric; import com.codahale.metrics.MetricRegistry; +import org.prebid.server.exception.PreBidException; import java.util.Objects; import java.util.function.Function; @@ -10,6 +11,9 @@ */ class TcfMetrics extends UpdatableMetrics { + private static final int TCF_V1_VERSION = 1; + private static final int TCF_V2_VERSION = 2; + private final TcfVersionMetrics tcfVersion1Metrics; private final TcfVersionMetrics tcfVersion2Metrics; @@ -23,12 +27,15 @@ class TcfMetrics extends UpdatableMetrics { tcfVersion2Metrics = new TcfVersionMetrics(metricRegistry, counterType, createTcfPrefix(prefix), "v2"); } - TcfVersionMetrics v1() { - return tcfVersion1Metrics; - } - - TcfVersionMetrics v2() { - return tcfVersion2Metrics; + TcfVersionMetrics fromVersion(int version) { + switch (version) { + case TCF_V1_VERSION: + return tcfVersion1Metrics; + case TCF_V2_VERSION: + return tcfVersion2Metrics; + default: + throw new PreBidException(String.format("Unknown tcf version %s", version)); + } } private static String createTcfPrefix(String prefix) { diff --git a/src/main/java/org/prebid/server/metric/TimeoutNotificationMetrics.java b/src/main/java/org/prebid/server/metric/TimeoutNotificationMetrics.java new file mode 100644 index 00000000000..b3e6bce3f5f --- /dev/null +++ b/src/main/java/org/prebid/server/metric/TimeoutNotificationMetrics.java @@ -0,0 +1,16 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; + +/** + * Contains user sync metrics for a bidders metrics support. + */ +class TimeoutNotificationMetrics extends UpdatableMetrics { + + TimeoutNotificationMetrics(MetricRegistry metricRegistry, CounterType counterType) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + metricName -> String.format("timeout_notification.%s", metricName.toString())); + } +} diff --git a/src/main/java/org/prebid/server/metric/UpdatableMetrics.java b/src/main/java/org/prebid/server/metric/UpdatableMetrics.java index 1198b9e89f9..93dd90edde7 100644 --- a/src/main/java/org/prebid/server/metric/UpdatableMetrics.java +++ b/src/main/java/org/prebid/server/metric/UpdatableMetrics.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.function.LongSupplier; class UpdatableMetrics { @@ -68,6 +69,14 @@ void updateHistogram(MetricName metricName, long value) { metricRegistry.histogram(name(metricName)).update(value); } + void createGauge(MetricName metricName, LongSupplier supplier) { + metricRegistry.gauge(name(metricName), () -> supplier::getAsLong); + } + + void removeMetric(MetricName metricName) { + metricRegistry.remove(name(metricName)); + } + private String name(MetricName metricName) { return metricNames.computeIfAbsent(metricName, key -> nameCreator.apply(metricName)); } diff --git a/src/main/java/org/prebid/server/metric/ValidationMetrics.java b/src/main/java/org/prebid/server/metric/ValidationMetrics.java new file mode 100644 index 00000000000..a00ebea15d2 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/ValidationMetrics.java @@ -0,0 +1,41 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Request metrics support. + */ +class ValidationMetrics extends UpdatableMetrics { + + private final SpecificValidationMetrics sizeValidationMetrics; + private final SpecificValidationMetrics secureValidationMetrics; + + ValidationMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix)))); + + sizeValidationMetrics = new SpecificValidationMetrics( + metricRegistry, counterType, createPrefix(prefix), "size"); + secureValidationMetrics = new SpecificValidationMetrics( + metricRegistry, counterType, createPrefix(prefix), "secure"); + } + + private static Function nameCreator(String prefix) { + return metricName -> String.format("%s.%s", prefix, metricName.toString()); + } + + private static String createPrefix(String prefix) { + return String.format("%s.validation", prefix); + } + + SpecificValidationMetrics size() { + return sizeValidationMetrics; + } + + SpecificValidationMetrics secure() { + return secureValidationMetrics; + } +} diff --git a/src/main/java/org/prebid/server/model/CaseInsensitiveMultiMap.java b/src/main/java/org/prebid/server/model/CaseInsensitiveMultiMap.java new file mode 100644 index 00000000000..905cc45a086 --- /dev/null +++ b/src/main/java/org/prebid/server/model/CaseInsensitiveMultiMap.java @@ -0,0 +1,105 @@ +package org.prebid.server.model; + +import io.vertx.core.MultiMap; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class CaseInsensitiveMultiMap { + + private static final CaseInsensitiveMultiMap EMPTY = builder().build(); + + private final io.vertx.core.MultiMap delegate; + + private CaseInsensitiveMultiMap(MultiMap delegate) { + this.delegate = delegate; + } + + public static CaseInsensitiveMultiMap empty() { + return EMPTY; + } + + public String get(CharSequence name) { + return this.delegate.get(name); + } + + public String get(String name) { + return this.delegate.get(name); + } + + public List getAll(String name) { + return this.delegate.getAll(name); + } + + public List getAll(CharSequence name) { + return this.delegate.getAll(name); + } + + public List> entries() { + return this.delegate.entries(); + } + + public boolean contains(String name) { + return this.delegate.contains(name); + } + + public boolean contains(CharSequence name) { + return this.delegate.contains(name); + } + + public boolean isEmpty() { + return this.delegate.isEmpty(); + } + + public Set names() { + return this.delegate.names(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private final io.vertx.core.MultiMap delegate; + + public Builder() { + this.delegate = io.vertx.core.MultiMap.caseInsensitiveMultiMap(); + } + + public CaseInsensitiveMultiMap build() { + return new CaseInsensitiveMultiMap(delegate); + } + + public Builder add(String name, String value) { + delegate.add(name, value); + return this; + } + + public Builder add(CharSequence name, CharSequence value) { + delegate.add(name, value); + return this; + } + + public Builder add(String name, Iterable values) { + delegate.add(name, values); + return this; + } + + public Builder add(CharSequence name, Iterable values) { + delegate.add(name, values); + return this; + } + + public Builder addAll(CaseInsensitiveMultiMap map) { + map.entries().forEach(entry -> delegate.add(entry.getKey(), entry.getValue())); + return this; + } + + public Builder addAll(Map map) { + delegate.addAll(map); + return this; + } + } +} diff --git a/src/main/java/org/prebid/server/model/Endpoint.java b/src/main/java/org/prebid/server/model/Endpoint.java new file mode 100644 index 00000000000..498b47dc432 --- /dev/null +++ b/src/main/java/org/prebid/server/model/Endpoint.java @@ -0,0 +1,47 @@ +package org.prebid.server.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.Arrays; + +/** + * Describes statically routed endpoints. + *

+ * Note: be aware, admin endpoints have configurable routing. + */ +public enum Endpoint { + + openrtb2_auction("/openrtb2/auction"), + openrtb2_amp("/openrtb2/amp"), + openrtb2_video("/openrtb2/video"), + cookie_sync("/cookie_sync"), + setuid("/setuid"), + + bidder_params("/bidders/params"), + event("/event"), + getuids("/getuids"), + info_bidders("/info/bidders"), + optout("/optout"), + status("/status"), + vtrack("/vtrack"); + + @JsonValue + private final String value; + + Endpoint(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @JsonCreator + public static Endpoint fromString(String value) { + return Arrays.stream(values()) + .filter(endpoint -> endpoint.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown endpoint")); + } +} diff --git a/src/main/java/org/prebid/server/model/HttpRequestContext.java b/src/main/java/org/prebid/server/model/HttpRequestContext.java new file mode 100644 index 00000000000..8ed9c135cc4 --- /dev/null +++ b/src/main/java/org/prebid/server/model/HttpRequestContext.java @@ -0,0 +1,21 @@ +package org.prebid.server.model; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class HttpRequestContext { + + String absoluteUri; + + CaseInsensitiveMultiMap queryParams; + + CaseInsensitiveMultiMap headers; + + String body; + + String scheme; + + String remoteHost; +} diff --git a/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java b/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java index cd11b584461..fae6afe39a6 100644 --- a/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java +++ b/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java @@ -13,7 +13,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.request.CookieSyncRequest; -import org.prebid.server.proto.request.PreBidRequest; + +import java.util.List; /** * GDPR-aware utilities @@ -40,12 +41,8 @@ public class PrivacyExtractor { *

* And construct {@link Privacy} from them. Use default values in case of invalid value. */ - public Privacy validPrivacyFrom(BidRequest bidRequest) { - return extractPrivacy(bidRequest.getRegs(), bidRequest.getUser()); - } - - public Privacy validPrivacyFrom(PreBidRequest preBidRequest) { - return extractPrivacy(preBidRequest.getRegs(), preBidRequest.getUser()); + public Privacy validPrivacyFrom(BidRequest bidRequest, List errors) { + return extractPrivacy(bidRequest.getRegs(), bidRequest.getUser(), errors); } public Privacy validPrivacyFrom(CookieSyncRequest request) { @@ -54,17 +51,17 @@ public Privacy validPrivacyFrom(CookieSyncRequest request) { final String gdprConsent = request.getGdprConsent(); final String usPrivacy = request.getUsPrivacy(); - return toValidPrivacy(gdpr, gdprConsent, usPrivacy, null); + return toValidPrivacy(gdpr, gdprConsent, usPrivacy, null, null); } public Privacy validPrivacyFromSetuidRequest(HttpServerRequest request) { final String gdpr = request.getParam(SETUID_GDPR_PARAM); final String gdprConsent = request.getParam(SETUID_GDPR_CONSENT_PARAM); - return toValidPrivacy(gdpr, gdprConsent, null, null); + return toValidPrivacy(gdpr, gdprConsent, null, null, null); } - private Privacy extractPrivacy(Regs regs, User user) { + private Privacy extractPrivacy(Regs regs, User user, List errors) { final ExtRegs extRegs = regs != null ? regs.getExt() : null; final ExtUser extUser = user != null ? user.getExt() : null; @@ -74,27 +71,34 @@ private Privacy extractPrivacy(Regs regs, User user) { final String usPrivacy = extRegs != null ? extRegs.getUsPrivacy() : null; final Integer coppa = regs != null ? regs.getCoppa() : null; - return toValidPrivacy(gdpr, consent, usPrivacy, coppa); + return toValidPrivacy(gdpr, consent, usPrivacy, coppa, errors); } - private static Privacy toValidPrivacy(String gdpr, String consent, String usPrivacy, Integer coppa) { + private static Privacy toValidPrivacy(String gdpr, + String consent, + String usPrivacy, + Integer coppa, + List errors) { final String validGdpr = ObjectUtils.notEqual(gdpr, "1") && ObjectUtils.notEqual(gdpr, "0") ? DEFAULT_GDPR_VALUE : gdpr; final String validConsent = consent == null ? DEFAULT_CONSENT_VALUE : consent; - final Ccpa validCcpa = usPrivacy == null ? DEFAULT_CCPA_VALUE : toValidCcpa(usPrivacy); + final Ccpa validCcpa = usPrivacy == null ? DEFAULT_CCPA_VALUE : toValidCcpa(usPrivacy, errors); final Integer validCoppa = coppa == null ? DEFAULT_COPPA_VALUE : coppa; return Privacy.of(validGdpr, validConsent, validCcpa, validCoppa); } - private static Ccpa toValidCcpa(String usPrivacy) { + private static Ccpa toValidCcpa(String usPrivacy, List errors) { try { Ccpa.validateUsPrivacy(usPrivacy); return Ccpa.of(usPrivacy); } catch (PreBidException e) { - // TODO add error to PBS response, not only in logs (See PR #758) - logger.debug("CCPA consent {0} has invalid format: {1}", usPrivacy, e.getMessage()); + final String message = String.format("CCPA consent %s has invalid format: %s", usPrivacy, e.getMessage()); + logger.debug(message); + if (errors != null) { + errors.add(message); + } return DEFAULT_CCPA_VALUE; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/GdprService.java b/src/main/java/org/prebid/server/privacy/gdpr/GdprService.java deleted file mode 100644 index 5d45046ae38..00000000000 --- a/src/main/java/org/prebid/server/privacy/gdpr/GdprService.java +++ /dev/null @@ -1,174 +0,0 @@ -package org.prebid.server.privacy.gdpr; - -import com.iab.gdpr.consent.VendorConsent; -import com.iab.gdpr.consent.VendorConsentDecoder; -import com.iab.gdpr.exception.VendorConsentParseException; -import io.vertx.core.Future; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; -import org.prebid.server.privacy.gdpr.model.VendorPermission; -import org.prebid.server.privacy.gdpr.vendorlist.VendorListService; -import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorListV1; -import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV1; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Service provides GDPR support. - *

- * For more information about GDPR, see https://gdpr.iab.com site. - */ -public class GdprService { - - private static final Logger logger = LoggerFactory.getLogger(GdprService.class); - - private static final int PURPOSE_ONE_ID = 1; - - private final VendorListService vendorListService; - - public GdprService(VendorListService vendorListService) { - this.vendorListService = Objects.requireNonNull(vendorListService); - } - - /** - * Determines what is allowed and what is not (in terms of TCF v1.1 implementation aka GDPR) for each vendor - * taking into account user consent string (version 1.1). - * - * @param vendorIds to examine in consent string and vendor list - * @param vendorConsentString user consent string - * @return collection of {@link VendorPermission}s indicating what vendor is allowed to do - */ - public Future> resultFor(Set vendorIds, String vendorConsentString) { - - final VendorConsent vendorConsent = vendorConsentFrom(vendorConsentString); - if (vendorConsent == null) { - // consent string is broken - return Future.succeededFuture(vendorIds.stream() - .map(GdprService::toRestrictedVendorPermission) - .collect(Collectors.toList())); - } - - return vendorListService.forVersion(vendorConsent.getVendorListVersion()) - .compose(vendorListMapping -> toResult(vendorListMapping, vendorIds, vendorConsent), - ignoredFailed -> toFallbackResult(vendorIds)); - } - - /** - * Parses consent string to {@link VendorConsent} model. Returns null if: - *

- * - consent string is missing - *

- * - parsing of consent string is failed - */ - private VendorConsent vendorConsentFrom(String vendorConsentString) { - if (StringUtils.isEmpty(vendorConsentString)) { - return null; - } - try { - return VendorConsentDecoder.fromBase64String(vendorConsentString); - } catch (IllegalArgumentException | IllegalStateException e) { - logger.info("Parsing consent string failed with error: {0}", e.getMessage()); - return null; - } - } - - /** - * Processes {@link VendorListService} response and returns GDPR result for every vendor ID. - */ - private static Future> toResult(Map vendorListMapping, - Set vendorIds, - VendorConsent vendorConsent) { - - final Set allowedPurposeIds = getAllowedPurposeIdsFromConsent(vendorConsent); - final List vendorPermissions = vendorIds.stream() - .map(vendorId -> toVendorPermission(vendorId, vendorListMapping, vendorConsent, allowedPurposeIds)) - .collect(Collectors.toList()); - return Future.succeededFuture(vendorPermissions); - } - - /** - * Retrieves allowed purpose ids from consent string. Throws {@link InvalidRequestException} in case of - * gdpr sdk throws {@link ArrayIndexOutOfBoundsException} when consent string is not valid. - */ - private static Set getAllowedPurposeIdsFromConsent(VendorConsent vendorConsent) { - try { - return vendorConsent.getAllowedPurposeIds(); - } catch (ArrayIndexOutOfBoundsException e) { - throw new InvalidRequestException( - "Error when retrieving allowed purpose ids in a reason of invalid consent string"); - } - } - - private static VendorPermission toVendorPermission(Integer vendorId, - Map vendorListMapping, - VendorConsent vendorConsent, - Set allowedPurposeIds) { - - // confirm that there is consent for vendor and it has entry in vendor list - if (!isVendorAllowed(vendorConsent, vendorId) || !vendorListMapping.containsKey(vendorId)) { - return toRestrictedVendorPermission(vendorId); - } - - final VendorV1 vendorListEntry = vendorListMapping.get(vendorId); - - // confirm purposes - final Set claimedPurposes = vendorListEntry.combinedPurposes(); - final boolean claimedPurposesAllowed = allowedPurposeIds.containsAll(claimedPurposes); - final boolean purposeOneClaimedAndAllowed = isPurposeOneClaimedAndAllowed(claimedPurposes, allowedPurposeIds); - - return VendorPermission.of(vendorId, null, toAction(claimedPurposesAllowed, purposeOneClaimedAndAllowed)); - } - - private static VendorPermission toRestrictedVendorPermission(Integer vendorId) { - return VendorPermission.of(vendorId, null, allDenied()); - } - - /** - * Checks if vendorId is in list of allowed vendors in consent string. Throws {@link InvalidRequestException} - * in case of gdpr sdk throws exception when consent string is not valid. - */ - private static boolean isVendorAllowed(VendorConsent vendorConsent, Integer vendorId) { - try { - return vendorConsent.isVendorAllowed(vendorId); - } catch (ArrayIndexOutOfBoundsException | VendorConsentParseException e) { - throw new InvalidRequestException( - "Error when checking if vendor is allowed in a reason of invalid consent string"); - } - } - - private static boolean isPurposeOneClaimedAndAllowed(Set claimedPurposes, Set allowedPurposeIds) { - return claimedPurposes.contains(PURPOSE_ONE_ID) && allowedPurposeIds.contains(PURPOSE_ONE_ID); - } - - private static PrivacyEnforcementAction allDenied() { - return toAction(false, false); - } - - private static PrivacyEnforcementAction toAction(boolean allowPrivateInfo, boolean allowUserSync) { - return PrivacyEnforcementAction.builder() - .removeUserIds(!allowPrivateInfo) - .maskGeo(!allowPrivateInfo) - .maskDeviceIp(!allowPrivateInfo) - .maskDeviceInfo(!allowPrivateInfo) - .blockAnalyticsReport(false) - .blockBidderRequest(false) - .blockPixelSync(!allowUserSync) - .build(); - } - - private static Future> toFallbackResult(Set vendorIds) { - final List vendorPermissions = vendorIds.stream() - .filter(Objects::nonNull) - .map(GdprService::toRestrictedVendorPermission) - .collect(Collectors.toList()); - return Future.succeededFuture(vendorPermissions); - } -} diff --git a/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java b/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java index 8fea27eaf1d..93560e416eb 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/Tcf2Service.java @@ -2,6 +2,8 @@ import com.iabtcf.decoder.TCString; import io.vertx.core.Future; +import lombok.Value; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; import org.prebid.server.privacy.gdpr.model.VendorPermission; @@ -9,6 +11,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.PurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.specialfeature.SpecialFeaturesStrategy; import org.prebid.server.privacy.gdpr.vendorlist.VendorListServiceV2; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.AccountGdprConfig; import org.prebid.server.settings.model.EnforcePurpose; @@ -20,11 +23,13 @@ import org.prebid.server.settings.model.SpecialFeatures; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; public class Tcf2Service { @@ -54,16 +59,15 @@ public Tcf2Service(GdprConfig gdprConfig, } public Future> permissionsFor(Set vendorIds, TCString tcfConsent) { - final Collection vendorPermissions = vendorPermissions(vendorIds); - return permissionsForInternal(vendorPermissions, tcfConsent, null); + return permissionsForInternal(vendorPermissions(vendorIds), tcfConsent, null); } public Future> permissionsFor(Set bidderNames, VendorIdResolver vendorIdResolver, TCString tcfConsent, AccountGdprConfig accountGdprConfig) { - final Collection vendorPermissions = vendorPermissions(bidderNames, vendorIdResolver); - return permissionsForInternal(vendorPermissions, tcfConsent, accountGdprConfig); + + return permissionsForInternal(vendorPermissions(bidderNames, vendorIdResolver), tcfConsent, accountGdprConfig); } private Collection vendorPermissions(Set vendorIds) { @@ -75,9 +79,7 @@ private Collection vendorPermissions(Set vendorIds) { .collect(Collectors.toList()); } - private Collection vendorPermissions(Set bidderNames, - VendorIdResolver vendorIdResolver) { - + private Collection vendorPermissions(Set bidderNames, VendorIdResolver vendorIdResolver) { return bidderNames.stream() // this check only for illegal arguments... .filter(Objects::nonNull) @@ -95,28 +97,64 @@ private Future> permissionsForInternal(Collection vendorPermissionsByType = toVendorPermissionsByType( + vendorPermissions, accountGdprConfig); + return vendorListServiceV2.forVersion(tcfConsent.getVendorListVersion()) - .map(vendorGvlPermissions -> wrapWithGVL(vendorPermissions, vendorGvlPermissions)) + .map(vendorGvlPermissions -> wrapWithGVL(vendorPermissionsByType, vendorGvlPermissions)) .compose(gvlResult -> processSupportedPurposeStrategies(tcfConsent, gvlResult, mergedPurposes, purposeOneTreatmentInterpretation), - ignoredFailed -> processDowngradedSupportedPurposeStrategies(tcfConsent, vendorPermissions, - mergedPurposes, mergedPurposeOneTreatmentInterpretation)) + ignoredFailed -> processDowngradedSupportedPurposeStrategies(tcfConsent, + vendorPermissionsByType, mergedPurposes, mergedPurposeOneTreatmentInterpretation)) .map(changedVendorPermissions -> processSupportedSpecialFeatureStrategies(tcfConsent, changedVendorPermissions, mergedSpecialFeatures)); + } + + private static VendorPermissionsByType toVendorPermissionsByType( + Collection vendorPermissions, + AccountGdprConfig accountGdprConfig) { + + final List basicEnforcedVendors = accountGdprConfig != null + ? accountGdprConfig.getBasicEnforcementVendors() + : null; + if (CollectionUtils.isEmpty(basicEnforcedVendors)) { + return VendorPermissionsByType.of(Collections.emptyList(), vendorPermissions); + } + + final Map> isBasicEnforcedToPermissions = vendorPermissions.stream() + .collect(Collectors.partitioningBy(vendorPermission -> + basicEnforcedVendors.contains(vendorPermission.getBidderName()))); + + final List weakPermissions = isBasicEnforcedToPermissions.getOrDefault(true, + Collections.emptyList()); + + final List standardPermissions = isBasicEnforcedToPermissions.getOrDefault(false, + Collections.emptyList()); + return VendorPermissionsByType.of(weakPermissions, standardPermissions); } - private static Collection wrapWithGVL(Collection vendorPermissions, - Map vendorGvlPermissions) { - return vendorPermissions.stream() + private static VendorPermissionsByType wrapWithGVL( + VendorPermissionsByType vendorPermissionsByType, + Map vendorGvlPermissions) { + + final List weakPermissions = vendorPermissionsByType.getWeakPermissions().stream() + .map(vendorPermission -> wrapWithGVL(vendorPermission, vendorGvlPermissions)) + .collect(Collectors.toList()); + + final List standardPermissions = vendorPermissionsByType.getStandardPermissions() + .stream() .map(vendorPermission -> wrapWithGVL(vendorPermission, vendorGvlPermissions)) .collect(Collectors.toList()); + + return VendorPermissionsByType.of(weakPermissions, standardPermissions); } private static VendorPermissionWithGvl wrapWithGVL(VendorPermission vendorPermission, Map vendorGvlPermissions) { + final Integer vendorId = vendorPermission.getVendorId(); final VendorV2 vendorGvlByVendorId = vendorId != null ? vendorGvlPermissions.getOrDefault(vendorId, VendorV2.empty(vendorId)) @@ -127,37 +165,56 @@ private static VendorPermissionWithGvl wrapWithGVL(VendorPermission vendorPermis private Future> processSupportedPurposeStrategies( TCString tcfConsent, - Collection vendorPermissionsWithGvl, + VendorPermissionsByType vendorPermissionsByType, Purposes purposes, PurposeOneTreatmentInterpretation purposeOneTreatmentInterpretation) { for (PurposeStrategy purposeStrategy : purposeStrategies) { - final int purposeId = purposeStrategy.getPurposeId(); - final Purpose purposeById = findPurposeById(purposeId, purposes); - processPurposeStrategy(tcfConsent, vendorPermissionsWithGvl, purposeById, purposeStrategy, + final PurposeCode tcfPurpose = purposeStrategy.getPurpose(); + final Purpose purposeById = findPurposeByTcfPurpose(tcfPurpose, purposes); + final Purpose weakPurpose = weakPurpose(purposeById); + + final Collection standardPermissions = vendorPermissionsByType + .getStandardPermissions(); + final Collection weakPermissions = vendorPermissionsByType.getWeakPermissions(); + + processPurposeStrategy(tcfConsent, standardPermissions, purposeById, purposeStrategy, purposeOneTreatmentInterpretation, false); + processPurposeStrategy(tcfConsent, weakPermissions, weakPurpose, purposeStrategy, + purposeOneTreatmentInterpretation, true); } - return Future.succeededFuture(vendorPermissionsWithGvl.stream() + return Future.succeededFuture(vendorPermissionsByType.joinPermissions().stream() .map(VendorPermissionWithGvl::getVendorPermission) .collect(Collectors.toList())); } private Future> processDowngradedSupportedPurposeStrategies( TCString tcfConsent, - Collection vendorPermissions, + VendorPermissionsByType vendorPermissionsByType, Purposes purposes, PurposeOneTreatmentInterpretation purposeOneTreatmentInterpretation) { - final List vendorPermissionsWithGvl = wrapWithEmptyGVL(vendorPermissions); + final VendorPermissionsByType vendorPermissionsWithGvlByType = wrapWithGVL( + vendorPermissionsByType, Collections.emptyMap()); for (PurposeStrategy purposeStrategy : purposeStrategies) { - final int purposeId = purposeStrategy.getPurposeId(); - final Purpose downgradedPurpose = downgradePurpose(findPurposeById(purposeId, purposes)); - processPurposeStrategy(tcfConsent, vendorPermissionsWithGvl, downgradedPurpose, purposeStrategy, + final PurposeCode tcfPurpose = purposeStrategy.getPurpose(); + final Purpose downgradedPurposeById = downgradePurpose(findPurposeByTcfPurpose(tcfPurpose, purposes)); + final Purpose weakPurpose = weakPurpose(downgradedPurposeById); + + final Collection standardPermissions = vendorPermissionsWithGvlByType + .getStandardPermissions(); + final Collection weakPermissions = vendorPermissionsWithGvlByType + .getWeakPermissions(); + + processPurposeStrategy(tcfConsent, standardPermissions, downgradedPurposeById, purposeStrategy, + purposeOneTreatmentInterpretation, true); + processPurposeStrategy(tcfConsent, weakPermissions, weakPurpose, purposeStrategy, purposeOneTreatmentInterpretation, true); } - return Future.succeededFuture(vendorPermissions); + + return Future.succeededFuture(vendorPermissionsByType.joinPermissions()); } private void processPurposeStrategy(TCString tcfConsent, @@ -166,9 +223,17 @@ private void processPurposeStrategy(TCString tcfConsent, PurposeStrategy purposeStrategy, PurposeOneTreatmentInterpretation purposeOneTreatmentInterpretation, boolean wasDowngraded) { - if (purposeStrategy.getPurposeId() == 1 && tcfConsent.getPurposeOneTreatment()) { - processPurposeOneTreatment(purposeOneTreatmentInterpretation, tcfConsent, purpose, - purposeStrategy, vendorPermissionsWithGvl, wasDowngraded); + + if (purposeStrategy.getPurpose() == PurposeCode.ONE + && tcfConsent.getPurposeOneTreatment()) { + + processPurposeOneTreatment( + purposeOneTreatmentInterpretation, + tcfConsent, + purpose, + purposeStrategy, + vendorPermissionsWithGvl, + wasDowngraded); } else { purposeStrategy.processTypePurposeStrategy(tcfConsent, purpose, vendorPermissionsWithGvl, wasDowngraded); } @@ -180,35 +245,40 @@ private void processPurposeOneTreatment(PurposeOneTreatmentInterpretation purpos PurposeStrategy purposeOneStrategy, Collection vendorPermissionsWithGvl, boolean wasDowngraded) { + switch (purposeOneTreatmentInterpretation) { case accessAllowed: - vendorPermissionsWithGvl.forEach(vendorPermission -> purposeOneStrategy.allow( - vendorPermission.getVendorPermission().getPrivacyEnforcementAction())); + vendorPermissionsWithGvl.forEach(vendorPermission -> + purposeOneStrategy.allow(vendorPermission.getVendorPermission().getPrivacyEnforcementAction())); break; case noAccessAllowed: // no need for special processing of no-access-allowed since everything is disallowed from the beginning break; case ignore: default: - purposeOneStrategy.processTypePurposeStrategy(tcfConsent, purposeOne, vendorPermissionsWithGvl, - wasDowngraded); + purposeOneStrategy.processTypePurposeStrategy( + tcfConsent, purposeOne, vendorPermissionsWithGvl, wasDowngraded); } } - private static List wrapWithEmptyGVL(Collection vendorPermissions) { - return vendorPermissions.stream() - .map(vendorPermission -> VendorPermissionWithGvl.of(vendorPermission, - VendorV2.empty(vendorPermission.getVendorId()))) - .collect(Collectors.toList()); - } - private static Purpose downgradePurpose(Purpose purpose) { final EnforcePurpose enforcePurpose = purpose.getEnforcePurpose(); + return enforcePurpose == null || Objects.equals(enforcePurpose, EnforcePurpose.full) ? Purpose.of(EnforcePurpose.basic, purpose.getEnforceVendors(), purpose.getVendorExceptions()) : purpose; } + private static Purpose weakPurpose(Purpose purpose) { + final EnforcePurpose enforcePurpose = purpose.getEnforcePurpose(); + final EnforcePurpose downgradedEnforce = + enforcePurpose == null || Objects.equals(enforcePurpose, EnforcePurpose.full) + ? EnforcePurpose.basic + : enforcePurpose; + + return Purpose.of(downgradedEnforce, false, purpose.getVendorExceptions()); + } + private Collection processSupportedSpecialFeatureStrategies( TCString tcfConsent, Collection vendorPermissions, @@ -227,6 +297,7 @@ private Purposes mergeAccountPurposes(AccountGdprConfig accountGdprConfig) { if (accountGdprConfig == null || accountGdprConfig.getPurposes() == null) { return defaultPurposes; } + final Purposes accountPurposes = accountGdprConfig.getPurposes(); return Purposes.builder() .p1(mergeItem(accountPurposes.getP1(), defaultPurposes.getP1())) @@ -238,7 +309,7 @@ private Purposes mergeAccountPurposes(AccountGdprConfig accountGdprConfig) { .p7(mergeItem(accountPurposes.getP7(), defaultPurposes.getP7())) .p8(mergeItem(accountPurposes.getP8(), defaultPurposes.getP8())) .p9(mergeItem(accountPurposes.getP9(), defaultPurposes.getP9())) - .p9(mergeItem(accountPurposes.getP10(), defaultPurposes.getP10())) + .p10(mergeItem(accountPurposes.getP10(), defaultPurposes.getP10())) .build(); } @@ -246,6 +317,7 @@ private SpecialFeatures mergeAccountSpecialFeatures(AccountGdprConfig accountGdp if (accountGdprConfig == null || accountGdprConfig.getSpecialFeatures() == null) { return defaultSpecialFeatures; } + final SpecialFeatures accountSpecialFeatures = accountGdprConfig.getSpecialFeatures(); return SpecialFeatures.builder() .sf1(mergeItem(accountSpecialFeatures.getSf1(), defaultSpecialFeatures.getSf1())) @@ -253,31 +325,30 @@ private SpecialFeatures mergeAccountSpecialFeatures(AccountGdprConfig accountGdp .build(); } - private Purpose findPurposeById(int tcfPurposeId, Purposes purposes) { - switch (tcfPurposeId) { - case 1: + private Purpose findPurposeByTcfPurpose(PurposeCode tcfPurpose, Purposes purposes) { + switch (tcfPurpose) { + case ONE: return purposes.getP1(); - case 2: + case TWO: return purposes.getP2(); - case 3: + case THREE: return purposes.getP3(); - case 4: + case FOUR: return purposes.getP4(); - case 5: + case FIVE: return purposes.getP5(); - case 6: + case SIX: return purposes.getP6(); - case 7: + case SEVEN: return purposes.getP7(); - case 8: + case EIGHT: return purposes.getP8(); - case 9: + case NINE: return purposes.getP9(); - case 10: + case TEN: return purposes.getP10(); default: - throw new IllegalArgumentException(String.format("Illegal TCF code for purpose: %d", - tcfPurposeId)); + throw new IllegalArgumentException(String.format("Illegal TCF code for purpose: %s", tcfPurpose)); } } @@ -306,4 +377,17 @@ private PurposeOneTreatmentInterpretation mergePurposeOneTreatmentInterpretation private static T mergeItem(T prioritisedItem, T item) { return prioritisedItem == null ? item : prioritisedItem; } + + @Value(staticConstructor = "of") + private static class VendorPermissionsByType { + + Collection weakPermissions; + + Collection standardPermissions; + + public Collection joinPermissions() { + return Stream.concat(weakPermissions.stream(), standardPermissions.stream()) + .collect(Collectors.toList()); + } + } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java index 971c290627e..bf6a1b1e085 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java @@ -8,13 +8,16 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.IpAddressHelper; +import org.prebid.server.auction.model.IpAddress; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.execution.Timeout; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.geolocation.model.GeoInfo; +import org.prebid.server.log.ConditionalLogger; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; +import org.prebid.server.privacy.gdpr.model.RequestLogInfo; import org.prebid.server.privacy.gdpr.model.TCStringEmpty; import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.gdpr.model.TcfResponse; @@ -25,7 +28,7 @@ import org.prebid.server.settings.model.GdprConfig; import java.util.Collection; -import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -36,6 +39,14 @@ public class TcfDefinerService { private static final Logger logger = LoggerFactory.getLogger(TcfDefinerService.class); + private static final ConditionalLogger AMP_CORRUPT_CONSENT_LOGGER = + new ConditionalLogger("amp_corrupt_consent", logger); + private static final ConditionalLogger APP_CORRUPT_CONSENT_LOGGER = + new ConditionalLogger("app_corrupt_consent", logger); + private static final ConditionalLogger SITE_CORRUPT_CONSENT_LOGGER = + new ConditionalLogger("site_corrupt_consent", logger); + private static final ConditionalLogger UNDEFINED_CORRUPT_CONSENT_LOGGER = + new ConditionalLogger("undefined_corrupt_consent", logger); private static final String GDPR_ZERO = "0"; private static final String GDPR_ONE = "1"; @@ -43,7 +54,6 @@ public class TcfDefinerService { private final boolean gdprEnabled; private final String gdprDefaultValue; private final boolean consentStringMeansInScope; - private final GdprService gdprService; private final Tcf2Service tcf2Service; private final Set eeaCountries; private final GeoLocationService geoLocationService; @@ -53,7 +63,6 @@ public class TcfDefinerService { public TcfDefinerService(GdprConfig gdprConfig, Set eeaCountries, - GdprService gdprService, Tcf2Service tcf2Service, GeoLocationService geoLocationService, BidderCatalog bidderCatalog, @@ -64,7 +73,6 @@ public TcfDefinerService(GdprConfig gdprConfig, this.gdprDefaultValue = gdprConfig != null ? gdprConfig.getDefaultValue() : null; this.consentStringMeansInScope = gdprConfig != null && BooleanUtils.isTrue(gdprConfig.getConsentStringMeansInScope()); - this.gdprService = Objects.requireNonNull(gdprService); this.tcf2Service = Objects.requireNonNull(tcf2Service); this.eeaCountries = Objects.requireNonNull(eeaCountries); this.geoLocationService = geoLocationService; @@ -73,35 +81,45 @@ public TcfDefinerService(GdprConfig gdprConfig, this.metrics = Objects.requireNonNull(metrics); } - public Future resolveTcfContext( - Privacy privacy, - String country, - String ipAddress, - AccountGdprConfig accountGdprConfig, - MetricName requestType, - Timeout timeout) { + /** + * Used for auctions. + */ + public Future resolveTcfContext(Privacy privacy, + String country, + String ipAddress, + AccountGdprConfig accountGdprConfig, + MetricName requestType, + RequestLogInfo requestLogInfo, + Timeout timeout, + List debugWarnings) { if (!isGdprEnabled(accountGdprConfig, requestType)) { return Future.succeededFuture(TcfContext.empty()); } - return toTcfContext(privacy, country, ipAddress, timeout) + return toTcfContext(privacy, country, ipAddress, requestLogInfo, timeout, debugWarnings) .map(this::updateTcfGeoMetrics); } - public Future resolveTcfContext( - Privacy privacy, String ipAddress, AccountGdprConfig accountGdprConfig, Timeout timeout) { - - return resolveTcfContext(privacy, null, ipAddress, accountGdprConfig, null, timeout); + /** + * Used for cookie sync and setuid. + */ + public Future resolveTcfContext(Privacy privacy, + String ipAddress, + AccountGdprConfig accountGdprConfig, + MetricName requestType, + RequestLogInfo requestLogInfo, + Timeout timeout) { + + return resolveTcfContext(privacy, null, ipAddress, + accountGdprConfig, requestType, requestLogInfo, timeout, null); } public Future> resultForVendorIds(Set vendorIds, TcfContext tcfContext) { return resultForInternal( tcfContext, country -> createAllowAllTcfResponse(vendorIds, country), - (consentString, country) -> tcf2Service.permissionsFor(vendorIds, consentString) - .map(vendorPermissions -> createVendorIdTcfResponse(vendorPermissions, country)), - (consentString, country) -> gdprService.resultFor(vendorIds, consentString) + (tcfConsent, country) -> tcf2Service.permissionsFor(vendorIds, tcfConsent) .map(vendorPermissions -> createVendorIdTcfResponse(vendorPermissions, country))); } @@ -114,9 +132,7 @@ public Future> resultForBidderNames(Set bidderNames, country -> createAllowAllTcfResponse(bidderNames, country), (consentString, country) -> tcf2Service.permissionsFor(bidderNames, vendorIdResolver, consentString, accountGdprConfig) - .map(vendorPermissions -> createBidderNameTcfResponse(vendorPermissions, country)), - (consentString, country) -> - bidderNameResultFromGdpr(bidderNames, vendorIdResolver, consentString, country)); + .map(vendorPermissions -> createBidderNameTcfResponse(vendorPermissions, country))); } public Future> resultForBidderNames( @@ -128,8 +144,7 @@ public Future> resultForBidderNames( private Future> resultForInternal( TcfContext tcfContext, Function>> allowAllTcfResponseCreator, - BiFunction>> tcf2Strategy, - BiFunction>> gdprStrategy) { + BiFunction>> tcf2Strategy) { final GeoInfo geoInfo = tcfContext.getGeoInfo(); final String country = geoInfo != null ? geoInfo.getCountry() : null; @@ -138,11 +153,7 @@ private Future> resultForInternal( return allowAllTcfResponseCreator.apply(country); } - final TCString consent = tcfContext.getConsent(); - - return consent.getVersion() == 2 - ? tcf2Strategy.apply(consent, country) - : gdprStrategy.apply(tcfContext.getConsentString(), country); + return tcf2Strategy.apply(tcfContext.getConsent(), country); } private boolean isGdprEnabled(AccountGdprConfig accountGdprConfig, MetricName requestType) { @@ -161,16 +172,23 @@ private boolean isGdprEnabled(AccountGdprConfig accountGdprConfig, MetricName re return ObjectUtils.firstNonNull(enabledForType, accountGdprEnabled, gdprEnabled); } - private Future toTcfContext(Privacy privacy, String country, String ipAddress, Timeout timeout) { + private Future toTcfContext(Privacy privacy, + String country, + String ipAddress, + RequestLogInfo requestLogInfo, + Timeout timeout, + List debugWarnings) { final String consentString = privacy.getConsentString(); - final TCString consent = parseConsentString(consentString); + final TCString consent = parseConsentString(consentString, requestLogInfo, debugWarnings); final String effectiveIpAddress = maybeMaskIp(ipAddress, consent); + final boolean consentIsValid = isConsentValid(consent); - if (consentStringMeansInScope && isConsentValid(consent)) { + if (consentStringMeansInScope && consentIsValid) { return Future.succeededFuture(TcfContext.builder() .gdpr(GDPR_ONE) .consentString(consentString) .consent(consent) + .isConsentValid(true) .ipAddress(effectiveIpAddress) .build()); } @@ -181,6 +199,7 @@ private Future toTcfContext(Privacy privacy, String country, String .gdpr(gdpr) .consentString(consentString) .consent(consent) + .isConsentValid(consentIsValid) .ipAddress(effectiveIpAddress) .build()); } @@ -193,6 +212,7 @@ private Future toTcfContext(Privacy privacy, String country, String .gdpr(gdprFromGeo(inEea)) .consentString(consentString) .consent(consent) + .isConsentValid(consentIsValid) .inEea(inEea) .ipAddress(effectiveIpAddress) .build()); @@ -212,7 +232,18 @@ private Future toTcfContext(Privacy privacy, String country, String } private String maybeMaskIp(String ipAddress, TCString consent) { - return shouldMaskIp(consent) ? ipAddressHelper.maskIpv4(ipAddress) : ipAddress; + if (!shouldMaskIp(consent)) { + return ipAddress; + } + + final IpAddress ip = ipAddressHelper.toIpAddress(ipAddress); + if (ip == null) { + return ipAddress; + } + + return ip.getVersion() == IpAddress.IP.v4 + ? ipAddressHelper.maskIpv4(ipAddress) + : ipAddressHelper.anonymizeIpv6(ipAddress); } private static boolean shouldMaskIp(TCString consent) { @@ -228,6 +259,7 @@ private TcfContext tcfContextFromGeo(GeoInfo geoInfo, String consentString, TCSt .gdpr(gdprFromGeo(inEea)) .consentString(consentString) .consent(consent) + .isConsentValid(isConsentValid(consent)) .geoInfo(geoInfo) .inEea(inEea) .ipAddress(ipAddress) @@ -261,6 +293,7 @@ private TcfContext defaultTcfContext(String consentString, TCString consent, Str .gdpr(gdprDefaultValue) .consentString(consentString) .consent(consent) + .isConsentValid(isConsentValid(consent)) .ipAddress(ipAddress) .build(); } @@ -301,56 +334,6 @@ private static TcfResponse createBidderNameTcfResponse( country); } - private Future> bidderNameResultFromGdpr( - Set bidderNames, VendorIdResolver vendorIdResolver, String consentString, String country) { - - final Map bidderToVendorId = resolveBidderToVendorId(bidderNames, vendorIdResolver); - final Set vendorIds = bidderToVendorId.values().stream() - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - return gdprService.resultFor(vendorIds, consentString) - .map(vendorPermissions -> gdprResponseToTcfResponse(vendorPermissions, bidderToVendorId, country)); - } - - private static Map resolveBidderToVendorId( - Set bidderNames, VendorIdResolver vendorIdResolver) { - - final Map bidderToVendorId = new HashMap<>(); - bidderNames.forEach(bidderName -> bidderToVendorId.put(bidderName, vendorIdResolver.resolve(bidderName))); - return bidderToVendorId; - } - - private static TcfResponse gdprResponseToTcfResponse(Collection vendorPermissions, - Map bidderToVendorId, - String country) { - - final Map bidderNameToAction = new HashMap<>(); - - final Map> vendorIdToBidders = bidderToVendorId.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .collect(Collectors.groupingBy( - Map.Entry::getValue, - Collectors.mapping(Map.Entry::getKey, Collectors.toSet()))); - - for (final VendorPermission vendorPermission : vendorPermissions) { - final Integer vendorId = vendorPermission.getVendorId(); - - if (vendorIdToBidders.containsKey(vendorId)) { - final PrivacyEnforcementAction action = vendorPermission.getPrivacyEnforcementAction(); - vendorIdToBidders.get(vendorId).forEach(bidderName -> bidderNameToAction.put(bidderName, action)); - } - } - - // process bidders whose vendorIds weren't resolved - bidderToVendorId.entrySet().stream() - .filter(entry -> entry.getValue() == null) - .map(Map.Entry::getKey) - .forEach(bidder -> bidderNameToAction.put(bidder, restrictAllButAnalyticsAndAuction())); - - return TcfResponse.of(true, bidderNameToAction, country); - } - private static Map allowAll(Collection identifiers) { return identifiers.stream() .collect(Collectors.toMap(Function.identity(), ignored -> PrivacyEnforcementAction.allowAll())); @@ -365,31 +348,80 @@ private static boolean inScope(TcfContext tcfContext) { *

* Note: parsing TC string should not fail the entire request, but assume the user does not consent. */ - private TCString parseConsentString(String consentString) { + private TCString parseConsentString(String consentString, + RequestLogInfo requestLogInfo, + List debugWarnings) { if (StringUtils.isBlank(consentString)) { metrics.updatePrivacyTcfMissingMetric(); return TCStringEmpty.create(); } - final TCString tcString = decodeTcString(consentString); + final TCString tcString = decodeTcString(consentString, requestLogInfo); if (tcString == null) { metrics.updatePrivacyTcfInvalidMetric(); return TCStringEmpty.create(); } + final int version = tcString.getVersion(); + metrics.updatePrivacyTcfRequestsMetric(version); + // disable TCF1 support + if (version == 1) { + if (debugWarnings != null) { + debugWarnings.add( + String.format("Parsing consent string:\"%s\" failed. TCF version 1 is " + + "deprecated and treated as corrupted TCF version 2", consentString)); + } + return TCStringEmpty.create(); + } return tcString; } - private TCString decodeTcString(String consentString) { + private TCString decodeTcString(String consentString, RequestLogInfo requestLogInfo) { try { return TCString.decode(consentString); } catch (Throwable e) { - logger.info("Parsing consent string ''{0}'' failed: {1}", consentString, e.getMessage()); + logWarn(consentString, e.getMessage(), requestLogInfo); return null; } } - public static boolean isConsentValid(TCString consent) { + private static void logWarn(String consent, String message, RequestLogInfo requestLogInfo) { + if (requestLogInfo == null || requestLogInfo.getRequestType() == null) { + final String exceptionMessage = String.format("Parsing consent string:\"%s\" failed for undefined type " + + "with exception %s", consent, message); + UNDEFINED_CORRUPT_CONSENT_LOGGER.info(exceptionMessage, 100); + return; + } + + switch (requestLogInfo.getRequestType()) { + case amp: + AMP_CORRUPT_CONSENT_LOGGER.info( + logMessage(consent, MetricName.amp.toString(), requestLogInfo, message), 100); + break; + case openrtb2app: + APP_CORRUPT_CONSENT_LOGGER.info( + logMessage(consent, MetricName.openrtb2app.toString(), requestLogInfo, message), 100); + break; + case openrtb2web: + SITE_CORRUPT_CONSENT_LOGGER.info( + logMessage(consent, MetricName.openrtb2web.toString(), requestLogInfo, message), 100); + break; + case video: + case cookiesync: + case setuid: + default: + UNDEFINED_CORRUPT_CONSENT_LOGGER.info( + logMessage(consent, "video or sync or setuid", requestLogInfo, message), 100); + } + } + + private static String logMessage(String consent, String type, RequestLogInfo requestLogInfo, String message) { + return String.format( + "Parsing consent string: \"%s\" failed for: %s type for account id: %s with ref: %s with exception: %s", + consent, type, requestLogInfo.getAccountId(), requestLogInfo.getRefUrl(), message); + } + + private static boolean isConsentValid(TCString consent) { return consent != null && !(consent instanceof TCStringEmpty); } @@ -401,16 +433,4 @@ public static boolean isConsentStringValid(String consentString) { return false; } } - - private static PrivacyEnforcementAction restrictAllButAnalyticsAndAuction() { - return PrivacyEnforcementAction.builder() - .removeUserIds(true) - .maskGeo(true) - .maskDeviceIp(true) - .maskDeviceInfo(true) - .blockAnalyticsReport(false) - .blockBidderRequest(false) - .blockPixelSync(true) - .build(); - } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java b/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java index e9ebe258752..3076d653030 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java @@ -6,20 +6,30 @@ public class VendorIdResolver { private final BidderAliases aliases; + private final BidderCatalog bidderCatalog; - private VendorIdResolver(BidderAliases aliases) { + private VendorIdResolver(BidderAliases aliases, BidderCatalog bidderCatalog) { this.aliases = aliases; + this.bidderCatalog = bidderCatalog; } - public static VendorIdResolver of(BidderAliases aliases) { - return new VendorIdResolver(aliases); + public static VendorIdResolver of(BidderAliases aliases, BidderCatalog bidderCatalog) { + return new VendorIdResolver(aliases, bidderCatalog); } public static VendorIdResolver of(BidderCatalog bidderCatalog) { - return of(BidderAliases.of(bidderCatalog)); + return of(null, bidderCatalog); } public Integer resolve(String aliasOrBidder) { - return aliases.resolveAliasVendorId(aliasOrBidder); + final Integer requestAliasVendorId = aliases != null ? aliases.resolveAliasVendorId(aliasOrBidder) : null; + + return requestAliasVendorId != null ? requestAliasVendorId : resolveViaCatalog(aliasOrBidder); + } + + private Integer resolveViaCatalog(String aliasOrBidder) { + final String bidderName = aliases != null ? aliases.resolveBidder(aliasOrBidder) : aliasOrBidder; + + return bidderCatalog.isActive(bidderName) ? bidderCatalog.vendorIdByName(bidderName) : null; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/model/HostVendorTcfResponse.java b/src/main/java/org/prebid/server/privacy/gdpr/model/HostVendorTcfResponse.java new file mode 100644 index 00000000000..6986bd0769f --- /dev/null +++ b/src/main/java/org/prebid/server/privacy/gdpr/model/HostVendorTcfResponse.java @@ -0,0 +1,19 @@ +package org.prebid.server.privacy.gdpr.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class HostVendorTcfResponse { + + private static final HostVendorTcfResponse ALLOWED_VENDOR = HostVendorTcfResponse.of(null, null, true); + + Boolean userInGdprScope; + + String country; + + boolean isVendorAllowed; + + public static HostVendorTcfResponse allowedVendor() { + return ALLOWED_VENDOR; + } +} diff --git a/src/main/java/org/prebid/server/privacy/gdpr/model/PrivacyEnforcementAction.java b/src/main/java/org/prebid/server/privacy/gdpr/model/PrivacyEnforcementAction.java index 4da89cc9a48..a5af3d00715 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/model/PrivacyEnforcementAction.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/model/PrivacyEnforcementAction.java @@ -7,7 +7,7 @@ @Data public class PrivacyEnforcementAction { - boolean removeUserIds; // user.buyeruid, user.id, user.ext.eids, user.ext.digitrust + boolean removeUserIds; // user.buyeruid, user.id, user.ext.eids boolean maskGeo; // user.geo, device.geo diff --git a/src/main/java/org/prebid/server/privacy/gdpr/model/RequestLogInfo.java b/src/main/java/org/prebid/server/privacy/gdpr/model/RequestLogInfo.java new file mode 100644 index 00000000000..55eb248b739 --- /dev/null +++ b/src/main/java/org/prebid/server/privacy/gdpr/model/RequestLogInfo.java @@ -0,0 +1,14 @@ +package org.prebid.server.privacy.gdpr.model; + +import lombok.Value; +import org.prebid.server.metric.MetricName; + +@Value(staticConstructor = "of") +public class RequestLogInfo { + + MetricName requestType; + + String refUrl; + + String accountId; +} diff --git a/src/main/java/org/prebid/server/privacy/gdpr/model/TcfContext.java b/src/main/java/org/prebid/server/privacy/gdpr/model/TcfContext.java index bd4a8ec6618..8b58ddde302 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/model/TcfContext.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/model/TcfContext.java @@ -18,6 +18,8 @@ public class TcfContext { TCString consent; + Boolean isConsentValid; + GeoInfo geoInfo; Boolean inEea; diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategy.java index b573607a877..416acfe78c8 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategy.java @@ -4,14 +4,14 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; public class PurposeEightStrategy extends PurposeStrategy { - private static final int PURPOSE_ID = 8; - public PurposeEightStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy); } @@ -26,8 +26,8 @@ public void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction) { } @Override - public int getPurposeId() { - return PURPOSE_ID; + public PurposeCode getPurpose() { + return PurposeCode.EIGHT; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategy.java index 67ec5bb7c38..89266044c1b 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategy.java @@ -4,14 +4,14 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; public class PurposeFiveStrategy extends PurposeStrategy { - private static final int PURPOSE_ID = 5; - public PurposeFiveStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy); } @@ -26,8 +26,8 @@ public void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction) { } @Override - public int getPurposeId() { - return PURPOSE_ID; + public PurposeCode getPurpose() { + return PurposeCode.FIVE; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategy.java index 1e1882e98c3..6f5c5036f40 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategy.java @@ -4,14 +4,14 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; public class PurposeFourStrategy extends PurposeStrategy { - private static final int PURPOSE_ID = 4; - public PurposeFourStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy); } @@ -26,8 +26,8 @@ public void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction) { } @Override - public int getPurposeId() { - return PURPOSE_ID; + public PurposeCode getPurpose() { + return PurposeCode.FOUR; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategy.java index 9229256ea61..fc8922112b5 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategy.java @@ -4,14 +4,14 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; public class PurposeNineStrategy extends PurposeStrategy { - private static final int PURPOSE_ID = 9; - public PurposeNineStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy); } @@ -26,8 +26,8 @@ public void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction) { } @Override - public int getPurposeId() { - return PURPOSE_ID; + public PurposeCode getPurpose() { + return PurposeCode.NINE; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategy.java index eeea6cedeec..a4eb920e776 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategy.java @@ -4,14 +4,14 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; public class PurposeOneStrategy extends PurposeStrategy { - private static final int PURPOSE_ID = 1; - public PurposeOneStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy); } @@ -25,8 +25,8 @@ public void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction) { } @Override - public int getPurposeId() { - return PURPOSE_ID; + public PurposeCode getPurpose() { + return PurposeCode.ONE; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategy.java index 27693a5d343..c28c63f6311 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategy.java @@ -4,14 +4,14 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; public class PurposeSevenStrategy extends PurposeStrategy { - private static final int PURPOSE_ID = 7; - public PurposeSevenStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy); } @@ -27,8 +27,8 @@ public void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction) { } @Override - public int getPurposeId() { - return PURPOSE_ID; + public PurposeCode getPurpose() { + return PurposeCode.SEVEN; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategy.java index 475d71aad5c..de332a75dd3 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategy.java @@ -4,14 +4,14 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; public class PurposeSixStrategy extends PurposeStrategy { - private static final int PURPOSE_ID = 6; - public PurposeSixStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy); } @@ -26,8 +26,8 @@ public void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction) { } @Override - public int getPurposeId() { - return PURPOSE_ID; + public PurposeCode getPurpose() { + return PurposeCode.SIX; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java index 0c7f023b030..4b7b111a1bc 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java @@ -9,6 +9,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -20,19 +21,20 @@ public abstract class PurposeStrategy { - private FullEnforcePurposeStrategy fullEnforcePurposeStrategy; - private BasicEnforcePurposeStrategy basicEnforcePurposeStrategy; - private NoEnforcePurposeStrategy noEnforcePurposeStrategy; + private final FullEnforcePurposeStrategy fullEnforcePurposeStrategy; + private final BasicEnforcePurposeStrategy basicEnforcePurposeStrategy; + private final NoEnforcePurposeStrategy noEnforcePurposeStrategy; public PurposeStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + this.fullEnforcePurposeStrategy = fullEnforcePurposeStrategy; this.basicEnforcePurposeStrategy = basicEnforcePurposeStrategy; this.noEnforcePurposeStrategy = noEnforcePurposeStrategy; } - public abstract int getPurposeId(); + public abstract PurposeCode getPurpose(); /** * This method is allow permission for purpose when account and server config was used. @@ -41,21 +43,28 @@ public PurposeStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, /** * This method represents allowance of permission that purpose should provide after full enforcement - * (can downgrade to basic if GCL failed) despite of host company or account configuration. + * (can downgrade to basic if GVL failed) despite of host company or account configuration. */ public abstract void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction); public Collection processTypePurposeStrategy( - TCString vendorConsent, Purpose purpose, Collection vendorPermissions, + TCString vendorConsent, + Purpose purpose, + Collection vendorPermissions, boolean wasDowngraded) { - allowedByTypeStrategy(vendorConsent, purpose, vendorPermissions).stream() + final Collection excludedVendors = excludedVendors(vendorPermissions, purpose); + final Collection vendorForPurpose = vendorPermissions.stream() + .filter(vendorPermission -> !excludedVendors.contains(vendorPermission)) + .collect(Collectors.toList()); + + allowedByTypeStrategy(vendorConsent, purpose, vendorForPurpose, excludedVendors).stream() .map(VendorPermission::getPrivacyEnforcementAction) .forEach(this::allow); final Collection naturalVendorPermission = wasDowngraded - ? allowedByBasicTypeStrategy(vendorConsent, true, vendorPermissions, Collections.emptyList()) - : allowedByFullTypeStrategy(vendorConsent, true, vendorPermissions, Collections.emptyList()); + ? allowedByBasicTypeStrategy(vendorConsent, true, vendorForPurpose, excludedVendors) + : allowedByFullTypeStrategy(vendorConsent, true, vendorForPurpose, excludedVendors); naturalVendorPermission.stream() .map(VendorPermission::getPrivacyEnforcementAction) @@ -68,11 +77,8 @@ public Collection processTypePurposeStrategy( private Collection allowedByTypeStrategy(TCString vendorConsent, Purpose purpose, - Collection vendorPermissions) { - final Collection excludedVendors = excludedVendors(vendorPermissions, purpose); - final Collection vendorForPurpose = vendorPermissions.stream() - .filter(vendorPermission -> !excludedVendors.contains(vendorPermission)) - .collect(Collectors.toList()); + Collection vendorForPurpose, + Collection excludedVendors) { final boolean isEnforceVendors = BooleanUtils.isNotFalse(purpose.getEnforceVendors()); final EnforcePurpose purposeType = purpose.getEnforcePurpose(); @@ -95,36 +101,43 @@ private Collection allowedByTypeStrategy(TCString vendorConsen protected Collection excludedVendors(Collection vendorPermissions, Purpose purpose) { + final List bidderNameExceptions = purpose.getVendorExceptions(); + return CollectionUtils.isEmpty(bidderNameExceptions) ? Collections.emptyList() : CollectionUtils.select(vendorPermissions, vendorPermission -> - bidderNameExceptions.contains(vendorPermission.getVendorPermission().getBidderName())); + bidderNameExceptions.contains(vendorPermission.getVendorPermission().getBidderName())); } protected Collection allowedByBasicTypeStrategy( - TCString vendorConsent, boolean isEnforceVendors, Collection vendorForPurpose, + TCString vendorConsent, + boolean isEnforceVendors, + Collection vendorForPurpose, Collection excludedVendors) { - return basicEnforcePurposeStrategy.allowedByTypeStrategy(getPurposeId(), vendorConsent, vendorForPurpose, - excludedVendors, isEnforceVendors); + return basicEnforcePurposeStrategy.allowedByTypeStrategy( + getPurpose(), vendorConsent, vendorForPurpose, excludedVendors, isEnforceVendors); } protected Collection allowedByNoTypeStrategy( - TCString vendorConsent, boolean isEnforceVendors, Collection vendorForPurpose, + TCString vendorConsent, + boolean isEnforceVendors, + Collection vendorForPurpose, Collection excludedVendors) { - return noEnforcePurposeStrategy.allowedByTypeStrategy(getPurposeId(), vendorConsent, vendorForPurpose, - excludedVendors, isEnforceVendors); + return noEnforcePurposeStrategy.allowedByTypeStrategy( + getPurpose(), vendorConsent, vendorForPurpose, excludedVendors, isEnforceVendors); } protected Collection allowedByFullTypeStrategy( - TCString vendorConsent, boolean isEnforceVendors, Collection vendorForPurpose, + TCString vendorConsent, + boolean isEnforceVendors, + Collection vendorForPurpose, Collection excludedVendors) { - return fullEnforcePurposeStrategy.allowedByTypeStrategy(getPurposeId(), vendorConsent, vendorForPurpose, - excludedVendors, isEnforceVendors); - + return fullEnforcePurposeStrategy.allowedByTypeStrategy( + getPurpose(), vendorConsent, vendorForPurpose, excludedVendors, isEnforceVendors); } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategy.java index 530f6f36d9f..834f5de800b 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategy.java @@ -4,14 +4,14 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; public class PurposeTenStrategy extends PurposeStrategy { - private static final int PURPOSE_ID = 10; - public PurposeTenStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy); } @@ -26,8 +26,8 @@ public void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction) { } @Override - public int getPurposeId() { - return PURPOSE_ID; + public PurposeCode getPurpose() { + return PurposeCode.TEN; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategy.java index c26a8c44dcf..324b3e4cf92 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategy.java @@ -4,14 +4,14 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; public class PurposeThreeStrategy extends PurposeStrategy { - private static final int PURPOSE_ID = 3; - public PurposeThreeStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy); } @@ -26,8 +26,8 @@ public void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction) { } @Override - public int getPurposeId() { - return PURPOSE_ID; + public PurposeCode getPurpose() { + return PurposeCode.THREE; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategy.java index 1a6887cf0d2..0268713310e 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategy.java @@ -4,14 +4,14 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; public class PurposeTwoStrategy extends PurposeStrategy { - private static final int PURPOSE_ID = 2; - public PurposeTwoStrategy(FullEnforcePurposeStrategy fullEnforcePurposeStrategy, BasicEnforcePurposeStrategy basicEnforcePurposeStrategy, NoEnforcePurposeStrategy noEnforcePurposeStrategy) { + super(fullEnforcePurposeStrategy, basicEnforcePurposeStrategy, noEnforcePurposeStrategy); } @@ -27,8 +27,8 @@ public void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction) { } @Override - public int getPurposeId() { - return PURPOSE_ID; + public PurposeCode getPurpose() { + return PurposeCode.TWO; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategy.java index 6f256d25919..4f07e5fe737 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategy.java @@ -6,6 +6,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import java.util.Collection; import java.util.List; @@ -15,18 +16,19 @@ public class BasicEnforcePurposeStrategy extends EnforcePurposeStrategy { private static final Logger logger = LoggerFactory.getLogger(BasicEnforcePurposeStrategy.class); - public Collection allowedByTypeStrategy(int purposeId, + public Collection allowedByTypeStrategy(PurposeCode purpose, TCString vendorConsent, Collection vendorsForPurpose, Collection excludedVendors, boolean isEnforceVendors) { - logger.debug("Basic strategy used fo purpose {0}", purposeId); + logger.debug("Basic strategy used for purpose {0}", purpose); + final List allowedVendorPermissions = vendorsForPurpose.stream() .map(VendorPermissionWithGvl::getVendorPermission) .filter(vendorPermission -> vendorPermission.getVendorId() != null) - .filter(vendorPermission -> isAllowedBySimpleConsent(purposeId, - vendorPermission.getVendorId(), isEnforceVendors, vendorConsent)) + .filter(vendorPermission -> isAllowedBySimpleConsent( + purpose, vendorPermission.getVendorId(), isEnforceVendors, vendorConsent)) .collect(Collectors.toList()); return CollectionUtils.union(allowedVendorPermissions, toVendorPermissions(excludedVendors)); diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java index 4734685e68d..e3e596aa6e1 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/EnforcePurposeStrategy.java @@ -4,6 +4,7 @@ import com.iabtcf.utils.IntIterable; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import java.util.Collection; import java.util.stream.Collectors; @@ -11,42 +12,50 @@ public abstract class EnforcePurposeStrategy { public abstract Collection allowedByTypeStrategy( - int purposeId, TCString vendorConsent, Collection vendorsForPurpose, - Collection excludedVendors, boolean isEnforceVendors); + PurposeCode purpose, + TCString vendorConsent, + Collection vendorsForPurpose, + Collection excludedVendors, + boolean isEnforceVendors); - protected boolean isAllowedBySimpleConsentOrLegitimateInterest(int purposeId, + protected boolean isAllowedBySimpleConsentOrLegitimateInterest(PurposeCode purpose, Integer vendorId, boolean isEnforceVendor, TCString tcString) { - return isAllowedBySimpleConsent(purposeId, vendorId, isEnforceVendor, tcString) - || isAllowedByLegitimateInterest(purposeId, vendorId, isEnforceVendor, tcString); + return isAllowedBySimpleConsent(purpose, vendorId, isEnforceVendor, tcString) + || isAllowedByLegitimateInterest(purpose, vendorId, isEnforceVendor, tcString); } - protected boolean isAllowedBySimpleConsent(int purposeId, + protected boolean isAllowedBySimpleConsent(PurposeCode purpose, Integer vendorId, boolean isEnforceVendor, TCString tcString) { + final IntIterable purposesConsent = tcString.getPurposesConsent(); final IntIterable vendorConsent = tcString.getVendorConsent(); - return isAllowedByConsents(purposeId, vendorId, isEnforceVendor, purposesConsent, vendorConsent); + + return isAllowedByConsents(purpose, vendorId, isEnforceVendor, purposesConsent, vendorConsent); } - protected boolean isAllowedByLegitimateInterest(int purposeId, + protected boolean isAllowedByLegitimateInterest(PurposeCode purpose, Integer vendorId, boolean isEnforceVendor, TCString tcString) { + final IntIterable purposesConsent = tcString.getPurposesLITransparency(); final IntIterable vendorConsent = tcString.getVendorLegitimateInterest(); - return isAllowedByConsents(purposeId, vendorId, isEnforceVendor, purposesConsent, vendorConsent); + + return isAllowedByConsents(purpose, vendorId, isEnforceVendor, purposesConsent, vendorConsent); } - private boolean isAllowedByConsents(int purposeId, + private boolean isAllowedByConsents(PurposeCode purpose, Integer vendorId, boolean isEnforceVendors, IntIterable purposesConsent, IntIterable vendorConsent) { - final boolean isPurposeAllowed = purposesConsent.contains(purposeId); + + final boolean isPurposeAllowed = purposesConsent.contains(purpose.code()); final boolean isVendorAllowed = !isEnforceVendors || vendorConsent.contains(vendorId); return isPurposeAllowed && isVendorAllowed; } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/FullEnforcePurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/FullEnforcePurposeStrategy.java index 598c8c21b07..0e5aae4e0b4 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/FullEnforcePurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/FullEnforcePurposeStrategy.java @@ -6,26 +6,27 @@ import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import java.util.Collection; +import java.util.EnumSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.StreamSupport; public class FullEnforcePurposeStrategy extends EnforcePurposeStrategy { - public Collection allowedByTypeStrategy(int purposeId, + public Collection allowedByTypeStrategy(PurposeCode purpose, TCString vendorConsent, Collection vendorsForPurpose, Collection excludedVendors, boolean isEnforceVendors) { final List publisherRestrictions = vendorConsent.getPublisherRestrictions().stream() - .filter(publisherRestriction -> publisherRestriction.getPurposeId() == purposeId) + .filter(publisherRestriction -> publisherRestriction.getPurposeId() == purpose.code()) .collect(Collectors.toList()); final List allowedExcluded = allowedExcludedVendorPermission(excludedVendors, @@ -36,7 +37,7 @@ public Collection allowedByTypeStrategy(int purposeId, final List allowedVendorPermissions = vendorPermissionToRestriction.entrySet().stream() .filter(permissionAndRestriction -> - isAllowedByPublisherRestrictionAndFlexible(purposeId, isEnforceVendors, + isAllowedByPublisherRestrictionAndFlexible(purpose, isEnforceVendors, permissionAndRestriction.getKey(), vendorConsent, permissionAndRestriction.getValue())) .map(Map.Entry::getKey) .map(VendorPermissionWithGvl::getVendorPermission) @@ -79,6 +80,7 @@ private Map mapVendorPermission( private RestrictionType restrictionType(VendorPermissionWithGvl vendorPermissionWithGvl, Collection publisherRestrictions) { + final VendorPermission vendorPermission = vendorPermissionWithGvl.getVendorPermission(); final Integer vendorId = vendorPermission.getVendorId(); @@ -90,23 +92,26 @@ private RestrictionType restrictionType(VendorPermissionWithGvl vendorPermission } /** - * Purpose is flexible when {@link VendorV2} flexiblePurposes contains it id. + * Purpose is flexible when {@link VendorV2} flexiblePurposes contains it. * When it is not flexible: - * We check purposeConsent and vendorConsent when it is contained in GVL purposes; - * We check purposesLITransparency and vendorLegitimateInterest when it is contained in GVL LegIntPurposes. + *

  • When it is contained in GVL purposes we reject REQUIRE_LEGITIMATE_INTEREST {@link RestrictionType} + * and check purposeConsent and vendorConsent;
  • + *
  • When it is contained in GVL LegIntPurposes we reject REQUIRE_CONSENT {@link RestrictionType} + * and check purposesLITransparency and vendorLegitimateInterest.
  • *


    - * If it flexible we check by {@link RestrictionType}: + * If it is flexible we check by {@link RestrictionType}: *

  • For REQUIRE_CONSENT we check by purposeConsent and vendorConsent
  • *
  • For REQUIRE_LEGITIMATE_INTEREST we check by purposesLITransparency and vendorLegitimateInterest
  • *
  • For UNDEFINED we check by purposeConsent and vendorConsent * or purposesLITransparency and vendorLegitimateInterest
  • *

    */ - private boolean isAllowedByPublisherRestrictionAndFlexible(int purposeId, + private boolean isAllowedByPublisherRestrictionAndFlexible(PurposeCode purpose, boolean isEnforceVendor, VendorPermissionWithGvl vendorPermissionWithGvl, TCString tcString, RestrictionType restrictionType) { + if (restrictionType.equals(RestrictionType.NOT_ALLOWED)) { return false; } @@ -114,37 +119,62 @@ private boolean isAllowedByPublisherRestrictionAndFlexible(int purposeId, final Integer vendorId = vendorPermissionWithGvl.getVendorPermission().getVendorId(); final VendorV2 vendorGvl = vendorPermissionWithGvl.getVendorV2(); - final Set flexiblePurposes = vendorGvl.getFlexiblePurposes(); - final boolean isFlexible = CollectionUtils.isNotEmpty(flexiblePurposes) && flexiblePurposes.contains(purposeId); + final EnumSet flexiblePurposes = vendorGvl.getFlexiblePurposes(); + final boolean isFlexible = CollectionUtils.isNotEmpty(flexiblePurposes) && flexiblePurposes.contains(purpose); - final Set gvlPurposes = vendorGvl.getPurposes(); - if (gvlPurposes != null && gvlPurposes.contains(purposeId)) { + final EnumSet gvlPurposeCodes = vendorGvl.getPurposes(); + if (gvlPurposeCodes != null && gvlPurposeCodes.contains(purpose)) { return isFlexible - ? isAllowedByRestrictionTypePurpose(purposeId, vendorId, isEnforceVendor, tcString, restrictionType) - : isAllowedBySimpleConsent(purposeId, vendorId, isEnforceVendor, tcString); + ? isAllowedByFlexible(purpose, vendorId, isEnforceVendor, tcString, restrictionType) + : isAllowedByNotFlexiblePurpose(purpose, vendorId, isEnforceVendor, tcString, restrictionType); } - final Set legIntGvlPurposes = vendorGvl.getLegIntPurposes(); - if (legIntGvlPurposes != null && legIntGvlPurposes.contains(purposeId)) { + final EnumSet legIntGvlPurposeCodes = vendorGvl.getLegIntPurposes(); + if (legIntGvlPurposeCodes != null && legIntGvlPurposeCodes.contains(purpose)) { return isFlexible - ? isAllowedByRestrictionTypePurpose(purposeId, vendorId, isEnforceVendor, tcString, restrictionType) - : isAllowedByLegitimateInterest(purposeId, vendorId, isEnforceVendor, tcString); + ? isAllowedByFlexible(purpose, vendorId, isEnforceVendor, tcString, restrictionType) + : isAllowedByNotFlexibleLegitimateInterest(purpose, vendorId, isEnforceVendor, tcString, + restrictionType); } + return false; } - private boolean isAllowedByRestrictionTypePurpose(int purposeId, - Integer vendorId, - boolean isEnforceVendor, - TCString tcString, - RestrictionType restrictionType) { + private boolean isAllowedByNotFlexiblePurpose(PurposeCode purpose, + Integer vendorId, + boolean isEnforceVendor, + TCString tcString, + RestrictionType restrictionType) { + final boolean isSupportedRestriction = restrictionType.equals(RestrictionType.REQUIRE_CONSENT) + || restrictionType.equals(RestrictionType.UNDEFINED); + + return isSupportedRestriction && isAllowedBySimpleConsent(purpose, vendorId, isEnforceVendor, tcString); + } + + private boolean isAllowedByNotFlexibleLegitimateInterest(PurposeCode purpose, + Integer vendorId, + boolean isEnforceVendor, + TCString tcString, + RestrictionType restrictionType) { + final boolean isSupportedRestriction = restrictionType.equals(RestrictionType.REQUIRE_LEGITIMATE_INTEREST) + || restrictionType.equals(RestrictionType.UNDEFINED); + + return isSupportedRestriction && isAllowedByLegitimateInterest(purpose, vendorId, isEnforceVendor, tcString); + } + + private boolean isAllowedByFlexible(PurposeCode purpose, + Integer vendorId, + boolean isEnforceVendor, + TCString tcString, + RestrictionType restrictionType) { + switch (restrictionType) { case REQUIRE_CONSENT: - return isAllowedBySimpleConsent(purposeId, vendorId, isEnforceVendor, tcString); + return isAllowedBySimpleConsent(purpose, vendorId, isEnforceVendor, tcString); case REQUIRE_LEGITIMATE_INTEREST: - return isAllowedByLegitimateInterest(purposeId, vendorId, isEnforceVendor, tcString); + return isAllowedByLegitimateInterest(purpose, vendorId, isEnforceVendor, tcString); case UNDEFINED: - return isAllowedBySimpleConsentOrLegitimateInterest(purposeId, vendorId, isEnforceVendor, tcString); + return isAllowedBySimpleConsentOrLegitimateInterest(purpose, vendorId, isEnforceVendor, tcString); default: return false; } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategy.java index 81465601924..1e7c247744d 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategy.java @@ -5,6 +5,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import java.util.Collection; import java.util.List; @@ -12,7 +13,7 @@ public class NoEnforcePurposeStrategy extends EnforcePurposeStrategy { - public Collection allowedByTypeStrategy(int purposeId, + public Collection allowedByTypeStrategy(PurposeCode purpose, TCString tcString, Collection vendorsForPurpose, Collection excludedVendors, @@ -35,6 +36,7 @@ private boolean isAllowedByVendorConsent(Integer vendorId, boolean isEnforceVendors, IntIterable vendorConsent, IntIterable vendorLIConsent) { + return !isEnforceVendors || vendorConsent.contains(vendorId) || vendorLIConsent.contains(vendorId); } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/PurposeTwoBasicEnforcePurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/PurposeTwoBasicEnforcePurposeStrategy.java index 2aac34737f1..c3870047270 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/PurposeTwoBasicEnforcePurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/PurposeTwoBasicEnforcePurposeStrategy.java @@ -7,6 +7,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import java.util.Collection; import java.util.List; @@ -16,30 +17,33 @@ public class PurposeTwoBasicEnforcePurposeStrategy extends BasicEnforcePurposeSt private static final Logger logger = LoggerFactory.getLogger(PurposeTwoBasicEnforcePurposeStrategy.class); - public Collection allowedByTypeStrategy(int purposeId, + public Collection allowedByTypeStrategy(PurposeCode purpose, TCString vendorConsent, Collection vendorsForPurpose, Collection excludedVendors, boolean isEnforceVendors) { - logger.debug("Basic strategy used fo purpose {0}", purposeId); + logger.debug("Basic strategy used fo purpose {0}", purpose); + final List allowedVendorPermissions = vendorsForPurpose.stream() .map(VendorPermissionWithGvl::getVendorPermission) .filter(vendorPermission -> vendorPermission.getVendorId() != null) - .filter(vendorPermission -> isAllowedBySimpleConsentOrPurposeLI(purposeId, + .filter(vendorPermission -> isAllowedBySimpleConsentOrPurposeLI(purpose, vendorPermission.getVendorId(), isEnforceVendors, vendorConsent)) .collect(Collectors.toList()); return CollectionUtils.union(allowedVendorPermissions, toVendorPermissions(excludedVendors)); } - private boolean isAllowedBySimpleConsentOrPurposeLI(int purposeId, + private boolean isAllowedBySimpleConsentOrPurposeLI(PurposeCode purpose, Integer vendorId, boolean isEnforceVendor, TCString tcString) { + final IntIterable purposesLIConsent = tcString.getPurposesLITransparency(); - return isAllowedBySimpleConsent(purposeId, vendorId, isEnforceVendor, tcString) - || purposesLIConsent.contains(purposeId); + + return isAllowedBySimpleConsent(purpose, vendorId, isEnforceVendor, tcString) + || purposesLIConsent.contains(purpose.code()); } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListService.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListService.java index 7427c6884fa..764066e1379 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListService.java @@ -29,7 +29,6 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -50,12 +49,12 @@ public abstract class VendorListService { private static final String JSON_SUFFIX = ".json"; private static final String VERSION_PLACEHOLDER = "{VERSION}"; - private static final Integer EXPIRE_DAY_CACHE_DURATION = 5; private final String cacheDir; private final String endpointTemplate; private final int defaultTimeoutMs; private final long refreshMissingListPeriodMs; + private final boolean deprecated; private final Vertx vertx; private final FileSystem fileSystem; private final HttpClient httpClient; @@ -77,6 +76,7 @@ public VendorListService(String cacheDir, String endpointTemplate, int defaultTimeoutMs, long refreshMissingListPeriodMs, + boolean deprecated, Integer gdprHostVendorId, String fallbackVendorListPath, BidderCatalog bidderCatalog, @@ -90,6 +90,7 @@ public VendorListService(String cacheDir, this.endpointTemplate = Objects.requireNonNull(endpointTemplate); this.defaultTimeoutMs = defaultTimeoutMs; this.refreshMissingListPeriodMs = refreshMissingListPeriodMs; + this.deprecated = deprecated; this.vertx = Objects.requireNonNull(vertx); this.fileSystem = Objects.requireNonNull(fileSystem); this.httpClient = Objects.requireNonNull(httpClient); @@ -101,8 +102,19 @@ public VendorListService(String cacheDir, createAndCheckWritePermissionsFor(fileSystem, cacheDir); cache = Objects.requireNonNull(createCache(fileSystem, cacheDir)); - fallbackVendorList = readFallbackVendorList(fallbackVendorListPath); - versionsToFallback = fallbackVendorList != null ? ConcurrentHashMap.newKeySet() : null; + fallbackVendorList = StringUtils.isNotBlank(fallbackVendorListPath) + ? readFallbackVendorList(fallbackVendorListPath) : null; + if (deprecated) { + validateFallbackVendorListIfDeprecatedVersion(); + } + versionsToFallback = fallbackVendorList != null + ? ConcurrentHashMap.newKeySet() : null; + } + + private void validateFallbackVendorListIfDeprecatedVersion() { + if (Objects.isNull(fallbackVendorList)) { + throw new PreBidException("No fallback vendorList for deprecated version present"); + } } /** @@ -190,7 +202,6 @@ private Map> createCache(FileSystem fileSystem, String final Map versionToFileContent = readFileSystemCache(fileSystem, cacheDir); final Map> cache = Caffeine.newBuilder() - .expireAfterWrite(EXPIRE_DAY_CACHE_DURATION, TimeUnit.DAYS) .>build() .asMap(); @@ -215,10 +226,6 @@ private Map readFileSystemCache(FileSystem fileSystem, String di } private Map readFallbackVendorList(String fallbackVendorListPath) { - if (StringUtils.isBlank(fallbackVendorListPath)) { - return null; - } - final String vendorListContent = fileSystem.readFileBlocking(fallbackVendorListPath).toString(); final T vendorList = toVendorList(vendorListContent); if (!isValid(vendorList)) { @@ -230,7 +237,7 @@ private Map readFallbackVendorList(String fallbackVendorListPath) { } private boolean shouldFallback(int version) { - return versionsToFallback != null && versionsToFallback.contains(version); + return deprecated || (versionsToFallback != null && versionsToFallback.contains(version)); } /** diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV1.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV1.java deleted file mode 100644 index e4fd673c54c..00000000000 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV1.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.prebid.server.privacy.gdpr.vendorlist; - -import io.vertx.core.Vertx; -import io.vertx.core.file.FileSystem; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.metric.Metrics; -import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorListV1; -import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV1; -import org.prebid.server.vertx.http.HttpClient; - -import java.io.IOException; -import java.util.Collection; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -public class VendorListServiceV1 extends VendorListService { - - private static final Logger logger = LoggerFactory.getLogger(VendorListServiceV1.class); - - private static final int TCF_VERSION = 1; - - public VendorListServiceV1(String cacheDir, - String endpointTemplate, - int defaultTimeoutMs, - long refreshMissingListPeriodMs, - Integer gdprHostVendorId, - String fallbackVendorListPath, - BidderCatalog bidderCatalog, - Vertx vertx, - FileSystem fileSystem, - HttpClient httpClient, - Metrics metrics, - JacksonMapper mapper) { - - super( - cacheDir, - endpointTemplate, - defaultTimeoutMs, - refreshMissingListPeriodMs, - gdprHostVendorId, - fallbackVendorListPath, - bidderCatalog, - vertx, - fileSystem, - httpClient, - metrics, - mapper); - } - - protected VendorListV1 toVendorList(String content) { - try { - return mapper.mapper().readValue(content, VendorListV1.class); - } catch (IOException e) { - final String message = String.format("Cannot parse vendor list from: %s", content); - - logger.error(message, e); - throw new PreBidException(message, e); - } - } - - protected Map filterVendorIdToVendors(VendorListV1 vendorList) { - return vendorList.getVendors().stream() - .filter(vendor -> knownVendorIds.contains(vendor.getId())) // optimize cache to use only known vendors - .collect(Collectors.toMap(VendorV1::getId, Function.identity())); - } - - protected boolean isValid(VendorListV1 vendorList) { - return vendorList.getVendorListVersion() != null - && vendorList.getLastUpdated() != null - && CollectionUtils.isNotEmpty(vendorList.getVendors()) - && isValidVendors(vendorList.getVendors()); - } - - @Override - protected int getTcfVersion() { - return TCF_VERSION; - } - - private static boolean isValidVendors(Collection vendors) { - return vendors.stream() - .allMatch(vendor -> vendor != null - && vendor.getId() != null - && vendor.getPurposeIds() != null - && vendor.getLegIntPurposeIds() != null); - } -} diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV2.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV2.java index 9a1ae78ba3d..ec0207a0140 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV2.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV2.java @@ -28,6 +28,7 @@ public VendorListServiceV2(String cacheDir, String endpointTemplate, int defaultTimeoutMs, long refreshMissingListPeriodMs, + boolean deprecated, Integer gdprHostVendorId, String fallbackVendorListPath, BidderCatalog bidderCatalog, @@ -42,6 +43,7 @@ public VendorListServiceV2(String cacheDir, endpointTemplate, defaultTimeoutMs, refreshMissingListPeriodMs, + deprecated, gdprHostVendorId, fallbackVendorListPath, bidderCatalog, diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/Feature.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/Feature.java new file mode 100644 index 00000000000..a6829a19511 --- /dev/null +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/Feature.java @@ -0,0 +1,27 @@ +package org.prebid.server.privacy.gdpr.vendorlist.proto; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import java.util.Arrays; + +public enum Feature { + + ONE(1), + TWO(2), + THREE(3), + UNKNOWN(0); + + private final int code; + + Feature(int code) { + this.code = code; + } + + @JsonCreator + public static Feature valueOf(int code) { + return Arrays.stream(values()) + .filter(purpose -> purpose.code == code) + .findFirst() + .orElse(UNKNOWN); + } +} diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/PurposeCode.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/PurposeCode.java new file mode 100644 index 00000000000..c6d08462045 --- /dev/null +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/PurposeCode.java @@ -0,0 +1,40 @@ +package org.prebid.server.privacy.gdpr.vendorlist.proto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.Arrays; + +public enum PurposeCode { + + ONE(1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8), + NINE(9), + TEN(10), + UNKNOWN(0); + + @JsonValue + private final int code; + + PurposeCode(int code) { + this.code = code; + } + + public int code() { + return code; + } + + @JsonCreator + public static PurposeCode valueOf(int code) { + return Arrays.stream(values()) + .filter(purpose -> purpose.code == code) + .findFirst() + .orElse(UNKNOWN); + } +} diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/SpecialFeature.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/SpecialFeature.java new file mode 100644 index 00000000000..8371ebc383b --- /dev/null +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/SpecialFeature.java @@ -0,0 +1,26 @@ +package org.prebid.server.privacy.gdpr.vendorlist.proto; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import java.util.Arrays; + +public enum SpecialFeature { + + ONE(1), + TWO(2), + UNKNOWN(0); + + private final int code; + + SpecialFeature(int code) { + this.code = code; + } + + @JsonCreator + public static SpecialFeature valueOf(int code) { + return Arrays.stream(values()) + .filter(purpose -> purpose.code == code) + .findFirst() + .orElse(UNKNOWN); + } +} diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/SpecialPurpose.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/SpecialPurpose.java new file mode 100644 index 00000000000..66d77b53764 --- /dev/null +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/SpecialPurpose.java @@ -0,0 +1,26 @@ +package org.prebid.server.privacy.gdpr.vendorlist.proto; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import java.util.Arrays; + +public enum SpecialPurpose { + + ONE(1), + TWO(2), + UNKNOWN(0); + + private final int code; + + SpecialPurpose(int code) { + this.code = code; + } + + @JsonCreator + public static SpecialPurpose valueOf(int code) { + return Arrays.stream(values()) + .filter(purpose -> purpose.code == code) + .findFirst() + .orElse(UNKNOWN); + } +} diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorListV1.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorListV1.java deleted file mode 100644 index 270fd6c8b2c..00000000000 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorListV1.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.prebid.server.privacy.gdpr.vendorlist.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.Date; -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value -public class VendorListV1 { - - @JsonProperty("vendorListVersion") - Integer vendorListVersion; - - @JsonProperty("lastUpdated") - Date lastUpdated; - - List vendors; -} diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorV1.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorV1.java deleted file mode 100644 index aa960997936..00000000000 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorV1.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.prebid.server.privacy.gdpr.vendorlist.proto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.Collection; -import java.util.Collections; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@AllArgsConstructor(staticName = "of") -@Value -public class VendorV1 { - - Integer id; - - @JsonProperty("purposeIds") - Set purposeIds; - - @JsonProperty("legIntPurposeIds") - Set legIntPurposeIds; - - public Set combinedPurposes() { - return Stream.of(purposeIds != null ? purposeIds : Collections.emptySet(), - legIntPurposeIds != null ? legIntPurposeIds : Collections.emptySet()) - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - } -} diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorV2.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorV2.java index 8c155e2e35e..364889444d2 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorV2.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorV2.java @@ -6,8 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.util.Collections; -import java.util.Set; +import java.util.EnumSet; @AllArgsConstructor @NoArgsConstructor @@ -17,32 +16,32 @@ public class VendorV2 { Integer id; - Set purposes; + EnumSet purposes; @JsonProperty("legIntPurposes") - Set legIntPurposes; + EnumSet legIntPurposes; @JsonProperty("flexiblePurposes") - Set flexiblePurposes; + EnumSet flexiblePurposes; @JsonProperty("specialPurposes") - Set specialPurposes; + EnumSet specialPurposes; @JsonProperty("features") - Set features; + EnumSet features; @JsonProperty("specialFeatures") - Set specialFeatures; + EnumSet specialFeatures; public static VendorV2 empty(Integer id) { return VendorV2.builder() .id(id) - .purposes(Collections.emptySet()) - .legIntPurposes(Collections.emptySet()) - .flexiblePurposes(Collections.emptySet()) - .specialPurposes(Collections.emptySet()) - .features(Collections.emptySet()) - .specialFeatures(Collections.emptySet()) + .purposes(EnumSet.noneOf(PurposeCode.class)) + .legIntPurposes(EnumSet.noneOf(PurposeCode.class)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .specialPurposes(EnumSet.noneOf(SpecialPurpose.class)) + .features(EnumSet.noneOf(Feature.class)) + .specialFeatures(EnumSet.noneOf(SpecialFeature.class)) .build(); } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/BidAdjustmentMediaType.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/BidAdjustmentMediaType.java new file mode 100644 index 00000000000..8c2d0351d82 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/BidAdjustmentMediaType.java @@ -0,0 +1,21 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum BidAdjustmentMediaType { + + banner, + audio, + @JsonProperty("native") + xNative, + video, + @JsonProperty("video-outstream") + video_outstream; + + @Override + public String toString() { + return this == xNative ? "native" + : this == video_outstream ? "video-outstream" + : super.toString(); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ConsentedProvidersSettings.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ConsentedProvidersSettings.java new file mode 100644 index 00000000000..dc739edc03b --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ConsentedProvidersSettings.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ConsentedProvidersSettings { + + String consentedProviders; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java index fb33d3d45bc..3a66ac2374e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java @@ -11,4 +11,9 @@ public class ExtBidderConfig { * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd */ ExtBidderConfigFpd fpd; + + /** + * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2 + */ + ExtBidderConfigOrtb ortb2; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigFpd.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigFpd.java index 7c77db16b6c..27748982011 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigFpd.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigFpd.java @@ -1,6 +1,6 @@ package org.prebid.server.proto.openrtb.ext.request; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; import lombok.Value; @@ -9,17 +9,12 @@ public class ExtBidderConfigFpd { /** - * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd.site + * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd.context */ - ObjectNode site; - - /** - * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd.app - */ - ObjectNode app; + JsonNode context; /** * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd.user */ - ObjectNode user; + JsonNode user; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java new file mode 100644 index 00000000000..d073bcd269e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java @@ -0,0 +1,25 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtBidderConfigOrtb { + + /** + * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2.site + */ + ObjectNode site; + + /** + * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2.app + */ + ObjectNode app; + + /** + * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2.user + */ + ObjectNode user; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeal.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeal.java new file mode 100644 index 00000000000..642720c3306 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeal.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidrequest.imp[i].deals[].ext.line + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtDeal { + + ExtDealLine line; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealLine.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealLine.java new file mode 100644 index 00000000000..fd8d263832a --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealLine.java @@ -0,0 +1,28 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.request.Format; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtDealLine { + + @JsonProperty("lineitemid") + String lineItemId; + + @JsonProperty("extlineitemid") + String extLineItemId; + + List sizes; + + /** + * Used to distinguish which deal belongs to bidder + *

    + * Note: should not be sent to bidder exchange! + */ + String bidder; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevice.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevice.java index 8afd91b2665..f8036cc616e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevice.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevice.java @@ -13,9 +13,11 @@ @ToString(callSuper = true) public class ExtDevice extends FlexibleExtension { + Integer atts; + ExtDevicePrebid prebid; public static ExtDevice empty() { - return of(null); + return of(null, null); } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeviceVendor.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeviceVendor.java new file mode 100644 index 00000000000..5fd48b458a0 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeviceVendor.java @@ -0,0 +1,36 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.Builder; +import lombok.Value; + +/** + * Defines the contract for bidrequest.device.ext.<vendor> + */ +@Builder +@Value +public class ExtDeviceVendor { + + public static final ExtDeviceVendor EMPTY = ExtDeviceVendor.builder().build(); + + String connspeed; + + String type; + + String osfamily; + + String os; + + String osver; + + String browser; + + String browserver; + + String make; + + String model; + + String language; + + String carrier; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtGeoVendor.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtGeoVendor.java new file mode 100644 index 00000000000..5da589b71a9 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtGeoVendor.java @@ -0,0 +1,37 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.Builder; +import lombok.Value; + +/** + * Defines the contract for bidrequest.device.geo.ext.<vendor> or bidrequest.user.geo.ext.<vendor> + */ +@Builder(toBuilder = true) +@Value +public class ExtGeoVendor { + + public static final ExtGeoVendor EMPTY = ExtGeoVendor.builder().build(); + + /** + * Continent code in two-letter format: + *

    + * af - Africa, an - Antarctica, as - Asia, eu - Europe, na - North America, oc - Australia, sa - South America. + */ + String continent; + + /** + * Country code in ISO-3166-1-alpha-2 format. + */ + String country; + + Integer region; + + /** + * Nielson DMA code (not Google). + */ + Integer metro; + + String city; + + String zip; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpContext.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpContext.java index bafd989f8c5..b3d47671cdc 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpContext.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpContext.java @@ -1,17 +1,18 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.EqualsAndHashCode; +import lombok.ToString; import lombok.Value; +import org.prebid.server.proto.openrtb.ext.FlexibleExtension; /** * Defines the contract for bidrequest.imp[i].ext.context */ @Value(staticConstructor = "of") -public class ExtImpContext { - - String keywords; - - String search; +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ExtImpContext extends FlexibleExtension { ObjectNode data; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpCriteo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpCriteo.java new file mode 100644 index 00000000000..1a4b9c065b0 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpCriteo.java @@ -0,0 +1,19 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpCriteo { + + @JsonProperty("zoneId") + @JsonAlias({"zoneid"}) + Integer zoneId; + + @JsonProperty("networkId") + @JsonAlias({"networkid"}) + Integer networkId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidadjustmentfactors.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidadjustmentfactors.java new file mode 100644 index 00000000000..105c141fc21 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidadjustmentfactors.java @@ -0,0 +1,31 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +@Value(staticConstructor = "of") +@Builder(toBuilder = true) +public class ExtRequestBidadjustmentfactors { + + Map adjustments = new HashMap<>(); + + EnumMap> mediatypes; + + @JsonAnyGetter + public Map getAdjustments() { + return Collections.unmodifiableMap(adjustments); + } + + @JsonAnySetter + public void addFactor(String key, BigDecimal value) { + adjustments.put(key, value); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java index b687825d550..221f19b1f37 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java @@ -1,10 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Builder; import lombok.Value; +import org.prebid.server.json.IntegerFlagDeserializer; -import java.math.BigDecimal; import java.util.List; import java.util.Map; @@ -18,8 +20,14 @@ public class ExtRequestPrebid { /** * Defines the contract for bidrequest.ext.prebid.debug */ + @JsonDeserialize(using = IntegerFlagDeserializer.class) Integer debug; + /** + * Defines the contract for bidrequest.ext.prebid.trace + */ + TraceLevel trace; + /** * Defines the contract for bidrequest.ext.prebid.aliases */ @@ -33,7 +41,7 @@ public class ExtRequestPrebid { /** * Defines the contract for bidrequest.ext.prebid.bidadjustmentfactors */ - Map bidadjustmentfactors; + ExtRequestBidadjustmentfactors bidadjustmentfactors; /** * Defines the contract for bidrequest.ext.prebid.currency @@ -109,4 +117,19 @@ public class ExtRequestPrebid { * Defines the contract for bidrequest.ext.prebid.channel */ ExtRequestPrebidChannel channel; + + /** + * Defines the contract for bidrequest.ext.prebid.multibid + */ + List multibid; + + /** + * Defines the contract for bidrequest.ext.prebid.analytics + */ + JsonNode analytics; + + /** + * Defines the contract for bidrequest.ext.prebid.pbs + */ + ExtRequestPrebidPbs pbs; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidData.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidData.java index e4098cac96e..990ad7c2ad8 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidData.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidData.java @@ -1,5 +1,7 @@ package org.prebid.server.proto.openrtb.ext.request; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Value; @@ -16,4 +18,8 @@ public class ExtRequestPrebidData { * Defines the contract for bidrequest.ext.prebid.data.bidders */ List bidders; + + @JsonProperty("eidpermissions") + @JsonFormat(without = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + List eidPermissions; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java new file mode 100644 index 00000000000..8ad5b5b2ffb --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java @@ -0,0 +1,25 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Value; + +import java.util.List; + +/** + * Defines the contract for bidrequest.ext.prebid.data.eidPermissions + */ +@Value(staticConstructor = "of") +public class ExtRequestPrebidDataEidPermissions { + + /** + * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.source + */ + String source; + + /** + * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.bidders + */ + @JsonFormat(without = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + List bidders; +} + diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidMultiBid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidMultiBid.java new file mode 100644 index 00000000000..1e9735ddc47 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidMultiBid.java @@ -0,0 +1,35 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +/** + * Defines the contract for bidrequest.ext.prebid.targeting + */ +@Value(staticConstructor = "of") +public class ExtRequestPrebidMultiBid { + + /** + * Defines the contract for bidrequest.ext.prebid.multibid.bidder + */ + String bidder; + + /** + * Defines the contract for bidrequest.ext.prebid.multibid.bidders + */ + List bidders; + + /** + * Defines the contract for bidrequest.ext.prebid.multibid.maxbids + */ + @JsonProperty("maxbids") + Integer maxBids; + + /** + * Defines the contract for bidrequest.ext.prebid.multibid.targetbiddercodeprefix + */ + @JsonProperty("targetbiddercodeprefix") + String targetBidderCodePrefix; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidPbs.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidPbs.java new file mode 100644 index 00000000000..81604921088 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidPbs.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtRequestPrebidPbs { + + String endpoint; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java index 0099e1d9d94..73f317d146e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java @@ -44,8 +44,18 @@ public class ExtRequestTargeting { */ Boolean includebidderkeys; + /** + * Defines the contract for bidrequest.ext.prebid.targeting.includeformat + */ + Boolean includeformat; + /** * Defines the contract for bidrequest.ext.prebid.targeting.truncateattrchars */ Integer truncateattrchars; + + /** + * Defines the contract for bidrequest.ext.prebid.targeting.preferdeals + */ + Boolean preferdeals; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java index abb3da58692..4def5213bfb 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java @@ -1,6 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -31,23 +33,37 @@ public class ExtUser extends FlexibleExtension { String consent; /** - * DigiTrust breaks the typical Prebid Server convention of namespacing "global" options inside "ext.prebid.*" - * to match the recommendation from the broader digitrust community. - *

    - * For more info, see: https://github.com/digi-trust/dt-cdn/wiki/OpenRTB-extension#openrtb-2x + * Standardized User IDs. */ - ExtUserDigiTrust digitrust; + List eids; /** - * Standardized User IDs. + * List of frequency capped for user. */ - List eids; + @JsonProperty("fcapids") + List fcapIds; + + /** + * User date and time. + */ + ExtUserTime time; /** * Defines the contract for bidrequest.user.ext.data. */ ObjectNode data; + /** + * Defines the contract for bidrequest.user.ext.digitrust + */ + JsonNode digitrust; + + /** + * Defines the contract for bidrequest.user.ext.ConsentedProvidersSettings + */ + @JsonProperty("ConsentedProvidersSettings") + ConsentedProvidersSettings consentedProvidersSettings; + @JsonIgnore public boolean isEmpty() { return Objects.equals(this, EMPTY); diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserDigiTrust.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserDigiTrust.java deleted file mode 100644 index 1a1c07cc3b7..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserDigiTrust.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request; - -import lombok.AllArgsConstructor; -import lombok.Value; - -/** - * Defines the contract for bidrequest.user.ext.digitrust - * More info on DigiTrust can be found here: https://github.com/digi-trust/dt-cdn/wiki/Integration-Guide - */ -@AllArgsConstructor(staticName = "of") -@Value -public class ExtUserDigiTrust { - - /** - * Unique device identifier - */ - String id; - - /** - * Key version used to encrypt id - */ - Integer keyv; - - /** - * User optout preference - */ - Integer pref; -} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserTime.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserTime.java new file mode 100644 index 00000000000..87ea86f8520 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserTime.java @@ -0,0 +1,21 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtUserTime { + + /** + * Day of week. + * 1=sun, 7=sat + */ + Integer userdow; + + /** + * Hour of day. + * 0-23 + */ + Integer userhour; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/TraceLevel.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/TraceLevel.java new file mode 100644 index 00000000000..b680bb7646e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/TraceLevel.java @@ -0,0 +1,6 @@ +package org.prebid.server.proto.openrtb.ext.request; + +public enum TraceLevel { + + basic, verbose +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/acuity/ExtImpAcuityads.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/acuity/ExtImpAcuityads.java new file mode 100644 index 00000000000..76c6c369114 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/acuity/ExtImpAcuityads.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.request.acuity; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpAcuityads { + + String host; + + @JsonProperty("accountid") + String accountId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adf/ExtImpAdf.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adf/ExtImpAdf.java new file mode 100644 index 00000000000..36ec8239f30 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adf/ExtImpAdf.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.adf; + +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidrequest.imp[i].ext.adf + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpAdf { + + String mid; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adhese/ExtImpAdhese.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adhese/ExtImpAdhese.java index c2508c6b094..e21e859bc67 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adhese/ExtImpAdhese.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adhese/ExtImpAdhese.java @@ -1,6 +1,5 @@ package org.prebid.server.proto.openrtb.ext.request.adhese; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; import lombok.Value; @@ -18,6 +17,5 @@ public class ExtImpAdhese { String format; - @JsonProperty("targets") - JsonNode keywords; + JsonNode targets; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java index ac28d2c7bdf..b839645945b 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java @@ -8,4 +8,5 @@ public class ExtImpAdoppler { String adunit; + String client; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adot/ExtImpAdot.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adot/ExtImpAdot.java new file mode 100644 index 00000000000..e7a90f3f09a --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adot/ExtImpAdot.java @@ -0,0 +1,18 @@ +package org.prebid.server.proto.openrtb.ext.request.adot; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidrequest.imp[i].ext.adot + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpAdot { + + Boolean parallax; + + @JsonProperty("placementId") + String placementId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adprime/ExtImpAdprime.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adprime/ExtImpAdprime.java new file mode 100644 index 00000000000..9d625de5ec1 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adprime/ExtImpAdprime.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.adprime; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidRequest.imp[i].ext.adprime + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpAdprime { + + @JsonProperty("TagID") + String tagId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adxcg/ExtImpAdxcg.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adxcg/ExtImpAdxcg.java new file mode 100644 index 00000000000..79542d3184e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adxcg/ExtImpAdxcg.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.adxcg; + +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidRequest.imp[i].ext.adxcg + */ +@Value +@AllArgsConstructor(staticName = "of") +public class ExtImpAdxcg { + + String adzoneid; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adyoulike/ExtImpAdyoulike.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adyoulike/ExtImpAdyoulike.java new file mode 100644 index 00000000000..19f4172942a --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adyoulike/ExtImpAdyoulike.java @@ -0,0 +1,24 @@ +package org.prebid.server.proto.openrtb.ext.request.adyoulike; + +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidRequest.imp[i].ext.adyoulike + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpAdyoulike { + + String placement; + + String campaign; + + String track; + + String creative; + + String source; + + String debug; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/algorix/ExtImpAlgorix.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/algorix/ExtImpAlgorix.java new file mode 100644 index 00000000000..1f85534b13b --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/algorix/ExtImpAlgorix.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.algorix; + +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Algorix Ext Imp + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpAlgorix { + + String sid; + + String token; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java new file mode 100644 index 00000000000..1ad50749816 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.amx; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpAmx { + + @JsonProperty("tagId") + String tagId; + + @JsonProperty("adUnitId") + String adUnitId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/appnexus/ExtImpAppnexus.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/appnexus/ExtImpAppnexus.java index 4f2e68796ae..fd6a8e26b3d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/appnexus/ExtImpAppnexus.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/appnexus/ExtImpAppnexus.java @@ -41,5 +41,7 @@ public class ExtImpAppnexus { Boolean usePmtRule; + Boolean generateAdPodId; + ObjectNode privateSizes; // At this time we do no processing on the private sizes, so just leaving it as a JSON blob } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/axonix/ExtImpAxonix.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/axonix/ExtImpAxonix.java new file mode 100644 index 00000000000..7ec0fbaf095 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/axonix/ExtImpAxonix.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.axonix; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class ExtImpAxonix { + + @JsonProperty("supplyId") + String supplyId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/between/ExtImpBetween.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/between/ExtImpBetween.java new file mode 100644 index 00000000000..f951e810006 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/between/ExtImpBetween.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.between; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpBetween { + + String host; + + String publisherId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmachine/ExtImpBidmachine.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmachine/ExtImpBidmachine.java new file mode 100644 index 00000000000..1339adebb98 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmachine/ExtImpBidmachine.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.request.bidmachine; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpBidmachine { + + String host; + + String path; + + String sellerId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmyadz/ExtImpBidmyadz.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmyadz/ExtImpBidmyadz.java new file mode 100644 index 00000000000..8f6bf79a71d --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmyadz/ExtImpBidmyadz.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.bidmyadz; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class ExtImpBidmyadz { + + @JsonProperty("placementId") + String placementId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidscube/ExtImpBidscube.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidscube/ExtImpBidscube.java new file mode 100644 index 00000000000..8cf58c3865c --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidscube/ExtImpBidscube.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.bidscube; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class ExtImpBidscube { + + @JsonProperty("placementId") + String placementId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bmtm/ExtImpBmtm.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bmtm/ExtImpBmtm.java new file mode 100644 index 00000000000..eb6866b287f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bmtm/ExtImpBmtm.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.bmtm; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class ExtImpBmtm { + + String placementId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/colossus/ExtImpColossus.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/colossus/ExtImpColossus.java new file mode 100644 index 00000000000..ffc19773add --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/colossus/ExtImpColossus.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.colossus; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidRequest.imp[i].ext.colossus + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpColossus { + + @JsonProperty("TagID") + String tagId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java new file mode 100644 index 00000000000..e9e852d9099 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java @@ -0,0 +1,20 @@ +package org.prebid.server.proto.openrtb.ext.request.connectad; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.math.BigDecimal; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpConnectAd { + + @JsonProperty("networkId") + Integer networkId; + + @JsonProperty("siteId") + Integer siteId; + + BigDecimal bidfloor; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/decenterads/ExtImpDecenterads.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/decenterads/ExtImpDecenterads.java new file mode 100644 index 00000000000..d009f9b014e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/decenterads/ExtImpDecenterads.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.decenterads; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidRequest.imp[i].ext.decenterads + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpDecenterads { + + @JsonProperty("placementId") + String placementId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java new file mode 100644 index 00000000000..235c5e6c7f5 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.deepintent; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpDeepintent { + + @JsonProperty("tagId") + String tagId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/dmx/ExtImpDmx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/dmx/ExtImpDmx.java index f056c075814..3f868b5b546 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/dmx/ExtImpDmx.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/dmx/ExtImpDmx.java @@ -4,6 +4,8 @@ import lombok.Builder; import lombok.Value; +import java.math.BigDecimal; + /** * Defines the contract for bidRequest.imp[i].ext.dmx */ @@ -20,9 +22,10 @@ public class ExtImpDmx { @JsonProperty("memberid") String memberId; - @JsonProperty("publisher_id") String publisherId; - @JsonProperty("seller_id") String sellerId; + + @JsonProperty("bidfloor") + BigDecimal bidFloor; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgum.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgum.java index 3177c5bf57d..f7d137152b7 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgum.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgum.java @@ -1,11 +1,20 @@ package org.prebid.server.proto.openrtb.ext.request.gumgum; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Value; +import java.math.BigInteger; + @AllArgsConstructor(staticName = "of") @Value public class ExtImpGumgum { String zone; + + @JsonProperty("pubId") + BigInteger pubId; + + @JsonProperty("irisid") + String irisId; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumVideo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumVideo.java new file mode 100644 index 00000000000..a0880109568 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumVideo.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.gumgum; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpGumgumVideo { + + @JsonProperty("irisid") + String irisId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/interactiveoffers/ExtImpInteractiveoffers.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/interactiveoffers/ExtImpInteractiveoffers.java new file mode 100644 index 00000000000..fc3427f745e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/interactiveoffers/ExtImpInteractiveoffers.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.interactiveoffers; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidrequest.imp[i].ext.interactiveoffers + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpInteractiveoffers { + + @JsonProperty("partnerId") + String partnerId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java new file mode 100644 index 00000000000..1de204b5755 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java @@ -0,0 +1,20 @@ +package org.prebid.server.proto.openrtb.ext.request.invibes; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.invibes.model.InvibesDebug; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpInvibes { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("domainId") + Integer domainId; + + @JsonProperty("debug") + InvibesDebug debug; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java new file mode 100644 index 00000000000..f3df563e03f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.invibes.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class InvibesDebug { + + @JsonProperty("testBvid") + String testBvid; + + @JsonProperty("testLog") + Boolean testLog; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java index 490dbd2bea9..f91aa6cc0fb 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java @@ -1,13 +1,19 @@ package org.prebid.server.proto.openrtb.ext.request.ix; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Value; +import java.util.List; + @AllArgsConstructor(staticName = "of") @Value public class ExtImpIx { @JsonProperty("siteId") + @JsonAlias({"siteid", "siteID"}) String siteId; + + List size; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/jixie/ExtImpJixie.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/jixie/ExtImpJixie.java new file mode 100644 index 00000000000..ee9d29f28e7 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/jixie/ExtImpJixie.java @@ -0,0 +1,21 @@ +package org.prebid.server.proto.openrtb.ext.request.jixie; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpJixie { + + String unit; + + @JsonProperty("accountid") + String accountId; + + @JsonProperty("jxprop1") + String jxProp1; + + @JsonProperty("jxprop2") + String jxProp2; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/kayzen/ExtImpKayzen.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kayzen/ExtImpKayzen.java new file mode 100644 index 00000000000..05cc2efe58f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kayzen/ExtImpKayzen.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.kayzen; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class ExtImpKayzen { + + String zone; + + String exchange; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/krushmedia/ExtImpKrushmedia.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/krushmedia/ExtImpKrushmedia.java new file mode 100644 index 00000000000..95a3bc67fd3 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/krushmedia/ExtImpKrushmedia.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.krushmedia; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpKrushmedia { + + @JsonProperty("key") + String accountId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/lifestreet/ExtImpLifestreet.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/lifestreet/ExtImpLifestreet.java deleted file mode 100644 index c9225295069..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/lifestreet/ExtImpLifestreet.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request.lifestreet; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class ExtImpLifestreet { - - String slotTag; -} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java new file mode 100644 index 00000000000..f572890ab72 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.loopme; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidrequest.imp[i].ext.loopme + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpLoopme { + + @JsonProperty("accountId") + String accountId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/madvertise/ExtImpMadvertise.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/madvertise/ExtImpMadvertise.java new file mode 100644 index 00000000000..8ca5fc64015 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/madvertise/ExtImpMadvertise.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.madvertise; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpMadvertise { + + @JsonProperty("zoneId") + String zoneId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobfoxpb/ExtImpMobfoxpb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobfoxpb/ExtImpMobfoxpb.java new file mode 100644 index 00000000000..b19f6fa5fc8 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobfoxpb/ExtImpMobfoxpb.java @@ -0,0 +1,18 @@ +package org.prebid.server.proto.openrtb.ext.request.mobfoxpb; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidRequest.imp[i].ext.mobfoxpb + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpMobfoxpb { + + @JsonProperty("TagID") + String tagId; + + String key; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/nobid/ExtImpNobid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nobid/ExtImpNobid.java new file mode 100644 index 00000000000..e2a25fc93b3 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nobid/ExtImpNobid.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.nobid; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpNobid { + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("placementId") + Integer placementId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/onetag/ExtImpOnetag.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/onetag/ExtImpOnetag.java new file mode 100644 index 00000000000..18cb4dd8ae2 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/onetag/ExtImpOnetag.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.onetag; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpOnetag { + + @JsonProperty("pubId") + String pubId; + + ObjectNode ext; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrain.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrain.java new file mode 100644 index 00000000000..a7d771bd1be --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrain.java @@ -0,0 +1,19 @@ +package org.prebid.server.proto.openrtb.ext.request.outbrains; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@Value +@AllArgsConstructor(staticName = "of") +public class ExtImpOutbrain { + + ExtImpOutbrainPublisher publisher; + + String tagid; + + List bcat; + + List badv; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrainPublisher.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrainPublisher.java new file mode 100644 index 00000000000..ba613cc8ff4 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrainPublisher.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.request.outbrains; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class ExtImpOutbrainPublisher { + + String id; + + String name; + + String domain; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pangle/ExtImpPangle.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pangle/ExtImpPangle.java new file mode 100644 index 00000000000..8746d804644 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pangle/ExtImpPangle.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.request.pangle; + +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpPangle { + + String token; + + String appid; + + String placementid; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubmatic/ExtImpPubmatic.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubmatic/ExtImpPubmatic.java index 5f592de2610..775f9190330 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubmatic/ExtImpPubmatic.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubmatic/ExtImpPubmatic.java @@ -23,8 +23,14 @@ public class ExtImpPubmatic { @JsonProperty("adSlot") String adSlot; - @JsonProperty("wrapper") + String dctr; + + @JsonProperty("pmzoneid") + String pmZoneId; + ObjectNode wrapper; List keywords; + + } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pulsepoint/ExtImpPulsepoint.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pulsepoint/ExtImpPulsepoint.java index 6bcb819c2ce..85625367548 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pulsepoint/ExtImpPulsepoint.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pulsepoint/ExtImpPulsepoint.java @@ -13,7 +13,4 @@ public class ExtImpPulsepoint { @JsonProperty("ct") Integer tagId; - - @JsonProperty("cf") - String adSize; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java index fc847965f79..c140e163661 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubicon.java @@ -34,4 +34,6 @@ public class ExtImpRubicon { String pchain; List keywords; + + ExtImpRubiconDebug debug; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubiconDebug.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubiconDebug.java new file mode 100644 index 00000000000..42590bd46f2 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rubicon/ExtImpRubiconDebug.java @@ -0,0 +1,17 @@ +package org.prebid.server.proto.openrtb.ext.request.rubicon; + +import lombok.Value; + +/** + * Defines the contract for bidrequest.imp[i].ext.prebid.bidder.rubicon.debug + */ +@Value(staticConstructor = "of") +public class ExtImpRubiconDebug { + + /** + * This should be used only for testing. + *

    + * CPM for bid will be replaced with this value. + */ + Float cpmoverride; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtData.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtData.java new file mode 100644 index 00000000000..9117b566e34 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtData.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.sharethrough; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class ExtData { + + @JsonProperty("pbadslot") + String pbAdSlot; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtImpSharethrough.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtImpSharethrough.java index 371fd1b7f09..4723fec5df5 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtImpSharethrough.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtImpSharethrough.java @@ -22,4 +22,6 @@ public class ExtImpSharethrough { List iframeSize; BigDecimal bidfloor; + + ExtData data; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/silvermob/ExtImpSilvermob.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/silvermob/ExtImpSilvermob.java new file mode 100644 index 00000000000..53585f07445 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/silvermob/ExtImpSilvermob.java @@ -0,0 +1,18 @@ +package org.prebid.server.proto.openrtb.ext.request.silvermob; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidRequest.imp[i].ext.silvermob + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtImpSilvermob { + + @JsonProperty("zoneid") + String zoneId; + + String host; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smaato/ExtImpSmaato.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smaato/ExtImpSmaato.java index d976f85fc52..9f6ef557196 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smaato/ExtImpSmaato.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smaato/ExtImpSmaato.java @@ -4,9 +4,6 @@ import lombok.AllArgsConstructor; import lombok.Value; -/** - * Defines the contract for bidRequest.imp[i].ext.smaato - */ @AllArgsConstructor(staticName = "of") @Value public class ExtImpSmaato { @@ -16,4 +13,7 @@ public class ExtImpSmaato { @JsonProperty("adspaceId") String adspaceId; + + @JsonProperty("adbreakId") + String adbreakId; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartyads/ExtImpSmartyAds.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartyads/ExtImpSmartyAds.java new file mode 100644 index 00000000000..2fa7b02cd85 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartyads/ExtImpSmartyAds.java @@ -0,0 +1,18 @@ +package org.prebid.server.proto.openrtb.ext.request.smartyads; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(staticName = "of") +public class ExtImpSmartyAds { + + @JsonProperty("accountid") + String accountId; + + @JsonProperty("sourceid") + String sourceId; + + String host; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/synacormedia/ExtRequestSynacormedia.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/synacormedia/ExtRequestSynacormedia.java new file mode 100644 index 00000000000..e8357f08d15 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/synacormedia/ExtRequestSynacormedia.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.synacormedia; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * Defines the contract for bidRequest.ext + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtRequestSynacormedia { + + @JsonProperty("seatId") + String seatId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java index 8d5cef88eed..c16b2d3177b 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java @@ -4,10 +4,8 @@ import lombok.Value; import java.math.BigDecimal; +import java.util.List; -/** - * Defines the contract for bidRequest.imp[i].ext.tappx - */ @AllArgsConstructor(staticName = "of") @Value public class ExtImpTappx { @@ -19,5 +17,11 @@ public class ExtImpTappx { String endpoint; BigDecimal bidfloor; + + String mktag; + + List bcid; + + List bcrid; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/unicorn/ExtImpUnicorn.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/unicorn/ExtImpUnicorn.java new file mode 100644 index 00000000000..83f12303fc4 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/unicorn/ExtImpUnicorn.java @@ -0,0 +1,24 @@ +package org.prebid.server.proto.openrtb.ext.request.unicorn; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +@Builder(toBuilder = true) +public class ExtImpUnicorn { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("publisherId") + Integer publisherId; + + @JsonProperty("mediaId") + String mediaId; + + @JsonProperty("accountId") + Integer accountId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/BidType.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/BidType.java index 161ec51a73a..f1143141538 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/BidType.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/BidType.java @@ -8,5 +8,9 @@ public enum BidType { video, audio, @JsonProperty("native") - xNative + xNative; + + public String getName() { + return this == xNative ? "native" : this.name(); + } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java index 6099f23fc52..6eaae8e9393 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java @@ -1,6 +1,7 @@ package org.prebid.server.proto.openrtb.ext.response; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Video; import lombok.Builder; import lombok.Value; @@ -10,7 +11,7 @@ /** * Defines the contract for bidresponse.seatbid.bid[i].ext.prebid */ -@Builder +@Builder(toBuilder = true) @Value public class ExtBidPrebid { @@ -20,6 +21,9 @@ public class ExtBidPrebid { Map targeting; + @JsonProperty("targetbiddercode") + String targetBidderCode; + ExtResponseCache cache; @JsonProperty("storedrequestattributes") @@ -28,4 +32,6 @@ public class ExtBidPrebid { Events events; ExtBidPrebidVideo video; + + ObjectNode meta; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponse.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponse.java index 399a3cff405..52e9c179b97 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponse.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponse.java @@ -1,6 +1,6 @@ package org.prebid.server.proto.openrtb.ext.response; -import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Value; import java.util.List; @@ -9,7 +9,7 @@ /** * Defines the contract for bidresponse.ext */ -@AllArgsConstructor(staticName = "of") +@Builder(toBuilder = true) @Value public class ExtBidResponse { @@ -20,6 +20,11 @@ public class ExtBidResponse { */ Map> errors; + /** + * Defines the contract for bidresponse.ext.warnings + */ + Map> warnings; + /** * Defines the contract for bidresponse.ext.responsetimemillis */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponsePrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponsePrebid.java index 91a15109297..2241076fd73 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponsePrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponsePrebid.java @@ -15,4 +15,8 @@ public class ExtBidResponsePrebid { */ Long auctiontimestamp; + /** + * Defines the contract for bidresponse.ext.prebid.modules + */ + ExtModules modules; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidderError.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidderError.java index 246c8ebaa9f..5ef9a58c20a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidderError.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidderError.java @@ -10,7 +10,7 @@ @Value public class ExtBidderError { - private int code; + int code; - private String message; + String message; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugPgmetrics.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugPgmetrics.java new file mode 100644 index 00000000000..bf87b233962 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugPgmetrics.java @@ -0,0 +1,41 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import lombok.Builder; +import lombok.Value; + +import java.util.Map; +import java.util.Set; + +/** + * Defines the contract for bidresponse.ext.debug.pgmetrics + */ +@Builder +@Value +public class ExtDebugPgmetrics { + + public static final ExtDebugPgmetrics EMPTY = ExtDebugPgmetrics.builder().build(); + + Set sentToClient; + + Set sentToClientAsTopMatch; + + Set matchedDomainTargeting; + + Set matchedWholeTargeting; + + Set matchedTargetingFcapped; + + Set matchedTargetingFcapLookupFailed; + + Set readyToServe; + + Set pacingDeferred; + + Map> sentToBidder; + + Map> sentToBidderAsTopMatch; + + Map> receivedFromBidder; + + Set responseInvalidated; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugTrace.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugTrace.java new file mode 100644 index 00000000000..f672e3d89c7 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtDebugTrace.java @@ -0,0 +1,28 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +/** + * Defines the contract for bidresponse.ext.debug.trace + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtDebugTrace { + + /** + * Defines the contract for bidresponse.ext.debug.trace.deals + */ + List deals; + + /** + * Defines the contract for bidresponse.ext.debug.trace.lineItems + */ + + @JsonProperty("lineitems") + Map> lineItems; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtHttpCall.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtHttpCall.java index fae55c73cab..d57adfc178d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtHttpCall.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtHttpCall.java @@ -3,6 +3,9 @@ import lombok.Builder; import lombok.Value; +import java.util.List; +import java.util.Map; + /** * Defines the contract for a bidresponse.ext.debug.httpcalls.{bidder}[i] */ @@ -16,5 +19,7 @@ public class ExtHttpCall { String responsebody; + Map> requestheaders; + Integer status; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModules.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModules.java new file mode 100644 index 00000000000..e26919f8ff1 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModules.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class ExtModules { + + Map>> errors; + + Map>> warnings; + + ExtModulesTrace trace; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTrace.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTrace.java new file mode 100644 index 00000000000..4c8cb424464 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTrace.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ExtModulesTrace { + + @JsonProperty("executiontimemillis") + Long executionTime; + + List stages; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsActivity.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsActivity.java new file mode 100644 index 00000000000..a4256a9a558 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsActivity.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ExtModulesTraceAnalyticsActivity { + + String name; + + String status; + + List results; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsAppliedTo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsAppliedTo.java new file mode 100644 index 00000000000..d6236c7c008 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsAppliedTo.java @@ -0,0 +1,24 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder +@Value +public class ExtModulesTraceAnalyticsAppliedTo { + + @JsonProperty("impids") + List impIds; + + List bidders; + + Boolean request; + + Boolean response; + + @JsonProperty("bidids") + List bidIds; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsResult.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsResult.java new file mode 100644 index 00000000000..5a641622e92 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsResult.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtModulesTraceAnalyticsResult { + + String status; + + ObjectNode values; + + @JsonProperty("appliedto") + ExtModulesTraceAnalyticsAppliedTo appliedTo; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsTags.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsTags.java new file mode 100644 index 00000000000..4d0be464ed0 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceAnalyticsTags.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ExtModulesTraceAnalyticsTags { + + List activities; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceGroup.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceGroup.java new file mode 100644 index 00000000000..02f66a44b09 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceGroup.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ExtModulesTraceGroup { + + @JsonProperty("executiontimemillis") + Long executionTime; + + @JsonProperty("invocationresults") + List invocationResults; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceInvocationResult.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceInvocationResult.java new file mode 100644 index 00000000000..c538b27c795 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceInvocationResult.java @@ -0,0 +1,33 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.HookId; + +import java.util.List; + +@Builder +@Value +public class ExtModulesTraceInvocationResult { + + @JsonProperty("hookid") + HookId hookId; + + @JsonProperty("executiontimemillis") + Long executionTime; + + ExecutionStatus status; + + String message; + + ExecutionAction action; + + @JsonProperty("debugmessages") + List debugMessages; + + @JsonProperty("analyticstags") + ExtModulesTraceAnalyticsTags analyticsTags; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceStage.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceStage.java new file mode 100644 index 00000000000..63a85d15a6d --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceStage.java @@ -0,0 +1,18 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.hooks.execution.model.Stage; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ExtModulesTraceStage { + + Stage stage; + + @JsonProperty("executiontimemillis") + Long executionTime; + + List outcomes; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceStageOutcome.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceStageOutcome.java new file mode 100644 index 00000000000..bd0beb1bc18 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtModulesTraceStageOutcome.java @@ -0,0 +1,17 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ExtModulesTraceStageOutcome { + + String entity; + + @JsonProperty("executiontimemillis") + Long executionTime; + + List groups; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseDebug.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseDebug.java index 6ed9fe22b6c..253a6d9f7ea 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseDebug.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseDebug.java @@ -23,4 +23,14 @@ public class ExtResponseDebug { * Request after resolution of stored requests and debug overrides */ BidRequest resolvedrequest; + + /** + * Defines the contract for bidresponse.ext.debug.pgmetrics + */ + ExtDebugPgmetrics pgmetrics; + + /** + * Defines the contract for bidresponse.ext.debug.trace + */ + ExtDebugTrace trace; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtTraceDeal.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtTraceDeal.java new file mode 100644 index 00000000000..1187ef36757 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtTraceDeal.java @@ -0,0 +1,40 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.time.ZonedDateTime; + +/** + * Defines the contract for bidresponse.ext.debug.trace.deals[] + */ +@AllArgsConstructor(staticName = "of") +@Value +public class ExtTraceDeal { + + /** + * Defines the contract for bidresponse.ext.debug.trace.deals[].lineitemid + */ + @JsonProperty("lineitemid") + String lineItemId; + + /** + * Defines the contract for bidresponse.ext.debug.trace.deals[].time + */ + ZonedDateTime time; + + /** + * Defines the contract for bidresponse.ext.debug.trace.deals[].category + */ + Category category; + + /** + * Defines the contract for bidresponse.ext.debug.trace.deals[].message + */ + String message; + + public enum Category { + targeting, pacing, cleanup, post_processing + } +} diff --git a/src/main/java/org/prebid/server/proto/request/AdUnit.java b/src/main/java/org/prebid/server/proto/request/AdUnit.java deleted file mode 100644 index e616c0fc676..00000000000 --- a/src/main/java/org/prebid/server/proto/request/AdUnit.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.prebid.server.proto.request; - -import com.iab.openrtb.request.Format; -import lombok.Builder; -import lombok.Value; - -import java.util.List; - -@Builder -@Value -public class AdUnit { - - /* Unique code of the ad unit on the page. */ - String code; - - List sizes; - - // --- One of the following two is required. --- - - List bids; - - /* The configuration to load for this ad unit. */ - String configId; - - /* Whether this ad will render in the top IFRAME. */ - Integer topframe; // ... really just a boolean 0|1. - - /* 1 = the ad is interstitial or full screen, 0 = not interstitial. */ - Integer instl; // ... really just a boolean 0|1. - - List mediaTypes; - - Video video; -} diff --git a/src/main/java/org/prebid/server/proto/request/Bid.java b/src/main/java/org/prebid/server/proto/request/Bid.java deleted file mode 100644 index 944b9c8992c..00000000000 --- a/src/main/java/org/prebid/server/proto/request/Bid.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.proto.request; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class Bid { - - /* Unique bid ID for this bidder for this slot. */ - String bidId; - - /* Unique code for an adapter to call. */ - String bidder; - - /* Optional params to send to the adapter. */ - ObjectNode params; -} diff --git a/src/main/java/org/prebid/server/proto/request/CookieSyncRequest.java b/src/main/java/org/prebid/server/proto/request/CookieSyncRequest.java index abbe4f52344..1de4ace0369 100644 --- a/src/main/java/org/prebid/server/proto/request/CookieSyncRequest.java +++ b/src/main/java/org/prebid/server/proto/request/CookieSyncRequest.java @@ -1,6 +1,7 @@ package org.prebid.server.proto.request; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Builder; import lombok.Value; @@ -24,5 +25,28 @@ public class CookieSyncRequest { Integer limit; String account; + + @JsonProperty("filterSettings") + FilterSettings filterSettings; + + @Value(staticConstructor = "of") + public static class FilterSettings { + + MethodFilter iframe; + + MethodFilter image; + } + + @Value(staticConstructor = "of") + public static class MethodFilter { + + JsonNode bidders; + + FilterType filter; + } + + public enum FilterType { + include, exclude + } } diff --git a/src/main/java/org/prebid/server/proto/request/PreBidRequest.java b/src/main/java/org/prebid/server/proto/request/PreBidRequest.java deleted file mode 100644 index ed1055ec772..00000000000 --- a/src/main/java/org/prebid/server/proto/request/PreBidRequest.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.prebid.server.proto.request; - -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Regs; -import com.iab.openrtb.request.User; -import lombok.Builder; -import lombok.Value; - -import java.util.List; - -@Builder(toBuilder = true) -@Value -public class PreBidRequest { - - String accountId; - - /* Unique transaction ID. */ - String tid; - - /* Cache markup for two-phase response (get response then separate call to get markup). */ - Integer cacheMarkup; - - // Sorts bids by price & response time and returns ad server targeting keys for each bid in prebid server response - Integer sortBids; // ... really just a boolean 0|1. - - // Used to determine whether ad server targeting key strings should be truncated on prebid server. For DFP max key - // length should be 20. - Integer maxKeyLength; - - /* - * Flag to indicate if the impression requires secure HTTPS URL creative - * assets and markup, where 0 = non-secure, 1 = secure. If omitted, the - * secure state will be interpreted from the request to the prebid server. - */ - Integer secure; // ... really just a boolean 0|1. - - /* How long to wait for adapters to return bids. */ - Long timeoutMillis; - - List adUnits; - - Boolean isDebug; - - /* - * This object should be included if the ad supported content is a - * non-browser application (typically in mobile) as opposed to a website. At - * a minimum, it is useful to provide an App ID or bundle, but this is not - * strictly required. - */ - App app; - - /* - * 3.2.18 Object: Device. This object provides information pertaining to - * the device through which the user is interacting. Device information - * includes its hardware, platform, location, and carrier data. The device - * can refer to a mobile handset, a desktop computer, set top box, or other - * digital device. - */ - Device device; - - User user; - - Regs regs; - - Sdk sdk; -} diff --git a/src/main/java/org/prebid/server/proto/request/Sdk.java b/src/main/java/org/prebid/server/proto/request/Sdk.java deleted file mode 100644 index dada77e75dc..00000000000 --- a/src/main/java/org/prebid/server/proto/request/Sdk.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.proto.request; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class Sdk { - - String version; - - String source; - - String platform; -} diff --git a/src/main/java/org/prebid/server/proto/request/Video.java b/src/main/java/org/prebid/server/proto/request/Video.java deleted file mode 100644 index 3429aa9a526..00000000000 --- a/src/main/java/org/prebid/server/proto/request/Video.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.prebid.server.proto.request; - -import lombok.Builder; -import lombok.Value; - -import java.util.List; - -@Builder -@Value -public class Video { - - /* - * Content MIME types supported. Popular MIME types may include “video/x-ms-wmv” - * for Windows Media and “video/x-flv” for Flash Video. - */ - List mimes; - - /* - * Minimum video ad duration in seconds. - */ - Integer minduration; - - /* - * Maximum video ad duration in seconds. - */ - Integer maxduration; - - /* - * Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. - */ - Integer startdelay; - - /* - * Indicates if the player will allow the video to be skipped ( 0 = no, 1 = yes). - */ - Integer skippable; - - /* - * Playback method code Description - * 1 - Initiates on Page Load with Sound On - * 2 - Initiates on Page Load with Sound Off by Default - * 3 - Initiates on Click with Sound On - * 4 - Initiates on Mouse-Over with Sound On - * 5 - Initiates on Entering Viewport with Sound On - * 6 - Initiates on Entering Viewport with Sound Off by Default - */ - Integer playbackMethod; - - /* - * protocols as specified in ORTB 5.8 - * 1 VAST 1.0 - * 2 VAST 2.0 - * 3 VAST 3.0 - * - * 4 VAST 1.0 Wrapper - * 5 VAST 2.0 Wrapper - * 6 VAST 3.0 Wrapper - * 7 VAST 4.0 - * 8 VAST 4.0 Wrapper - * 9 DAAST 1.0 - * 10 DAAST 1.0 Wrapper - */ - List protocols; -} diff --git a/src/main/java/org/prebid/server/proto/response/AmpResponse.java b/src/main/java/org/prebid/server/proto/response/AmpResponse.java index 0423df9a236..f9c0b332b9c 100644 --- a/src/main/java/org/prebid/server/proto/response/AmpResponse.java +++ b/src/main/java/org/prebid/server/proto/response/AmpResponse.java @@ -1,7 +1,6 @@ package org.prebid.server.proto.response; import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; @@ -9,8 +8,7 @@ import java.util.List; import java.util.Map; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AmpResponse { Map targeting; @@ -18,4 +16,6 @@ public class AmpResponse { ExtResponseDebug debug; Map> errors; + + ExtAmpVideoResponse ext; } diff --git a/src/main/java/org/prebid/server/proto/response/Bid.java b/src/main/java/org/prebid/server/proto/response/Bid.java deleted file mode 100644 index 74cc3aabd5e..00000000000 --- a/src/main/java/org/prebid/server/proto/response/Bid.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.prebid.server.proto.response; - -import lombok.Builder; -import lombok.Data; -import lombok.experimental.Accessors; - -import java.math.BigDecimal; -import java.util.Map; - -/** - * Bid is a bid from the auction. These are produced by Adapters, and target a particular Ad Unit. - *

    - * This JSON format is a contract with both Prebid.js and Prebid-mobile. - * All changes *must* be backwards compatible, since clients cannot be forced to update their code. - *

    - * IMPORTANT: unlike other data classes this one is mutable (annotated with {@link Data} instead of - * {@link lombok.Value}). Motivation: during the course of processing bids could be altered several times (caching, - * targeting keywords). Creating new instance of the bid in each of these cases seems to cause unnecessary memory - * pressure. In order to avoid unnecessary allocations this class is made mutable (as an exception) i.e. this - * decision could be seen as a performance optimisation. - */ -@Builder -@Data -@Accessors(chain = true) -public final class Bid { - - // Identifies the Bid Request within the Ad Unit which this Bid targets. It should match one of - // the values inside PreBidRequest.adUnits[i].bids[j].bidId. - String bidId; - - // Identifies the AdUnit which this Bid targets. - // It should match one of PreBidRequest.adUnits[i].code, where "i" matches the AdUnit used in - // as bidId. - String code; - - // Uniquely identifies the creative being served. It is not used by prebid-server, but - // it helps publishers and bidders identify and communicate about malicious or inappropriate ads. - // This project simply passes it along with the bid. - String creativeId; - - // Shows whether the creative is a video or banner. - MediaType mediaType; - - // bidderCode of the Bidder who made this bid. - String bidder; - - // Cpm, in US Dollars, which the bidder is willing to pay if this bid is chosen. - BigDecimal price; - - // URL which returns ad markup, and should be called if the bid wins. - // If NURL and Adm are both defined, then Adm takes precedence. - String nurl; - - // Ad markup which should be used to deliver the ad, if this bid is chosen. - // If NURL and Adm are both defined, then Adm takes precedence. - String adm; - - // Intended width which Adm should be shown, in pixels. - Integer width; - - // Intended width which Adm should be shown, in pixels. - Integer height; - - // Not used by prebid-server, but may be used by buyers and sellers who make special - // deals with each other. We simply pass this information along with the bid. - String dealId; - - // ID in prebid-cache which can be used to fetch this ad's content. - // This supports prebid-mobile, which requires that the content be available from a URL. - String cacheId; - - // Complete cache url returned from the prebid-cache. - // more flexible than a design that assumes the UUID is always appended to the end of the URL. - String cacheUrl; - - // Number of milliseconds it took for the adapter to return a bid. - Integer responseTimeMs; - - Map adServerTargeting; -} diff --git a/src/main/java/org/prebid/server/proto/response/BidderDebug.java b/src/main/java/org/prebid/server/proto/response/BidderDebug.java deleted file mode 100644 index 9629df0c946..00000000000 --- a/src/main/java/org/prebid/server/proto/response/BidderDebug.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.proto.response; - -import lombok.Builder; -import lombok.Value; - -@Builder -@Value -public class BidderDebug { - - String requestUri; - - String requestBody; - - String responseBody; - - Integer statusCode; -} diff --git a/src/main/java/org/prebid/server/proto/response/BidderInfo.java b/src/main/java/org/prebid/server/proto/response/BidderInfo.java index 7e347265668..6526bf586fd 100644 --- a/src/main/java/org/prebid/server/proto/response/BidderInfo.java +++ b/src/main/java/org/prebid/server/proto/response/BidderInfo.java @@ -2,14 +2,19 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; +import org.apache.commons.lang3.StringUtils; import java.util.List; -@Value +@Value(staticConstructor = "of") public class BidderInfo { boolean enabled; + boolean usesHttps; + + String aliasOf; + MaintainerInfo maintainer; CapabilitiesInfo capabilities; @@ -22,16 +27,28 @@ public class BidderInfo { boolean modifyingVastXmlAllowed; - public static BidderInfo create(boolean enabled, String maintainerEmail, List appMediaTypes, - List siteMediaTypes, List supportedVendors, int vendorId, - boolean enforceGdpr, boolean ccpaEnforced, boolean modifyingVastXmlAllowed) { - final MaintainerInfo maintainer = new MaintainerInfo(maintainerEmail); - final CapabilitiesInfo capabilities = new CapabilitiesInfo(platformInfo(appMediaTypes), - platformInfo(siteMediaTypes)); - final GdprInfo gdpr = new GdprInfo(vendorId, enforceGdpr); - - return new BidderInfo( - enabled, maintainer, capabilities, supportedVendors, gdpr, ccpaEnforced, modifyingVastXmlAllowed); + public static BidderInfo create(boolean enabled, + String endpoint, + String aliasOf, + String maintainerEmail, + List appMediaTypes, + List siteMediaTypes, + List supportedVendors, + int vendorId, + boolean enforceGdpr, + boolean ccpaEnforced, + boolean modifyingVastXmlAllowed) { + + return of( + enabled, + StringUtils.startsWith(endpoint, "https://"), + aliasOf, + new MaintainerInfo(maintainerEmail), + new CapabilitiesInfo(platformInfo(appMediaTypes), platformInfo(siteMediaTypes)), + supportedVendors, + new GdprInfo(vendorId, enforceGdpr), + ccpaEnforced, + modifyingVastXmlAllowed); } private static PlatformInfo platformInfo(List mediaTypes) { @@ -65,8 +82,8 @@ public static class GdprInfo { /** * GDPR Vendor ID in the IAB Global Vendor List which refers to this Bidder. *

    - * The Global Vendor list can be found here: https://vendorlist.consensu.org/vendorlist.json - * Bidders can register for the list here: https://register.consensu.org/ + * The Global Vendor list can be found at https://iabeurope.eu/ + * Bidders can be registered to the list at https://register.consensu.org/ *

    * If you're not on the list, this should return 0. If cookie sync requests have GDPR consent info, * or the Prebid Server host company configures its deploy to be "cautious" when no GDPR info exists diff --git a/src/main/java/org/prebid/server/proto/response/BidderStatus.java b/src/main/java/org/prebid/server/proto/response/BidderStatus.java deleted file mode 100644 index 4f37ca9e0b6..00000000000 --- a/src/main/java/org/prebid/server/proto/response/BidderStatus.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.prebid.server.proto.response; - -import lombok.Builder; -import lombok.Value; - -import java.util.List; - -@Builder(toBuilder = true) -@Value -public class BidderStatus { - - String bidder; - - String adUnit; - - Integer responseTimeMs; - - Integer numBids; - - String error; - - Boolean noCookie; - - Boolean noBid; - - UsersyncInfo usersync; - - List debug; -} diff --git a/src/main/java/org/prebid/server/proto/response/ExtAmpVideoPrebid.java b/src/main/java/org/prebid/server/proto/response/ExtAmpVideoPrebid.java new file mode 100644 index 00000000000..99b103ec132 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/response/ExtAmpVideoPrebid.java @@ -0,0 +1,10 @@ +package org.prebid.server.proto.response; + +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.response.ExtModules; + +@Value(staticConstructor = "of") +public class ExtAmpVideoPrebid { + + ExtModules modules; +} diff --git a/src/main/java/org/prebid/server/proto/response/ExtAmpVideoResponse.java b/src/main/java/org/prebid/server/proto/response/ExtAmpVideoResponse.java new file mode 100644 index 00000000000..1331c67dd7f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/response/ExtAmpVideoResponse.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.response; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtAmpVideoResponse { + + ExtAmpVideoPrebid prebid; +} diff --git a/src/main/java/org/prebid/server/proto/response/MediaType.java b/src/main/java/org/prebid/server/proto/response/MediaType.java deleted file mode 100644 index c8c214c0385..00000000000 --- a/src/main/java/org/prebid/server/proto/response/MediaType.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.prebid.server.proto.response; - -public enum MediaType { - - banner, video -} diff --git a/src/main/java/org/prebid/server/proto/response/PreBidResponse.java b/src/main/java/org/prebid/server/proto/response/PreBidResponse.java deleted file mode 100644 index 1bc8d6fc358..00000000000 --- a/src/main/java/org/prebid/server/proto/response/PreBidResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.prebid.server.proto.response; - -import lombok.Builder; -import lombok.Value; - -import java.util.List; - -@Builder -@Value -public class PreBidResponse { - - String tid; - - String status; - - List bidderStatus; - - List bids; - - String burl; -} diff --git a/src/main/java/org/prebid/server/proto/response/VideoResponse.java b/src/main/java/org/prebid/server/proto/response/VideoResponse.java index 672a11b5c79..14cf1964e33 100644 --- a/src/main/java/org/prebid/server/proto/response/VideoResponse.java +++ b/src/main/java/org/prebid/server/proto/response/VideoResponse.java @@ -1,8 +1,6 @@ package org.prebid.server.proto.response; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.proto.openrtb.ext.response.ExtAdPod; import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; @@ -11,8 +9,7 @@ import java.util.List; import java.util.Map; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class VideoResponse { @JsonProperty("adPods") @@ -22,6 +19,6 @@ public class VideoResponse { Map> errors; - ObjectNode ext; + ExtAmpVideoResponse ext; } diff --git a/src/main/java/org/prebid/server/settings/ApplicationSettings.java b/src/main/java/org/prebid/server/settings/ApplicationSettings.java index ab5ce4ce3c5..2d78a3a6bdc 100644 --- a/src/main/java/org/prebid/server/settings/ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/ApplicationSettings.java @@ -21,32 +21,30 @@ public interface ApplicationSettings { /** - * Returns {@link Account} info for given a accountId + * Returns {@link Account} for the given account ID. */ Future getAccountById(String accountId, Timeout timeout); /** - * Returns AddUnitConfig info for a given adUnitConfigId + * Fetches stored requests and imps by IDs. */ - Future getAdUnitConfigById(String adUnitConfigId, Timeout timeout); + Future getStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout); /** - * Fetches stored requests and imps + * Fetches AMP stored requests and imps by IDs. */ - Future getStoredData(Set requestIds, Set impIds, Timeout timeout); + Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout); /** - * Fetches stored response + * Fetches Video stored requests and imps by IDs. */ - Future getStoredResponses(Set responseIds, Timeout timeout); + Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout); /** - * Fetches AMP stored requests and imps + * Fetches stored response by IDs. */ - Future getAmpStoredData(Set requestIds, Set impIds, Timeout timeout); - - /** - * Fetches Video stored requests and imps - */ - Future getVideoStoredData(Set requestIds, Set impIds, Timeout timeout); + Future getStoredResponses(Set responseIds, Timeout timeout); } diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java index b763e18dc03..e0d532ed8e6 100644 --- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java @@ -3,12 +3,17 @@ import io.vertx.core.Future; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.settings.helper.StoredDataFetcher; +import org.prebid.server.settings.helper.StoredItemResolver; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredItem; import org.prebid.server.settings.model.StoredResponseDataResult; -import org.prebid.server.settings.model.TriFunction; import java.util.Collections; import java.util.HashMap; @@ -17,9 +22,10 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Consumer; /** - * Adds caching functionality for {@link ApplicationSettings} implementation + * Adds caching functionality for {@link ApplicationSettings} implementation. */ public class CachingApplicationSettings implements ApplicationSettings { @@ -29,23 +35,29 @@ public class CachingApplicationSettings implements ApplicationSettings { private final Map accountCache; private final Map accountToErrorCache; - private final Map adUnitConfigCache; private final SettingsCache cache; private final SettingsCache ampCache; private final SettingsCache videoCache; + private final Metrics metrics; + + public CachingApplicationSettings(ApplicationSettings delegate, + SettingsCache cache, + SettingsCache ampCache, + SettingsCache videoCache, + Metrics metrics, + int ttl, + int size) { - public CachingApplicationSettings(ApplicationSettings delegate, SettingsCache cache, SettingsCache ampCache, - SettingsCache videoCache, int ttl, int size) { if (ttl <= 0 || size <= 0) { throw new IllegalArgumentException("ttl and size must be positive"); } this.delegate = Objects.requireNonNull(delegate); this.accountCache = SettingsCache.createCache(ttl, size); this.accountToErrorCache = SettingsCache.createCache(ttl, size); - this.adUnitConfigCache = SettingsCache.createCache(ttl, size); this.cache = Objects.requireNonNull(cache); this.ampCache = Objects.requireNonNull(ampCache); this.videoCache = Objects.requireNonNull(videoCache); + this.metrics = Objects.requireNonNull(metrics); } /** @@ -53,56 +65,72 @@ public CachingApplicationSettings(ApplicationSettings delegate, SettingsCache ca */ @Override public Future getAccountById(String accountId, Timeout timeout) { - return getFromCacheOrDelegate(accountCache, accountToErrorCache, accountId, timeout, delegate::getAccountById); + return getFromCacheOrDelegate( + accountCache, + accountToErrorCache, + accountId, + timeout, + delegate::getAccountById, + event -> metrics.updateSettingsCacheEventMetric(MetricName.account, event)); } /** - * Retrieves adUnit config from cache or delegates it to original fetcher. + * Retrieves stored data from cache or delegates it to original fetcher. */ @Override - public Future getAdUnitConfigById(String adUnitConfigId, Timeout timeout) { - return getFromCacheOrDelegate(adUnitConfigCache, accountToErrorCache, adUnitConfigId, timeout, - delegate::getAdUnitConfigById); + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getFromCacheOrDelegate(cache, accountId, requestIds, impIds, timeout, delegate::getStoredData); } /** - * Retrieves stored data from cache or delegates it to original fetcher. + * Retrieves amp stored data from cache or delegates it to original fetcher. */ @Override - public Future getStoredData(Set requestIds, Set impIds, Timeout timeout) { - return getFromCacheOrDelegate(cache, requestIds, impIds, timeout, delegate::getStoredData); + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getFromCacheOrDelegate(ampCache, accountId, requestIds, impIds, timeout, delegate::getAmpStoredData); } - /** - * Delegates stored response retrieve to original fetcher, as caching is not supported fot stored response. - */ @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return delegate.getStoredResponses(responseIds, timeout); + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getFromCacheOrDelegate(videoCache, accountId, requestIds, impIds, timeout, delegate::getVideoStoredData); } /** - * Retrieves amp stored data from cache or delegates it to original fetcher. + * Delegates stored response retrieve to original fetcher, as caching is not supported fot stored response. */ @Override - public Future getAmpStoredData(Set requestIds, Set impIds, Timeout timeout) { - return getFromCacheOrDelegate(ampCache, requestIds, impIds, timeout, delegate::getAmpStoredData); - } - - @Override - public Future getVideoStoredData(Set requestIds, Set impIds, Timeout timeout) { - return getFromCacheOrDelegate(videoCache, requestIds, impIds, timeout, delegate::getVideoStoredData); + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return delegate.getStoredResponses(responseIds, timeout); } - private static Future getFromCacheOrDelegate(Map cache, Map accountToErrorCache, - String key, Timeout timeout, - BiFunction> retriever) { + private static Future getFromCacheOrDelegate(Map cache, + Map accountToErrorCache, + String key, + Timeout timeout, + BiFunction> retriever, + Consumer metricUpdater) { final T cachedValue = cache.get(key); if (cachedValue != null) { + metricUpdater.accept(MetricName.hit); + return Future.succeededFuture(cachedValue); } + metricUpdater.accept(MetricName.miss); + final String preBidExceptionMessage = accountToErrorCache.get(key); if (preBidExceptionMessage != null) { return Future.failedFuture(new PreBidException(preBidExceptionMessage)); @@ -118,23 +146,32 @@ private static Future getFromCacheOrDelegate(Map cache, Map getFromCacheOrDelegate( - SettingsCache cache, Set requestIds, Set impIds, Timeout timeout, - TriFunction, Set, Timeout, Future> retriever) { + SettingsCache cache, + String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + StoredDataFetcher, Set, Timeout, Future> retriever) { - final Map requestCache = cache.getRequestCache(); - final Map impCache = cache.getImpCache(); + // empty string account ID doesn't make sense + final String normalizedAccountId = StringUtils.stripToNull(accountId); + + // search in cache + final Map> requestCache = cache.getRequestCache(); + final Map> impCache = cache.getImpCache(); final Set missedRequestIds = new HashSet<>(); - final Map storedIdToRequest = getFromCacheOrAddMissedIds(requestIds, requestCache, - missedRequestIds); + final Map storedIdToRequest = getFromCacheOrAddMissedIds(normalizedAccountId, requestIds, + requestCache, missedRequestIds); final Set missedImpIds = new HashSet<>(); - final Map storedIdToImp = getFromCacheOrAddMissedIds(impIds, impCache, missedImpIds); + final Map storedIdToImp = getFromCacheOrAddMissedIds(normalizedAccountId, impIds, impCache, + missedImpIds); if (missedRequestIds.isEmpty() && missedImpIds.isEmpty()) { return Future.succeededFuture( @@ -142,43 +179,60 @@ private static Future getFromCacheOrDelegate( } // delegate call to original source for missed ids and update cache with it - return retriever.apply(missedRequestIds, missedImpIds, timeout).compose(result -> { + return retriever.apply(normalizedAccountId, missedRequestIds, missedImpIds, timeout).map(result -> { final Map storedIdToRequestFromDelegate = result.getStoredIdToRequest(); - final Map storedIdToImpFromDelegate = result.getStoredIdToImp(); - - cache.save(storedIdToRequestFromDelegate, storedIdToImpFromDelegate); - storedIdToRequest.putAll(storedIdToRequestFromDelegate); + for (Map.Entry entry : storedIdToRequestFromDelegate.entrySet()) { + cache.saveRequestCache(normalizedAccountId, entry.getKey(), entry.getValue()); + } + + final Map storedIdToImpFromDelegate = result.getStoredIdToImp(); storedIdToImp.putAll(storedIdToImpFromDelegate); + for (Map.Entry entry : storedIdToImpFromDelegate.entrySet()) { + cache.saveImpCache(normalizedAccountId, entry.getKey(), entry.getValue()); + } - return Future.succeededFuture(StoredDataResult.of(storedIdToRequest, storedIdToImp, result.getErrors())); + return StoredDataResult.of(storedIdToRequest, storedIdToImp, result.getErrors()); }); } - private static Future cacheAndReturnFailedFuture(Throwable throwable, String key, + private static Future cacheAndReturnFailedFuture(Throwable throwable, + String key, Map cache) { + if (throwable instanceof PreBidException) { cache.put(key, throwable.getMessage()); } + return Future.failedFuture(throwable); } - private static Map getFromCacheOrAddMissedIds(Set ids, Map cache, + private static Map getFromCacheOrAddMissedIds(String accountId, + Set ids, + Map> cache, Set missedIds) { - final Map storedIdToJson = new HashMap<>(ids.size()); + + final Map idToStoredItem = new HashMap<>(ids.size()); + for (String id : ids) { - final String cachedValue = cache.get(id); - if (cachedValue != null) { - storedIdToJson.put(id, cachedValue); - } else { + try { + final StoredItem resolvedStoredItem = StoredItemResolver.resolve(null, accountId, id, cache.get(id)); + idToStoredItem.put(id, resolvedStoredItem.getData()); + } catch (PreBidException e) { missedIds.add(id); } } - return storedIdToJson; + + return idToStoredItem; } public void invalidateAccountCache(String accountId) { accountCache.remove(accountId); logger.debug("Account with id {0} was invalidated", accountId); } + + public void invalidateAllAccountCache() { + accountCache.clear(); + logger.debug("All accounts cache were invalidated"); + } } diff --git a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java index 92f0cb8bc59..f119ff45b49 100644 --- a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java @@ -2,10 +2,10 @@ import io.vertx.core.Future; import org.prebid.server.execution.Timeout; +import org.prebid.server.settings.helper.StoredDataFetcher; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; -import org.prebid.server.settings.model.TriFunction; import java.util.Collections; import java.util.HashMap; @@ -44,48 +44,42 @@ private static Proxy createProxy(List delegates) { /** * Runs a process to get account by id from a chain of retrievers - * and returns {@link Future<{@link Account}>} + * and returns {@link Future<{@link Account}>}. */ @Override public Future getAccountById(String accountId, Timeout timeout) { return proxy.getAccountById(accountId, timeout); } - /** - * Runs a process to get AdUnit config by id from a chain of retrievers - * and returns {@link Future<{@link String}>} - */ - @Override - public Future getAdUnitConfigById(String adUnitConfigId, Timeout timeout) { - return proxy.getAdUnitConfigById(adUnitConfigId, timeout); - } - /** * Runs a process to get stored requests by a collection of ids from a chain of retrievers - * and returns {@link Future<{@link StoredDataResult }>} + * and returns {@link Future<{@link StoredDataResult }>}. */ @Override - public Future getStoredData(Set requestIds, Set impIds, Timeout timeout) { - return proxy.getStoredData(requestIds, impIds, timeout); + public Future getStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return proxy.getStoredData(accountId, requestIds, impIds, timeout); } /** * Runs a process to get stored requests by a collection of amp ids from a chain of retrievers - * and returns {@link Future<{@link StoredDataResult }>} + * and returns {@link Future<{@link StoredDataResult }>}. */ @Override - public Future getAmpStoredData(Set requestIds, Set impIds, Timeout timeout) { - return proxy.getAmpStoredData(requestIds, Collections.emptySet(), timeout); + public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return proxy.getAmpStoredData(accountId, requestIds, Collections.emptySet(), timeout); } @Override - public Future getVideoStoredData(Set requestIds, Set impIds, Timeout timeout) { - return proxy.getVideoStoredData(requestIds, impIds, timeout); + public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return proxy.getVideoStoredData(accountId, requestIds, impIds, timeout); } /** * Runs a process to get stored responses by a collection of ids from a chain of retrievers - * and returns {@link Future<{@link StoredResponseDataResult }>} + * and returns {@link Future<{@link StoredResponseDataResult }>}. */ @Override public Future getStoredResponses(Set responseIds, Timeout timeout) { @@ -93,7 +87,7 @@ public Future getStoredResponses(Set responseI } /** - * Decorates {@link ApplicationSettings} for a chain of retrievers + * Decorates {@link ApplicationSettings} for a chain of retrievers. */ private static class Proxy implements ApplicationSettings { @@ -111,12 +105,6 @@ public Future getAccountById(String accountId, Timeout timeout) { next != null ? next::getAccountById : null); } - @Override - public Future getAdUnitConfigById(String adUnitConfigId, Timeout timeout) { - return getConfig(adUnitConfigId, timeout, applicationSettings::getAdUnitConfigById, - next != null ? next::getAdUnitConfigById : null); - } - private static Future getConfig(String key, Timeout timeout, BiFunction> retriever, BiFunction> nextRetriever) { @@ -127,22 +115,24 @@ private static Future getConfig(String key, Timeout timeout, } @Override - public Future getStoredData(Set requestIds, Set impIds, Timeout timeout) { - return getStoredRequests(requestIds, impIds, timeout, applicationSettings::getStoredData, + public Future getStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return getStoredRequests(accountId, requestIds, impIds, timeout, applicationSettings::getStoredData, next != null ? next::getStoredData : null); } @Override - public Future getAmpStoredData(Set requestIds, Set impIds, + public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, Timeout timeout) { - return getStoredRequests(requestIds, Collections.emptySet(), timeout, applicationSettings::getAmpStoredData, + return getStoredRequests(accountId, requestIds, Collections.emptySet(), timeout, + applicationSettings::getAmpStoredData, next != null ? next::getAmpStoredData : null); } @Override - public Future getVideoStoredData(Set requestIds, Set impIds, + public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, Timeout timeout) { - return getStoredRequests(requestIds, impIds, timeout, + return getStoredRequests(accountId, requestIds, impIds, timeout, applicationSettings::getVideoStoredData, next != null ? next::getVideoStoredData : null); } @@ -156,35 +146,35 @@ private static Future getStoredResponses( Set responseIds, Timeout timeout, BiFunction, Timeout, Future> retriever, BiFunction, Timeout, Future> nextRetriever) { + return retriever.apply(responseIds, timeout) .compose(retrieverResult -> nextRetriever == null || retrieverResult.getErrors().isEmpty() ? Future.succeededFuture(retrieverResult) : getRemainingStoredResponses(responseIds, timeout, - retrieverResult.getStoredSeatBid(), nextRetriever)); + retrieverResult.getIdToStoredResponses(), nextRetriever)); } private static Future getStoredRequests( - Set requestIds, Set impIds, Timeout timeout, - TriFunction, Set, Timeout, Future> retriever, - TriFunction, Set, Timeout, Future> nextRetriever) { + String accountId, Set requestIds, Set impIds, Timeout timeout, + StoredDataFetcher, Set, Timeout, Future> retriever, + StoredDataFetcher, Set, Timeout, Future> nextRetriever) { - return retriever.apply(requestIds, impIds, timeout) + return retriever.apply(accountId, requestIds, impIds, timeout) .compose(retrieverResult -> nextRetriever == null || retrieverResult.getErrors().isEmpty() ? Future.succeededFuture(retrieverResult) - : getRemainingStoredRequests(requestIds, impIds, timeout, + : getRemainingStoredRequests(accountId, requestIds, impIds, timeout, retrieverResult.getStoredIdToRequest(), retrieverResult.getStoredIdToImp(), nextRetriever)); } private static Future getRemainingStoredRequests( - Set requestIds, Set impIds, Timeout timeout, + String accountId, Set requestIds, Set impIds, Timeout timeout, Map storedIdToRequest, Map storedIdToImp, - TriFunction, Set, Timeout, Future> retriever) { + StoredDataFetcher, Set, Timeout, Future> retriever) { - return retriever.apply( - subtractSets(requestIds, storedIdToRequest.keySet()), + return retriever.apply(accountId, subtractSets(requestIds, storedIdToRequest.keySet()), subtractSets(impIds, storedIdToImp.keySet()), timeout) .map(result -> StoredDataResult.of( combineMaps(storedIdToRequest, result.getStoredIdToRequest()), @@ -195,22 +185,23 @@ private static Future getRemainingStoredRequests( private static Future getRemainingStoredResponses( Set responseIds, Timeout timeout, Map storedSeatBids, BiFunction, Timeout, Future> retriever) { + return retriever.apply(subtractSets(responseIds, storedSeatBids.keySet()), timeout) .map(result -> StoredResponseDataResult.of( - combineMaps(storedSeatBids, result.getStoredSeatBid()), + combineMaps(storedSeatBids, result.getIdToStoredResponses()), result.getErrors())); } - private static Set subtractSets(Set set1, Set set2) { - final Set remaining = new HashSet<>(set1); - remaining.removeAll(set2); + private static Set subtractSets(Set first, Set second) { + final Set remaining = new HashSet<>(first); + remaining.removeAll(second); return remaining; } - private static Map combineMaps(Map map1, Map map2) { - final Map combined = new HashMap<>(map1.size() + map2.size()); - combined.putAll(map1); - combined.putAll(map2); + private static Map combineMaps(Map first, Map second) { + final Map combined = new HashMap<>(first.size() + second.size()); + combined.putAll(first); + combined.putAll(second); return combined; } } diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java new file mode 100644 index 00000000000..04dc64ad3c9 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -0,0 +1,96 @@ +package org.prebid.server.settings; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.execution.Timeout; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredResponseDataResult; + +import java.util.Objects; +import java.util.Set; + +public class EnrichingApplicationSettings implements ApplicationSettings { + + private final ApplicationSettings delegate; + private final JsonMerger jsonMerger; + private final Account defaultAccount; + + public EnrichingApplicationSettings(String defaultAccountConfig, + ApplicationSettings delegate, + JsonMerger jsonMerger) { + + this.delegate = Objects.requireNonNull(delegate); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + + this.defaultAccount = parseAccount(defaultAccountConfig); + } + + @Override + public Future getAccountById(String accountId, Timeout timeout) { + final Future accountFuture = delegate.getAccountById(accountId, timeout); + + if (defaultAccount == null) { + return accountFuture; + } + + return accountFuture + .map(this::mergeAccounts) + .otherwise(mergeAccounts(Account.empty(accountId))); + } + + @Override + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return delegate.getStoredData(accountId, requestIds, impIds, timeout); + } + + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return delegate.getStoredResponses(responseIds, timeout); + } + + @Override + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return delegate.getAmpStoredData(accountId, requestIds, impIds, timeout); + } + + @Override + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return delegate.getVideoStoredData(accountId, requestIds, impIds, timeout); + } + + private static Account parseAccount(String accountConfig) { + try { + final Account account = StringUtils.isNotBlank(accountConfig) + ? new ObjectMapper().readValue(accountConfig, Account.class) + : null; + + return isNotEmpty(account) ? account : null; + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Could not parse default account configuration", e); + } + } + + private static boolean isNotEmpty(Account account) { + return account != null && !account.equals(Account.builder().build()); + } + + private Account mergeAccounts(Account account) { + return jsonMerger.merge(account, defaultAccount, Account.class); + } +} diff --git a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java index 4ba325e6c42..b7b805ea97b 100644 --- a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java @@ -5,12 +5,10 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.file.FileSystem; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AdUnitConfig; import org.prebid.server.settings.model.SettingsFile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredDataType; @@ -41,7 +39,6 @@ public class FileApplicationSettings implements ApplicationSettings { private static final String JSON_SUFFIX = ".json"; private final Map accounts; - private final Map configs; private final Map storedIdToRequest; private final Map storedIdToImp; private final Map storedIdToSeatBid; @@ -56,10 +53,6 @@ public FileApplicationSettings(FileSystem fileSystem, String settingsFileName, S Account::getId, Function.identity()); - configs = toMap(settingsFile.getConfigs(), - AdUnitConfig::getId, - config -> ObjectUtils.defaultIfNull(config.getConfig(), StringUtils.EMPTY)); - this.storedIdToRequest = readStoredData(fileSystem, Objects.requireNonNull(storedRequestsDir)); this.storedIdToImp = readStoredData(fileSystem, Objects.requireNonNull(storedImpsDir)); this.storedIdToSeatBid = readStoredData(fileSystem, Objects.requireNonNull(storedResponsesDir)); @@ -70,18 +63,14 @@ public Future getAccountById(String accountId, Timeout timeout) { return mapValueToFuture(accounts, accountId, "Account"); } - @Override - public Future getAdUnitConfigById(String adUnitConfigId, Timeout timeout) { - return mapValueToFuture(configs, adUnitConfigId, "AdUnitConfig"); - } - /** * Creates {@link StoredDataResult} by checking if any ids are missed in storedRequest map * and adding an error to list for each missed Id * and returns {@link Future<{@link StoredDataResult }>} with all loaded files and errors list. */ @Override - public Future getStoredData(Set requestIds, Set impIds, Timeout timeout) { + public Future getStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { return Future.succeededFuture(CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds) ? StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList()) : StoredDataResult.of( @@ -94,6 +83,18 @@ public Future getStoredData(Set requestIds, Set getAmpStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return getStoredData(accountId, requestIds, Collections.emptySet(), timeout); + } + + @Override + public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return getStoredData(accountId, requestIds, impIds, timeout); + } + /** * Creates {@link StoredResponseDataResult} by checking if any ids are missed in storedResponse map * and adding an error to list for each missed Id @@ -108,16 +109,6 @@ public Future getStoredResponses(Set responseI errorsForMissedIds(responseIds, storedIdToSeatBid, StoredDataType.seatbid))); } - @Override - public Future getAmpStoredData(Set requestIds, Set impIds, Timeout timeout) { - return getStoredData(requestIds, Collections.emptySet(), timeout); - } - - @Override - public Future getVideoStoredData(Set requestIds, Set impIds, Timeout timeout) { - return getStoredData(requestIds, impIds, timeout); - } - private static Map toMap(List list, Function keyMapper, Function valueMapper) { return list != null ? list.stream().collect(Collectors.toMap(keyMapper, valueMapper)) : Collections.emptyMap(); } diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java index 54a9d3f7f77..9fcdd816305 100644 --- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java @@ -2,10 +2,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.json.DecodeException; @@ -14,6 +16,7 @@ import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredDataType; import org.prebid.server.settings.model.StoredResponseDataResult; +import org.prebid.server.settings.proto.response.HttpAccountsResponse; import org.prebid.server.settings.proto.response.HttpFetcherResponse; import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.http.HttpClient; @@ -59,7 +62,6 @@ */ public class HttpApplicationSettings implements ApplicationSettings { - private static final String NOT_SUPPORTED = "Not supported"; private static final Logger logger = LoggerFactory.getLogger(HttpApplicationSettings.class); private String endpoint; @@ -77,20 +79,62 @@ public HttpApplicationSettings(HttpClient httpClient, JacksonMapper mapper, Stri this.videoEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(videoEndpoint)); } - /** - * Not supported and returns failed result. - */ @Override public Future getAccountById(String accountId, Timeout timeout) { - return Future.failedFuture(new PreBidException("Not supported")); + + return fetchAccountsByIds(Collections.singleton(accountId), timeout) + .map(accounts -> accounts.stream() + .findFirst() + .orElseThrow(() -> + new PreBidException(String.format("Account with id : %s not found", accountId)))); } - /** - * Not supported and returns failed result. - */ - @Override - public Future getAdUnitConfigById(String adUnitConfigId, Timeout timeout) { - return Future.failedFuture(new PreBidException("Not supported")); + private Future> fetchAccountsByIds(Set accountIds, Timeout timeout) { + if (CollectionUtils.isEmpty(accountIds)) { + return Future.succeededFuture(Collections.emptySet()); + } + final long remainingTimeout = timeout.remaining(); + if (timeout.remaining() <= 0) { + return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); + } + + return httpClient.get(accountsRequestUrlFrom(endpoint, accountIds), HttpUtil.headers(), remainingTimeout) + .compose(response -> processAccountsResponse(response, accountIds)) + .recover(Future::failedFuture); + } + + private static String accountsRequestUrlFrom(String endpoint, Set accountIds) { + final StringBuilder url = new StringBuilder(endpoint); + url.append(endpoint.contains("?") ? "&" : "?"); + + if (!accountIds.isEmpty()) { + url.append("account-ids=[\"").append(joinIds(accountIds)).append("\"]"); + } + + return url.toString(); + } + + private Future> processAccountsResponse(HttpClientResponse response, Set accountIds) { + return Future.succeededFuture( + toAccountsResult(response.getStatusCode(), response.getBody(), accountIds)); + } + + private Set toAccountsResult(int statusCode, String body, Set accountIds) { + if (statusCode != HttpResponseStatus.OK.code()) { + throw new PreBidException(String.format("Error fetching accounts %s via http: " + + "unexpected response status %d", accountIds, statusCode)); + } + + final HttpAccountsResponse response; + try { + response = mapper.decodeValue(body, HttpAccountsResponse.class); + } catch (DecodeException e) { + throw new PreBidException(String.format("Error fetching accounts %s " + + "via http: failed to parse response: %s", accountIds, e.getMessage())); + } + final Map accounts = response.getAccounts(); + + return MapUtils.isNotEmpty(accounts) ? new HashSet<>(accounts.values()) : Collections.emptySet(); } /** @@ -98,33 +142,36 @@ public Future getAdUnitConfigById(String adUnitConfigId, Timeout timeout * and returns {@link Future<{@link StoredDataResult }>} */ @Override - public Future getStoredData(Set requestIds, Set impIds, Timeout timeout) { + public Future getStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { return fetchStoredData(endpoint, requestIds, impIds, timeout); } /** - * Not supported and returns failed result. + * Runs a process to get stored requests by a collection of amp ids from http service + * and returns {@link Future<{@link StoredDataResult }>} */ @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return Future.failedFuture(new PreBidException(NOT_SUPPORTED)); + public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return fetchStoredData(ampEndpoint, requestIds, Collections.emptySet(), timeout); } /** - * Runs a process to get stored requests by a collection of amp ids from http service - * and returns {@link Future<{@link StoredDataResult }>} + * Not supported and returns failed result. */ @Override - public Future getAmpStoredData(Set requestIds, Set impIds, Timeout timeout) { - return fetchStoredData(ampEndpoint, requestIds, Collections.emptySet(), timeout); + public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return fetchStoredData(videoEndpoint, requestIds, impIds, timeout); } /** * Not supported and returns failed result. */ @Override - public Future getVideoStoredData(Set requestIds, Set impIds, Timeout timeout) { - return fetchStoredData(videoEndpoint, requestIds, impIds, timeout); + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return Future.failedFuture(new PreBidException("Not supported")); } private Future fetchStoredData(String endpoint, Set requestIds, Set impIds, @@ -136,15 +183,15 @@ private Future fetchStoredData(String endpoint, Set re final long remainingTimeout = timeout.remaining(); if (remainingTimeout <= 0) { - return failResponse(new TimeoutException("Timeout has been exceeded"), requestIds, impIds); + return failStoredDataResponse(new TimeoutException("Timeout has been exceeded"), requestIds, impIds); } - return httpClient.get(urlFrom(endpoint, requestIds, impIds), HttpUtil.headers(), remainingTimeout) - .compose(response -> processResponse(response, requestIds, impIds)) - .recover(exception -> failResponse(exception, requestIds, impIds)); + return httpClient.get(storeRequestUrlFrom(endpoint, requestIds, impIds), HttpUtil.headers(), remainingTimeout) + .compose(response -> processStoredDataResponse(response, requestIds, impIds)) + .recover(exception -> failStoredDataResponse(exception, requestIds, impIds)); } - private static String urlFrom(String endpoint, Set requestIds, Set impIds) { + private static String storeRequestUrlFrom(String endpoint, Set requestIds, Set impIds) { final StringBuilder url = new StringBuilder(endpoint); url.append(endpoint.contains("?") ? "&" : "?"); @@ -166,14 +213,14 @@ private static String joinIds(Set ids) { return String.join("\",\"", ids); } - private static Future failResponse(Throwable throwable, Set requestIds, - Set impIds) { + private static Future failStoredDataResponse(Throwable throwable, Set requestIds, + Set impIds) { return Future.succeededFuture( toFailedStoredDataResult(requestIds, impIds, throwable.getMessage())); } - private Future processResponse(HttpClientResponse response, Set requestIds, - Set impIds) { + private Future processStoredDataResponse(HttpClientResponse response, Set requestIds, + Set impIds) { return Future.succeededFuture( toStoredDataResult(requestIds, impIds, response.getStatusCode(), response.getBody())); } @@ -194,7 +241,7 @@ private static StoredDataResult toFailedStoredDataResult(Set requestIds, private StoredDataResult toStoredDataResult(Set requestIds, Set impIds, int statusCode, String body) { - if (statusCode != 200) { + if (statusCode != HttpResponseStatus.OK.code()) { return toFailedStoredDataResult(requestIds, impIds, "HTTP status code %d", statusCode); } diff --git a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java index 904242ee702..d5f20d6f2da 100644 --- a/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/JdbcApplicationSettings.java @@ -9,11 +9,9 @@ import org.prebid.server.execution.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.settings.mapper.JdbcStoredDataResultMapper; -import org.prebid.server.settings.mapper.JdbcStoredResponseResultMapper; +import org.prebid.server.settings.helper.JdbcStoredDataResultMapper; +import org.prebid.server.settings.helper.JdbcStoredResponseResultMapper; import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountAnalyticsConfig; -import org.prebid.server.settings.model.AccountGdprConfig; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; import org.prebid.server.vertx.jdbc.JdbcClient; @@ -37,36 +35,43 @@ */ public class JdbcApplicationSettings implements ApplicationSettings { + private static final String ACCOUNT_ID_PLACEHOLDER = "%ACCOUNT_ID%"; private static final String REQUEST_ID_PLACEHOLDER = "%REQUEST_ID_LIST%"; private static final String IMP_ID_PLACEHOLDER = "%IMP_ID_LIST%"; private static final String RESPONSE_ID_PLACEHOLDER = "%RESPONSE_ID_LIST%"; + private static final String QUERY_PARAM_PLACEHOLDER = "?"; private final JdbcClient jdbcClient; private final JacksonMapper mapper; + /** + * Query to select account by ids. + */ + private final String selectAccountQuery; + /** * Query to select stored requests and imps by ids, for example: *

    -     * SELECT reqid, requestData, 'request' as dataType
    +     * SELECT accountId, reqid, requestData, 'request' as dataType
          *   FROM stored_requests
          *   WHERE reqid in (%REQUEST_ID_LIST%)
          * UNION ALL
    -     * SELECT impid, impData, 'imp' as dataType
    +     * SELECT accountId, impid, impData, 'imp' as dataType
          *   FROM stored_imps
          *   WHERE impid in (%IMP_ID_LIST%)
          * 
    */ - private final String selectQuery; + private final String selectStoredRequestsQuery; /** * Query to select amp stored requests by ids, for example: *
    -     * SELECT reqid, requestData, 'request' as dataType
    +     * SELECT accountId, reqid, requestData, 'request' as dataType
          *   FROM stored_requests
          *   WHERE reqid in (%REQUEST_ID_LIST%)
          * 
    */ - private final String selectAmpQuery; + private final String selectAmpStoredRequestsQuery; /** * Query to select stored responses by ids, for example: @@ -76,61 +81,38 @@ public class JdbcApplicationSettings implements ApplicationSettings { * WHERE respid in (%RESPONSE_ID_LIST%) * */ - private final String selectResponseQuery; + private final String selectStoredResponsesQuery; public JdbcApplicationSettings(JdbcClient jdbcClient, JacksonMapper mapper, - String selectQuery, - String selectAmpQuery, - String selectResponseQuery) { + String selectAccountQuery, + String selectStoredRequestsQuery, + String selectAmpStoredRequestsQuery, + String selectStoredResponsesQuery) { this.jdbcClient = Objects.requireNonNull(jdbcClient); this.mapper = Objects.requireNonNull(mapper); - this.selectQuery = Objects.requireNonNull(selectQuery); - this.selectAmpQuery = Objects.requireNonNull(selectAmpQuery); - this.selectResponseQuery = Objects.requireNonNull(selectResponseQuery); + this.selectAccountQuery = Objects.requireNonNull(selectAccountQuery) + .replace(ACCOUNT_ID_PLACEHOLDER, QUERY_PARAM_PLACEHOLDER); + this.selectStoredRequestsQuery = Objects.requireNonNull(selectStoredRequestsQuery); + this.selectAmpStoredRequestsQuery = Objects.requireNonNull(selectAmpStoredRequestsQuery); + this.selectStoredResponsesQuery = Objects.requireNonNull(selectStoredResponsesQuery); } /** * Runs a process to get account by id from database - * and returns {@link Future<{@link Account}>}. + * and returns {@link Future}<{@link Account}>. */ @Override public Future getAccountById(String accountId, Timeout timeout) { - return jdbcClient.executeQuery("SELECT uuid, price_granularity, banner_cache_ttl, video_cache_ttl," - + " events_enabled, enforce_ccpa, tcf_config, analytics_sampling_factor, truncate_target_attr," - + " default_integration, analytics_config FROM accounts_account where uuid = ? LIMIT 1", + return jdbcClient.executeQuery( + selectAccountQuery, Collections.singletonList(accountId), - result -> mapToModelOrError(result, row -> Account.builder() - .id(row.getString(0)) - .priceGranularity(row.getString(1)) - .bannerCacheTtl(row.getInteger(2)) - .videoCacheTtl(row.getInteger(3)) - .eventsEnabled(row.getBoolean(4)) - .enforceCcpa(row.getBoolean(5)) - .gdpr(toModel(row.getString(6), AccountGdprConfig.class)) - .analyticsSamplingFactor(row.getInteger(7)) - .truncateTargetAttr(row.getInteger(8)) - .defaultIntegration(row.getString(9)) - .analyticsConfig(toModel(row.getString(10), AccountAnalyticsConfig.class)) - .build()), + result -> mapToModelOrError(result, row -> toAccount(row.getString(0))), timeout) .compose(result -> failedIfNull(result, accountId, "Account")); } - /** - * Runs a process to get AdUnit config by id from database - * and returns {@link Future<{@link String}>}. - */ - @Override - public Future getAdUnitConfigById(String adUnitConfigId, Timeout timeout) { - return jdbcClient.executeQuery("SELECT config FROM s2sconfig_config where uuid = ? LIMIT 1", - Collections.singletonList(adUnitConfigId), - result -> mapToModelOrError(result, row -> row.getString(0)), - timeout) - .compose(result -> failedIfNull(result, adUnitConfigId, "AdUnitConfig")); - } - /** * Transforms the first row of {@link ResultSet} to required object or returns null. *

    @@ -153,9 +135,9 @@ private static Future failedIfNull(T value, String id, String errorPrefix : Future.failedFuture(new PreBidException(String.format("%s not found: %s", errorPrefix, id))); } - private T toModel(String source, Class targetClass) { + private Account toAccount(String source) { try { - return source != null ? mapper.decodeValue(source, targetClass) : null; + return source != null ? mapper.decodeValue(source, Account.class) : null; } catch (DecodeException e) { throw new PreBidException(e.getMessage()); } @@ -163,53 +145,56 @@ private T toModel(String source, Class targetClass) { /** * Runs a process to get stored requests by a collection of ids from database - * and returns {@link Future<{@link StoredDataResult }>}. + * and returns {@link Future}<{@link StoredDataResult}>. */ @Override - public Future getStoredData(Set requestIds, Set impIds, Timeout timeout) { - return fetchStoredData(selectQuery, requestIds, impIds, timeout); + public Future getStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout); } /** - * Runs a process to get stored responses by a collection of ids from database - * and returns {@link Future<{@link StoredResponseDataResult }>}. + * Runs a process to get stored requests by a collection of amp ids from database + * and returns {@link Future}<{@link StoredDataResult}>. */ @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - final String queryResolvedWithParameters = selectResponseQuery.replaceAll(RESPONSE_ID_PLACEHOLDER, - parameterHolders(responseIds.size())); - - final List idsQueryParameters = new ArrayList<>(); - IntStream.rangeClosed(1, StringUtils.countMatches(selectResponseQuery, RESPONSE_ID_PLACEHOLDER)) - .forEach(i -> idsQueryParameters.addAll(responseIds)); - - return jdbcClient.executeQuery(queryResolvedWithParameters, idsQueryParameters, - result -> JdbcStoredResponseResultMapper.map(result, responseIds), timeout); + public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return fetchStoredData(selectAmpStoredRequestsQuery, accountId, requestIds, Collections.emptySet(), timeout); } /** - * Runs a process to get stored requests by a collection of amp ids from database - * and returns {@link Future<{@link StoredDataResult }>}. + * Runs a process to get stored requests by a collection of video ids from database + * and returns {@link Future}<{@link StoredDataResult}>. */ @Override - public Future getAmpStoredData(Set requestIds, Set impIds, Timeout timeout) { - return fetchStoredData(selectAmpQuery, requestIds, Collections.emptySet(), timeout); + public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, + Timeout timeout) { + return fetchStoredData(selectStoredRequestsQuery, accountId, requestIds, impIds, timeout); } /** - * Runs a process to get stored requests by a collection of video ids from database - * and returns {@link Future<{@link StoredDataResult }>}. + * Runs a process to get stored responses by a collection of ids from database + * and returns {@link Future}<{@link StoredResponseDataResult}>. */ @Override - public Future getVideoStoredData(Set requestIds, Set impIds, Timeout timeout) { - return fetchStoredData(selectQuery, requestIds, impIds, timeout); + public Future getStoredResponses(Set responseIds, Timeout timeout) { + final String queryResolvedWithParameters = selectStoredResponsesQuery.replaceAll(RESPONSE_ID_PLACEHOLDER, + parameterHolders(responseIds.size())); + + final List idsQueryParameters = new ArrayList<>(); + IntStream.rangeClosed(1, StringUtils.countMatches(selectStoredResponsesQuery, RESPONSE_ID_PLACEHOLDER)) + .forEach(i -> idsQueryParameters.addAll(responseIds)); + + return jdbcClient.executeQuery(queryResolvedWithParameters, idsQueryParameters, + result -> JdbcStoredResponseResultMapper.map(result, responseIds), timeout); } /** * Fetches stored requests from database for the given query. */ - private Future fetchStoredData(String query, Set requestIds, Set impIds, - Timeout timeout) { + private Future fetchStoredData(String query, String accountId, Set requestIds, + Set impIds, Timeout timeout) { final Future future; if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { @@ -224,7 +209,7 @@ private Future fetchStoredData(String query, Set reque final String parametrizedQuery = createParametrizedQuery(query, requestIds.size(), impIds.size()); future = jdbcClient.executeQuery(parametrizedQuery, idsQueryParameters, - result -> JdbcStoredDataResultMapper.map(result, requestIds, impIds), + result -> JdbcStoredDataResultMapper.map(result, accountId, requestIds, impIds), timeout); } @@ -247,6 +232,8 @@ private static String createParametrizedQuery(String query, int requestIdsSize, private static String parameterHolders(int paramsSize) { return paramsSize == 0 ? "NULL" - : IntStream.range(0, paramsSize).mapToObj(i -> "?").collect(Collectors.joining(",")); + : IntStream.range(0, paramsSize) + .mapToObj(i -> QUERY_PARAM_PLACEHOLDER) + .collect(Collectors.joining(",")); } } diff --git a/src/main/java/org/prebid/server/settings/SettingsCache.java b/src/main/java/org/prebid/server/settings/SettingsCache.java index b7c532c786a..0bb11a4b9b3 100644 --- a/src/main/java/org/prebid/server/settings/SettingsCache.java +++ b/src/main/java/org/prebid/server/settings/SettingsCache.java @@ -1,9 +1,15 @@ package org.prebid.server.settings; import com.github.benmanes.caffeine.cache.Caffeine; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.settings.model.StoredItem; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -11,15 +17,15 @@ */ public class SettingsCache implements CacheNotificationListener { - private final Map requestCache; - private final Map impCache; + private final Map> requestCache; + private final Map> impCache; public SettingsCache(int ttl, int size) { if (ttl <= 0 || size <= 0) { throw new IllegalArgumentException("ttl and size must be positive"); } - this.requestCache = createCache(ttl, size); - this.impCache = createCache(ttl, size); + requestCache = createCache(ttl, size); + impCache = createCache(ttl, size); } static Map createCache(int ttl, int size) { @@ -30,18 +36,42 @@ static Map createCache(int ttl, int size) { .asMap(); } - Map getRequestCache() { + Map> getRequestCache() { return requestCache; } - Map getImpCache() { + Map> getImpCache() { return impCache; } + void saveRequestCache(String accountId, String requestId, String requestValue) { + saveCachedValue(requestCache, accountId, requestId, requestValue); + } + + void saveImpCache(String accountId, String impId, String impValue) { + saveCachedValue(impCache, accountId, impId, impValue); + } + + private static void saveCachedValue(Map> cache, + String accountId, String id, String value) { + final Set values = ObjectUtils.defaultIfNull(cache.get(id), new HashSet<>()); + values.add(StoredItem.of(accountId, value)); + cache.put(id, values); + } + + /** + * Saves given stored requests and imps for NULL account. + *

    + * TODO: account should be added to all services uses this method + */ @Override public void save(Map requests, Map imps) { - requestCache.putAll(requests); - impCache.putAll(imps); + if (MapUtils.isNotEmpty(requests)) { + requests.forEach((key, value) -> requestCache.put(key, Collections.singleton(StoredItem.of(null, value)))); + } + if (MapUtils.isNotEmpty(imps)) { + imps.forEach((key, value) -> impCache.put(key, Collections.singleton(StoredItem.of(null, value)))); + } } @Override diff --git a/src/main/java/org/prebid/server/settings/helper/JdbcStoredDataResultMapper.java b/src/main/java/org/prebid/server/settings/helper/JdbcStoredDataResultMapper.java new file mode 100644 index 00000000000..77963d034f5 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/JdbcStoredDataResultMapper.java @@ -0,0 +1,159 @@ +package org.prebid.server.settings.helper; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import io.vertx.ext.sql.ResultSet; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredDataType; +import org.prebid.server.settings.model.StoredItem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Utility class for mapping {@link ResultSet} to {@link StoredDataResult}. + */ +public class JdbcStoredDataResultMapper { + + private static final Logger logger = LoggerFactory.getLogger(JdbcStoredDataResultMapper.class); + + private JdbcStoredDataResultMapper() { + } + + /** + * Maps {@link ResultSet} to {@link StoredDataResult} and creates an error for each missing ID and add it to result. + * + * @param resultSet - incoming Result Set representing a result of SQL query + * @param accountId - an account ID extracted from request + * @param requestIds - a specified set of stored requests' IDs. Adds error for each ID missing in result set + * @param impIds - a specified set of stored imps' IDs. Adds error for each ID missing in result set + * @return - a {@link StoredDataResult} object + *

    + * Note: mapper should never throws exception in case of using + * {@link org.prebid.server.vertx.jdbc.CircuitBreakerSecuredJdbcClient}. + */ + public static StoredDataResult map(ResultSet resultSet, String accountId, Set requestIds, + Set impIds) { + final Map storedIdToRequest; + final Map storedIdToImp; + final List errors = new ArrayList<>(); + + if (resultSet == null || CollectionUtils.isEmpty(resultSet.getResults())) { + storedIdToRequest = Collections.emptyMap(); + storedIdToImp = Collections.emptyMap(); + + if (requestIds.isEmpty() && impIds.isEmpty()) { + errors.add("No stored requests or imps were found"); + } else { + final String errorRequests = requestIds.isEmpty() ? "" + : String.format("stored requests for ids %s", requestIds); + final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and "; + final String errorImps = impIds.isEmpty() ? "" : String.format("stored imps for ids %s", impIds); + + errors.add(String.format("No %s%s%s were found", errorRequests, separator, errorImps)); + } + } else { + final Map> requestIdToStoredItems = new HashMap<>(); + final Map> impIdToStoredItems = new HashMap<>(); + + for (JsonArray result : resultSet.getResults()) { + final String fetchedAccountId; + final String id; + final String data; + final String typeAsString; + try { + fetchedAccountId = result.getString(0); + id = result.getString(1); + data = result.getString(2); + typeAsString = result.getString(3); + } catch (IndexOutOfBoundsException | ClassCastException e) { + final String message = "Error occurred while mapping stored request data"; + logger.error(message, e); + errors.add(message); + return StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), errors); + } + + final StoredDataType type; + try { + type = StoredDataType.valueOf(typeAsString); + } catch (IllegalArgumentException e) { + logger.error("Stored request data with id={0} has invalid type: ''{1}'' and will be ignored.", e, + id, typeAsString); + continue; + } + + if (type == StoredDataType.request) { + addStoredItem(fetchedAccountId, id, data, requestIdToStoredItems); + } else { + addStoredItem(fetchedAccountId, id, data, impIdToStoredItems); + } + } + + storedIdToRequest = storedItemsOrAddError(StoredDataType.request, accountId, requestIds, + requestIdToStoredItems, errors); + storedIdToImp = storedItemsOrAddError(StoredDataType.imp, accountId, impIds, + impIdToStoredItems, errors); + } + + return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors); + } + + /** + * Overloaded method for cases when no specific IDs are required, e.g. fetching all records. + * + * @param resultSet - incoming {@link ResultSet} representing a result of SQL query. + * @return - a {@link StoredDataResult} object. + */ + public static StoredDataResult map(ResultSet resultSet) { + return map(resultSet, null, Collections.emptySet(), Collections.emptySet()); + } + + private static void addStoredItem(String accountId, String id, String data, + Map> idToStoredItems) { + final StoredItem storedItem = StoredItem.of(accountId, data); + + final Set storedItems = idToStoredItems.get(id); + if (storedItems == null) { + idToStoredItems.put(id, new HashSet<>(Collections.singleton(storedItem))); + } else { + storedItems.add(storedItem); + } + } + + /** + * Returns map of stored ID -> value or populates error. + */ + private static Map storedItemsOrAddError(StoredDataType type, + String accountId, + Set searchIds, + Map> foundIdToStoredItems, + List errors) { + final Map result = new HashMap<>(); + + if (searchIds.isEmpty()) { + for (Map.Entry> entry : foundIdToStoredItems.entrySet()) { + entry.getValue().forEach(storedItem -> result.put(entry.getKey(), storedItem.getData())); + } + } else { + for (String id : searchIds) { + try { + final StoredItem resolvedStoredItem = StoredItemResolver.resolve(type, accountId, id, + foundIdToStoredItems.get(id)); + result.put(id, resolvedStoredItem.getData()); + } catch (PreBidException e) { + errors.add(e.getMessage()); + } + } + } + + return result; + } +} diff --git a/src/main/java/org/prebid/server/settings/mapper/JdbcStoredResponseResultMapper.java b/src/main/java/org/prebid/server/settings/helper/JdbcStoredResponseResultMapper.java similarity index 97% rename from src/main/java/org/prebid/server/settings/mapper/JdbcStoredResponseResultMapper.java rename to src/main/java/org/prebid/server/settings/helper/JdbcStoredResponseResultMapper.java index b0f1f398571..5acbdee952d 100644 --- a/src/main/java/org/prebid/server/settings/mapper/JdbcStoredResponseResultMapper.java +++ b/src/main/java/org/prebid/server/settings/helper/JdbcStoredResponseResultMapper.java @@ -1,4 +1,4 @@ -package org.prebid.server.settings.mapper; +package org.prebid.server.settings.helper; import io.vertx.core.json.JsonArray; import io.vertx.ext.sql.ResultSet; diff --git a/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java b/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java new file mode 100644 index 00000000000..f173bcff45e --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java @@ -0,0 +1,18 @@ +package org.prebid.server.settings.helper; + +import org.prebid.server.settings.model.StoredDataResult; + +/** + * Interface to satisfy obtaining of {@link StoredDataResult}. + * + * @param account ID + * @param set of stored request IDs + * @param set of stored imp IDs + * @param processing timeout + * @param result of fetching stored data + */ +@FunctionalInterface +public interface StoredDataFetcher { + + R apply(ACC account, REQS reqIds, IMPS impIds, T timeout); +} diff --git a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java new file mode 100644 index 00000000000..a1a7bf1b9ab --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java @@ -0,0 +1,63 @@ +package org.prebid.server.settings.helper; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.settings.model.StoredDataType; +import org.prebid.server.settings.model.StoredItem; + +import java.util.Objects; +import java.util.Set; + +public class StoredItemResolver { + + private StoredItemResolver() { + } + + /** + * Returns {@link StoredItem} which belongs to appropriate account or throw error if not matched. + *

    + * Additional processing involved because incoming prebid request may not have account defined, + * so there are two cases: + *

    + * 1. Multiple stored items were found: + *

    + * - If account is not specified in prebid request - report an error. + *

    + * - Otherwise, find stored item for this account or report an error if no one account matched. + *

    + * 2. One stored stored item was found: + *

    + * - If account is not specified in stored item or found stored item has the same account - use it. + *

    + * - Otherwise, reject stored item as if there hadn't been match. + */ + public static StoredItem resolve(StoredDataType type, String accountId, String id, Set storedItems) { + if (CollectionUtils.isEmpty(storedItems)) { + throw new PreBidException(String.format("No stored %s found for id: %s", type, id)); + } + + // at least one stored item has account + if (storedItems.size() > 1) { + if (StringUtils.isEmpty(accountId)) { + // we cannot choose stored item among multiple without account + throw new PreBidException(String.format( + "Multiple stored %ss found for id: %s but no account was specified", type, id)); + } + return storedItems.stream() + .filter(storedItem -> Objects.equals(storedItem.getAccountId(), accountId)) + .findAny() + .orElseThrow(() -> new PreBidException(String.format( + "No stored %s found among multiple id: %s for account: %s", type, id, accountId))); + } + + // only one stored item found + final StoredItem storedItem = storedItems.iterator().next(); + if (StringUtils.isBlank(accountId) || storedItem.getAccountId() == null + || Objects.equals(accountId, storedItem.getAccountId())) { + return storedItem; + } + throw new PreBidException( + String.format("No stored %s found for id: %s for account: %s", type, id, accountId)); + } +} diff --git a/src/main/java/org/prebid/server/settings/mapper/JdbcStoredDataResultMapper.java b/src/main/java/org/prebid/server/settings/mapper/JdbcStoredDataResultMapper.java deleted file mode 100644 index 45d432a8838..00000000000 --- a/src/main/java/org/prebid/server/settings/mapper/JdbcStoredDataResultMapper.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.prebid.server.settings.mapper; - -import io.vertx.core.json.JsonArray; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import io.vertx.ext.sql.ResultSet; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.settings.model.StoredDataResult; -import org.prebid.server.settings.model.StoredDataType; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Utility class that maps {@link ResultSet} to {@link StoredDataResult}. - */ -public class JdbcStoredDataResultMapper { - - private static final Logger logger = LoggerFactory.getLogger(JdbcStoredDataResultMapper.class); - - private JdbcStoredDataResultMapper() { - } - - /** - * Maps {@link ResultSet} to {@link StoredDataResult} and creates an error for each missing id and add it to result. - * - * @param resultSet - incoming Result Set representing a result of SQL query - * @param requestIds - a specified set of stored requests' ids. Adds error for each ID missing in result set - * @param impIds - a specified set of stored imps' ids. Adds error for each ID missing in result set - * @return - a {@link StoredDataResult} object - *

    - * Note: mapper should never throws exception in case of using - * {@link org.prebid.server.vertx.jdbc.CircuitBreakerSecuredJdbcClient}. - */ - public static StoredDataResult map(ResultSet resultSet, Set requestIds, Set impIds) { - final Map storedIdToRequest = new HashMap<>(requestIds.size()); - final Map storedIdToImp = new HashMap<>(impIds.size()); - final List errors = new ArrayList<>(); - - if (resultSet == null || CollectionUtils.isEmpty(resultSet.getResults())) { - if (requestIds.isEmpty() && impIds.isEmpty()) { - errors.add("No stored requests or imps found"); - } else { - final String errorRequests = requestIds.isEmpty() ? "" - : String.format("stored requests for ids %s", requestIds); - final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and "; - final String errorImps = impIds.isEmpty() ? "" : String.format("stored imps for ids %s", impIds); - - errors.add(String.format("No %s%s%s were found", errorRequests, separator, errorImps)); - } - } else { - for (JsonArray result : resultSet.getResults()) { - final String id; - final String json; - final String typeAsString; - try { - id = result.getString(0); - json = result.getString(1); - typeAsString = result.getString(2); - } catch (IndexOutOfBoundsException e) { - errors.add("Result set column number is less than expected"); - return StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), errors); - } - - final StoredDataType type; - try { - type = StoredDataType.valueOf(typeAsString); - } catch (IllegalArgumentException e) { - logger.error("Result set with id={0} has invalid type: {1}. This will be ignored.", e, id, - typeAsString); - continue; - } - - if (type == StoredDataType.request) { - storedIdToRequest.put(id, json); - } else { - storedIdToImp.put(id, json); - } - } - - errors.addAll(errorsForMissedIds(requestIds, storedIdToRequest, StoredDataType.request)); - errors.addAll(errorsForMissedIds(impIds, storedIdToImp, StoredDataType.imp)); - } - - return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors); - } - - /** - * Overloaded method for cases when no specific IDs are required, e.g. fetching all records. - * - * @param resultSet - incoming Result Set representing a result of SQL query - * @return - a {@link StoredDataResult} object - */ - public static StoredDataResult map(ResultSet resultSet) { - return map(resultSet, Collections.emptySet(), Collections.emptySet()); - } - - /** - * Return errors for missed IDs. - */ - private static List errorsForMissedIds(Set ids, Map storedIdToJson, - StoredDataType type) { - final List missedIds = ids.stream() - .filter(id -> !storedIdToJson.containsKey(id)) - .collect(Collectors.toList()); - - return missedIds.isEmpty() ? Collections.emptyList() : missedIds.stream() - .map(id -> String.format("No stored %s found for id: %s", type, id)) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/org/prebid/server/settings/model/Account.java b/src/main/java/org/prebid/server/settings/model/Account.java index 0d011c9df31..9b5a550b8de 100644 --- a/src/main/java/org/prebid/server/settings/model/Account.java +++ b/src/main/java/org/prebid/server/settings/model/Account.java @@ -1,33 +1,27 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Value; -@Builder +@Builder(toBuilder = true) @Value public class Account { String id; - String priceGranularity; + AccountStatus status; - Integer bannerCacheTtl; + AccountAuctionConfig auction; - Integer videoCacheTtl; + AccountPrivacyConfig privacy; - Boolean eventsEnabled; + AccountAnalyticsConfig analytics; - Boolean enforceCcpa; + @JsonProperty("cookie-sync") + AccountCookieSyncConfig cookieSync; - AccountGdprConfig gdpr; - - Integer analyticsSamplingFactor; - - Integer truncateTargetAttr; - - String defaultIntegration; - - AccountAnalyticsConfig analyticsConfig; + AccountHooksConfiguration hooks; public static Account empty(String id) { return Account.builder() diff --git a/src/main/java/org/prebid/server/settings/model/AccountAnalyticsConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAnalyticsConfig.java index cd0d9ad01e9..2b431e1fe71 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAnalyticsConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAnalyticsConfig.java @@ -1,6 +1,7 @@ package org.prebid.server.settings.model; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; import java.util.Collections; @@ -10,7 +11,7 @@ @Value(staticConstructor = "of") public class AccountAnalyticsConfig { - private static final AccountAnalyticsConfig FALLBACK; + private static final Map FALLBACK_AUCTION_EVENTS; static { final Map events = new HashMap<>(); @@ -18,13 +19,15 @@ public class AccountAnalyticsConfig { events.put("amp", true); events.put("app", true); - FALLBACK = AccountAnalyticsConfig.of(Collections.unmodifiableMap(events)); + FALLBACK_AUCTION_EVENTS = Collections.unmodifiableMap(events); } @JsonProperty("auction-events") Map auctionEvents; - public static AccountAnalyticsConfig fallback() { - return FALLBACK; + Map modules; + + public static Map fallbackAuctionEvents() { + return FALLBACK_AUCTION_EVENTS; } } diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java new file mode 100644 index 00000000000..e666958ab97 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -0,0 +1,30 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class AccountAuctionConfig { + + @JsonProperty("price-granularity") + String priceGranularity; + + @JsonProperty("banner-cache-ttl") + Integer bannerCacheTtl; + + @JsonProperty("video-cache-ttl") + Integer videoCacheTtl; + + @JsonProperty("truncate-target-attr") + Integer truncateTargetAttr; + + @JsonProperty("default-integration") + String defaultIntegration; + + @JsonProperty("bid-validations") + AccountBidValidationConfig bidValidations; + + AccountEventsConfig events; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java b/src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java new file mode 100644 index 00000000000..38c5afb2bfb --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountBidValidationConfig.java @@ -0,0 +1,11 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountBidValidationConfig { + + @JsonProperty("banner-creative-max-size") + BidValidationEnforcement bannerMaxSizeEnforcement; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountCcpaConfig.java b/src/main/java/org/prebid/server/settings/model/AccountCcpaConfig.java new file mode 100644 index 00000000000..ec009aa7e5d --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountCcpaConfig.java @@ -0,0 +1,20 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class AccountCcpaConfig { + + @JsonProperty("enabled") + Boolean enabled; + + @JsonProperty("integration-enabled") + EnabledForRequestType enabledForRequestType; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java b/src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java new file mode 100644 index 00000000000..574f7798772 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountCookieSyncConfig.java @@ -0,0 +1,17 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountCookieSyncConfig { + + @JsonProperty("default-limit") + Integer defaultLimit; + + @JsonProperty("max-limit") + Integer maxLimit; + + @JsonProperty("default-coop-sync") + Boolean defaultCoopSync; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountEventsConfig.java b/src/main/java/org/prebid/server/settings/model/AccountEventsConfig.java new file mode 100644 index 00000000000..669dd6b9baf --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountEventsConfig.java @@ -0,0 +1,9 @@ +package org.prebid.server.settings.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountEventsConfig { + + Boolean enabled; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java b/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java index 62293e59059..65760b8cd49 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountGdprConfig.java @@ -1,15 +1,13 @@ package org.prebid.server.settings.model; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.Value; + +import java.util.List; @Builder -@AllArgsConstructor -@NoArgsConstructor -@Data +@Value public class AccountGdprConfig { @JsonProperty("enabled") @@ -25,4 +23,7 @@ public class AccountGdprConfig { @JsonProperty("purpose-one-treatment-interpretation") PurposeOneTreatmentInterpretation purposeOneTreatmentInterpretation; + + @JsonProperty("basic-enforcement-vendors") + List basicEnforcementVendors; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java b/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java new file mode 100644 index 00000000000..9d7788b084b --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java @@ -0,0 +1,17 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; +import org.prebid.server.hooks.execution.model.ExecutionPlan; + +import java.util.Map; + +@Value(staticConstructor = "of") +public class AccountHooksConfiguration { + + @JsonProperty("execution-plan") + ExecutionPlan executionPlan; + + Map modules; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountPrivacyConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPrivacyConfig.java new file mode 100644 index 00000000000..3b41cdf53b6 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountPrivacyConfig.java @@ -0,0 +1,15 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountPrivacyConfig { + + @JsonProperty("enforce-ccpa") + Boolean enforceCcpa; + + AccountGdprConfig gdpr; + + AccountCcpaConfig ccpa; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountStatus.java b/src/main/java/org/prebid/server/settings/model/AccountStatus.java new file mode 100644 index 00000000000..0ac0ebcb66f --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountStatus.java @@ -0,0 +1,6 @@ +package org.prebid.server.settings.model; + +public enum AccountStatus { + + active, inactive +} diff --git a/src/main/java/org/prebid/server/settings/model/AdUnitConfig.java b/src/main/java/org/prebid/server/settings/model/AdUnitConfig.java deleted file mode 100644 index a3542a487cb..00000000000 --- a/src/main/java/org/prebid/server/settings/model/AdUnitConfig.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.settings.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class AdUnitConfig { - - String id; - - String config; -} diff --git a/src/main/java/org/prebid/server/settings/model/BidValidationEnforcement.java b/src/main/java/org/prebid/server/settings/model/BidValidationEnforcement.java new file mode 100644 index 00000000000..f5ed5a6f510 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/BidValidationEnforcement.java @@ -0,0 +1,6 @@ +package org.prebid.server.settings.model; + +public enum BidValidationEnforcement { + + skip, enforce, warn +} diff --git a/src/main/java/org/prebid/server/settings/model/GdprConfig.java b/src/main/java/org/prebid/server/settings/model/GdprConfig.java index 1c57a04c4d5..c1ddf431e71 100644 --- a/src/main/java/org/prebid/server/settings/model/GdprConfig.java +++ b/src/main/java/org/prebid/server/settings/model/GdprConfig.java @@ -5,11 +5,15 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotBlank; @Builder @AllArgsConstructor @NoArgsConstructor @Data +@Validated public class GdprConfig { @JsonProperty("host-vendor-id") @@ -17,6 +21,7 @@ public class GdprConfig { Boolean enabled; + @NotBlank @JsonProperty("default-value") String defaultValue; diff --git a/src/main/java/org/prebid/server/settings/model/SettingsFile.java b/src/main/java/org/prebid/server/settings/model/SettingsFile.java index bf3dc3cd74d..d757cf19dde 100644 --- a/src/main/java/org/prebid/server/settings/model/SettingsFile.java +++ b/src/main/java/org/prebid/server/settings/model/SettingsFile.java @@ -9,7 +9,5 @@ public class SettingsFile { List accounts; - List configs; - List domains; } diff --git a/src/main/java/org/prebid/server/settings/model/StoredItem.java b/src/main/java/org/prebid/server/settings/model/StoredItem.java new file mode 100644 index 00000000000..5b639a30cdd --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/StoredItem.java @@ -0,0 +1,16 @@ +package org.prebid.server.settings.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +/** + * The model helps to reduce multiple rows found for single stored request/imp ID. + */ +@AllArgsConstructor(staticName = "of") +@Value +public class StoredItem { + + String accountId; + + String data; +} diff --git a/src/main/java/org/prebid/server/settings/model/StoredResponseDataResult.java b/src/main/java/org/prebid/server/settings/model/StoredResponseDataResult.java index 8c336bcebb3..ad27eda7954 100644 --- a/src/main/java/org/prebid/server/settings/model/StoredResponseDataResult.java +++ b/src/main/java/org/prebid/server/settings/model/StoredResponseDataResult.java @@ -10,7 +10,7 @@ @Value public class StoredResponseDataResult { - Map storedSeatBid; + Map idToStoredResponses; List errors; } diff --git a/src/main/java/org/prebid/server/settings/model/TriFunction.java b/src/main/java/org/prebid/server/settings/model/TriFunction.java deleted file mode 100644 index 4ab63a95824..00000000000 --- a/src/main/java/org/prebid/server/settings/model/TriFunction.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.settings.model; - -/** - * Interface to satisfy obtaining {@link StoredDataResult} - * - * @param set of stored request IDs - * @param set of stored imp IDs - * @param processing timeout - * @param result of fetching stored data - */ -@FunctionalInterface -public interface TriFunction { - - R apply(T t, U u, V v); -} diff --git a/src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java b/src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java new file mode 100644 index 00000000000..a893b2dc01a --- /dev/null +++ b/src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java @@ -0,0 +1,14 @@ +package org.prebid.server.settings.proto.response; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.prebid.server.settings.model.Account; + +import java.util.Map; + +@AllArgsConstructor(staticName = "of") +@Value +public class HttpAccountsResponse { + + Map accounts; +} diff --git a/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java b/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java index 9608e9167d7..256e9d3273e 100644 --- a/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java +++ b/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java @@ -10,7 +10,7 @@ @Value public class HttpFetcherResponse { - private Map requests; + Map requests; - private Map imps; + Map imps; } diff --git a/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java index a1e2b0f7e41..035a03fa028 100644 --- a/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java +++ b/src/main/java/org/prebid/server/settings/service/JdbcPeriodicRefreshService.java @@ -7,12 +7,15 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; import org.prebid.server.settings.CacheNotificationListener; -import org.prebid.server.settings.mapper.JdbcStoredDataResultMapper; +import org.prebid.server.settings.helper.JdbcStoredDataResultMapper; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.vertx.Initializable; import org.prebid.server.vertx.jdbc.JdbcClient; +import java.time.Clock; import java.time.Instant; import java.util.Collections; import java.util.Date; @@ -42,11 +45,6 @@ public class JdbcPeriodicRefreshService implements Initializable { private static final Logger logger = LoggerFactory.getLogger(JdbcPeriodicRefreshService.class); - private final CacheNotificationListener cacheNotificationListener; - private final Vertx vertx; - private final JdbcClient jdbcClient; - private final long refreshPeriod; - /** * Example of initialize query: *

    @@ -56,9 +54,8 @@ public class JdbcPeriodicRefreshService implements Initializable {
          * This query will be run once on startup to fetch _all_ known Stored Request data from the database.
          */
         private final String initQuery;
    -
         /**
    -     * Example of initialize query:
    +     * Example of update query:
          * 
          * SELECT id, requestData, type
          * FROM stored_requests
    @@ -68,21 +65,41 @@ public class JdbcPeriodicRefreshService implements Initializable {
          * Wildcard "?" would be used to pass last update date automatically.
          */
         private final String updateQuery;
    -    private final TimeoutFactory timeoutFactory;
    +    private final long refreshPeriod;
         private final long timeout;
    +    private final MetricName cacheType;
    +    private final CacheNotificationListener cacheNotificationListener;
    +    private final Vertx vertx;
    +    private final JdbcClient jdbcClient;
    +    private final TimeoutFactory timeoutFactory;
    +    private final Metrics metrics;
    +    private final Clock clock;
    +
         private Instant lastUpdate;
     
    -    public JdbcPeriodicRefreshService(CacheNotificationListener cacheNotificationListener,
    -                                      Vertx vertx, JdbcClient jdbcClient, long refreshPeriod, String initQuery,
    -                                      String updateQuery, TimeoutFactory timeoutFactory, long timeout) {
    +    public JdbcPeriodicRefreshService(String initQuery,
    +                                      String updateQuery,
    +                                      long refreshPeriod,
    +                                      long timeout,
    +                                      MetricName cacheType,
    +                                      CacheNotificationListener cacheNotificationListener,
    +                                      Vertx vertx,
    +                                      JdbcClient jdbcClient,
    +                                      TimeoutFactory timeoutFactory,
    +                                      Metrics metrics,
    +                                      Clock clock) {
    +
    +        this.initQuery = Objects.requireNonNull(StringUtils.stripToNull(initQuery));
    +        this.updateQuery = Objects.requireNonNull(StringUtils.stripToNull(updateQuery));
    +        this.refreshPeriod = refreshPeriod;
    +        this.timeout = timeout;
    +        this.cacheType = Objects.requireNonNull(cacheType);
             this.cacheNotificationListener = Objects.requireNonNull(cacheNotificationListener);
             this.vertx = Objects.requireNonNull(vertx);
             this.jdbcClient = Objects.requireNonNull(jdbcClient);
    -        this.refreshPeriod = refreshPeriod;
    -        this.initQuery = Objects.requireNonNull(StringUtils.stripToNull(initQuery));
    -        this.updateQuery = Objects.requireNonNull(StringUtils.stripToNull(updateQuery));
             this.timeoutFactory = Objects.requireNonNull(timeoutFactory);
    -        this.timeout = timeout;
    +        this.metrics = Objects.requireNonNull(metrics);
    +        this.clock = Objects.requireNonNull(clock);
         }
     
         @Override
    @@ -94,36 +111,52 @@ public void initialize() {
         }
     
         private void getAll() {
    -        jdbcClient.executeQuery(initQuery, Collections.emptyList(), JdbcStoredDataResultMapper::map, createTimeout())
    -                .map(this::save)
    -                .map(ignored -> setLastUpdate(Instant.now()))
    -                .recover(JdbcPeriodicRefreshService::failResponse);
    +        final long startTime = clock.millis();
    +
    +        jdbcClient.executeQuery(
    +                initQuery,
    +                Collections.emptyList(),
    +                JdbcStoredDataResultMapper::map,
    +                createTimeout())
    +                .map(storedDataResult ->
    +                        handleResult(storedDataResult, Instant.now(clock), startTime, MetricName.initialize))
    +                .recover(exception -> handleFailure(exception, startTime, MetricName.initialize));
         }
     
    -    private Void save(StoredDataResult storedDataResult) {
    +    private Void handleResult(StoredDataResult storedDataResult,
    +                              Instant updateTime,
    +                              long startTime,
    +                              MetricName refreshType) {
    +
             cacheNotificationListener.save(storedDataResult.getStoredIdToRequest(), storedDataResult.getStoredIdToImp());
    -        return null;
    -    }
    +        lastUpdate = updateTime;
    +
    +        metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime);
     
    -    private Void setLastUpdate(Instant instant) {
    -        lastUpdate = instant;
             return null;
         }
     
    -    private static Future failResponse(Throwable exception) {
    +    private Future handleFailure(Throwable exception, long startTime, MetricName refreshType) {
             logger.warn("Error occurred while request to jdbc refresh service", exception);
    +
    +        metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime);
    +        metrics.updateSettingsCacheRefreshErrorMetric(cacheType, refreshType);
    +
             return Future.failedFuture(exception);
         }
     
         private void refresh() {
    -        final Instant updateTime = Instant.now();
    -
    -        jdbcClient.executeQuery(updateQuery, Collections.singletonList(Date.from(lastUpdate)),
    -                JdbcStoredDataResultMapper::map, createTimeout())
    -                .map(this::invalidate)
    -                .map(this::save)
    -                .map(ignored -> setLastUpdate(updateTime))
    -                .recover(JdbcPeriodicRefreshService::failResponse);
    +        final Instant updateTime = Instant.now(clock);
    +        final long startTime = clock.millis();
    +
    +        jdbcClient.executeQuery(
    +                updateQuery,
    +                Collections.singletonList(Date.from(lastUpdate)),
    +                JdbcStoredDataResultMapper::map,
    +                createTimeout())
    +                .map(storedDataResult ->
    +                        handleResult(invalidate(storedDataResult), updateTime, startTime, MetricName.update))
    +                .recover(exception -> handleFailure(exception, startTime, MetricName.update));
         }
     
         private StoredDataResult invalidate(StoredDataResult storedDataResult) {
    diff --git a/src/main/java/org/prebid/server/spring/config/AdminEndpointsConfiguration.java b/src/main/java/org/prebid/server/spring/config/AdminEndpointsConfiguration.java
    index 091d48152aa..667dd5b9307 100644
    --- a/src/main/java/org/prebid/server/spring/config/AdminEndpointsConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/AdminEndpointsConfiguration.java
    @@ -4,21 +4,29 @@
     import lombok.NoArgsConstructor;
     import org.apache.commons.lang3.ObjectUtils;
     import org.prebid.server.currency.CurrencyConversionService;
    +import org.prebid.server.deals.DeliveryProgressService;
    +import org.prebid.server.deals.simulation.DealsSimulationAdminHandler;
     import org.prebid.server.handler.AccountCacheInvalidationHandler;
     import org.prebid.server.handler.CurrencyRatesHandler;
     import org.prebid.server.handler.CustomizedAdminEndpoint;
    +import org.prebid.server.handler.DealsStatusHandler;
     import org.prebid.server.handler.HttpInteractionLogHandler;
    +import org.prebid.server.handler.LineItemStatusHandler;
     import org.prebid.server.handler.LoggerControlKnobHandler;
     import org.prebid.server.handler.SettingsCacheNotificationHandler;
    +import org.prebid.server.handler.TracerLogHandler;
     import org.prebid.server.handler.VersionHandler;
     import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.log.CriteriaManager;
     import org.prebid.server.log.HttpInteractionLogger;
     import org.prebid.server.log.LoggerControlKnob;
     import org.prebid.server.settings.CachingApplicationSettings;
     import org.prebid.server.settings.SettingsCache;
    +import org.prebid.server.util.VersionInfo;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.beans.factory.annotation.Value;
     import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
     import org.springframework.boot.context.properties.ConfigurationProperties;
     import org.springframework.context.annotation.Bean;
     import org.springframework.context.annotation.Configuration;
    @@ -33,6 +41,7 @@ public class AdminEndpointsConfiguration {
         @Bean
         @ConditionalOnExpression("${admin-endpoints.version.enabled} == true")
         CustomizedAdminEndpoint versionEndpoint(
    +            VersionInfo versionInfo,
                 JacksonMapper mapper,
                 @Value("${admin-endpoints.version.path}") String path,
                 @Value("${admin-endpoints.version.on-application-port}") boolean isOnApplicationPort,
    @@ -41,7 +50,7 @@ CustomizedAdminEndpoint versionEndpoint(
     
             return new CustomizedAdminEndpoint(
                     path,
    -                VersionHandler.create("git-revision.json", mapper),
    +                new VersionHandler(versionInfo.getVersion(), versionInfo.getCommitHash(), mapper, path),
                     isOnApplicationPort,
                     isProtected)
                     .withCredentials(adminEndpointCredentials);
    @@ -60,7 +69,7 @@ CustomizedAdminEndpoint currencyConversionRatesEndpoint(
     
             return new CustomizedAdminEndpoint(
                     path,
    -                new CurrencyRatesHandler(currencyConversionRates, mapper),
    +                new CurrencyRatesHandler(currencyConversionRates, path, mapper),
                     isOnApplicationPort,
                     isProtected)
                     .withCredentials(adminEndpointCredentials);
    @@ -79,7 +88,7 @@ CustomizedAdminEndpoint cacheNotificationEndpoint(
     
             return new CustomizedAdminEndpoint(
                     path,
    -                new SettingsCacheNotificationHandler(settingsCache, mapper),
    +                new SettingsCacheNotificationHandler(settingsCache, mapper, path),
                     isOnApplicationPort,
                     isProtected)
                     .withCredentials(adminEndpointCredentials);
    @@ -98,7 +107,7 @@ CustomizedAdminEndpoint ampCacheNotificationEndpoint(
     
             return new CustomizedAdminEndpoint(
                     path,
    -                new SettingsCacheNotificationHandler(ampSettingsCache, mapper),
    +                new SettingsCacheNotificationHandler(ampSettingsCache, mapper, path),
                     isOnApplicationPort,
                     isProtected)
                     .withCredentials(adminEndpointCredentials);
    @@ -116,7 +125,7 @@ CustomizedAdminEndpoint cacheInvalidateNotificationEndpoint(
     
             return new CustomizedAdminEndpoint(
                     path,
    -                new AccountCacheInvalidationHandler(cachingApplicationSettings),
    +                new AccountCacheInvalidationHandler(cachingApplicationSettings, path),
                     isOnApplicationPort,
                     isProtected)
                     .withCredentials(adminEndpointCredentials);
    @@ -134,7 +143,7 @@ CustomizedAdminEndpoint loggingHttpInteractionEndpoint(
     
             return new CustomizedAdminEndpoint(
                     path,
    -                new HttpInteractionLogHandler(maxLimit, httpInteractionLogger),
    +                new HttpInteractionLogHandler(maxLimit, httpInteractionLogger, path),
                     isOnApplicationPort,
                     isProtected)
                     .withCredentials(adminEndpointCredentials);
    @@ -152,7 +161,78 @@ CustomizedAdminEndpoint loggingChangeLevelEndpoint(
     
             return new CustomizedAdminEndpoint(
                     path,
    -                new LoggerControlKnobHandler(maxDuration, loggerControlKnob),
    +                new LoggerControlKnobHandler(maxDuration, loggerControlKnob, path),
    +                isOnApplicationPort,
    +                isProtected)
    +                .withCredentials(adminEndpointCredentials);
    +    }
    +
    +    @Bean
    +    @ConditionalOnProperty(prefix = "admin-endpoints.tracelog", name = "enabled", havingValue = "true")
    +    CustomizedAdminEndpoint tracerLogEndpoint(
    +            CriteriaManager criteriaManager,
    +            @Value("${admin-endpoints.tracelog.path}") String path,
    +            @Value("${admin-endpoints.tracelog.on-application-port}") boolean isOnApplicationPort,
    +            @Value("${admin-endpoints.tracelog.protected}") boolean isProtected,
    +            @Autowired(required = false) Map adminEndpointCredentials) {
    +
    +        return new CustomizedAdminEndpoint(
    +                path,
    +                new TracerLogHandler(criteriaManager),
    +                isOnApplicationPort,
    +                isProtected)
    +                .withCredentials(adminEndpointCredentials);
    +    }
    +
    +    @Bean
    +    @ConditionalOnExpression("${deals.enabled} == true and ${admin-endpoints.deals-status.enabled} == true")
    +    CustomizedAdminEndpoint dealsStatusEndpoint(
    +            DeliveryProgressService deliveryProgressService,
    +            JacksonMapper mapper,
    +            @Value("${admin-endpoints.deals-status.path}") String path,
    +            @Value("${admin-endpoints.deals-status.on-application-port}") boolean isOnApplicationPort,
    +            @Value("${admin-endpoints.deals-status.protected}") boolean isProtected,
    +            @Autowired(required = false) Map adminEndpointCredentials) {
    +
    +        return new CustomizedAdminEndpoint(
    +                path,
    +                new DealsStatusHandler(deliveryProgressService, mapper),
    +                isOnApplicationPort,
    +                isProtected)
    +                .withCredentials(adminEndpointCredentials);
    +    }
    +
    +    @Bean
    +    @ConditionalOnExpression("${deals.enabled} == true and ${admin-endpoints.lineitem-status.enabled} == true")
    +    CustomizedAdminEndpoint lineItemStatusEndpoint(
    +            DeliveryProgressService deliveryProgressService,
    +            JacksonMapper mapper,
    +            @Value("${admin-endpoints.lineitem-status.path}") String path,
    +            @Value("${admin-endpoints.lineitem-status.on-application-port}") boolean isOnApplicationPort,
    +            @Value("${admin-endpoints.lineitem-status.protected}") boolean isProtected,
    +            @Autowired(required = false) Map adminEndpointCredentials) {
    +
    +        return new CustomizedAdminEndpoint(
    +                path,
    +                new LineItemStatusHandler(deliveryProgressService, mapper, path),
    +                isOnApplicationPort,
    +                isProtected)
    +                .withCredentials(adminEndpointCredentials);
    +    }
    +
    +    @Bean
    +    @ConditionalOnExpression("${deals.enabled} == true and ${deals.simulation.enabled} == true"
    +            + " and ${admin-endpoints.e2eadmin.enabled} == true")
    +    CustomizedAdminEndpoint dealsSimulationAdminEndpoint(
    +            DealsSimulationAdminHandler dealsSimulationAdminHandler,
    +            @Value("${admin-endpoints.e2eadmin.path}") String path,
    +            @Value("${admin-endpoints.e2eadmin.on-application-port}") boolean isOnApplicationPort,
    +            @Value("${admin-endpoints.e2eadmin.protected}") boolean isProtected,
    +            @Autowired(required = false) Map adminEndpointCredentials) {
    +
    +        return new CustomizedAdminEndpoint(
    +                path,
    +                dealsSimulationAdminHandler,
                     isOnApplicationPort,
                     isProtected)
                     .withCredentials(adminEndpointCredentials);
    diff --git a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java
    index c2bee2f928a..4eb1e345414 100644
    --- a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java
    @@ -1,15 +1,24 @@
     package org.prebid.server.spring.config;
     
     import io.vertx.core.Vertx;
    +import lombok.Data;
    +import lombok.NoArgsConstructor;
     import org.prebid.server.analytics.AnalyticsReporter;
    -import org.prebid.server.analytics.CompositeAnalyticsReporter;
    +import org.prebid.server.analytics.AnalyticsReporterDelegator;
     import org.prebid.server.analytics.LogAnalyticsReporter;
    +import org.prebid.server.analytics.pubstack.PubstackAnalyticsReporter;
    +import org.prebid.server.auction.PrivacyEnforcementService;
     import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.metric.Metrics;
    +import org.prebid.server.vertx.http.HttpClient;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
     import org.springframework.context.annotation.Bean;
     import org.springframework.context.annotation.Configuration;
    +import org.springframework.validation.annotation.Validated;
     
    +import javax.validation.constraints.NotNull;
     import java.util.Collections;
     import java.util.List;
     
    @@ -17,10 +26,17 @@
     public class AnalyticsConfiguration {
     
         @Bean
    -    CompositeAnalyticsReporter compositeAnalyticsReporter(
    -            @Autowired(required = false) List delegates, Vertx vertx) {
    +    AnalyticsReporterDelegator analyticsReporterDelegator(
    +            @Autowired(required = false) List delegates,
    +            Vertx vertx,
    +            PrivacyEnforcementService privacyEnforcementService,
    +            Metrics metrics) {
     
    -        return new CompositeAnalyticsReporter(delegates != null ? delegates : Collections.emptyList(), vertx);
    +        return new AnalyticsReporterDelegator(
    +                delegates != null ? delegates : Collections.emptyList(),
    +                vertx,
    +                privacyEnforcementService,
    +                metrics);
         }
     
         @Bean
    @@ -28,4 +44,77 @@ CompositeAnalyticsReporter compositeAnalyticsReporter(
         LogAnalyticsReporter logAnalyticsReporter(JacksonMapper mapper) {
             return new LogAnalyticsReporter(mapper);
         }
    +
    +    @Configuration
    +    @ConditionalOnProperty(prefix = "analytics.pubstack", name = "enabled", havingValue = "true")
    +    public static class PubstackAnalyticsConfiguration {
    +
    +        @Bean
    +        PubstackAnalyticsReporter pubstackAnalyticsReporter(PubstackAnalyticsProperties pubstackAnalyticsProperties,
    +                                                            HttpClient httpClient,
    +                                                            JacksonMapper jacksonMapper,
    +                                                            Vertx vertx) {
    +            return new PubstackAnalyticsReporter(
    +                    pubstackAnalyticsProperties.toComponentProperties(),
    +                    httpClient,
    +                    jacksonMapper,
    +                    vertx);
    +        }
    +
    +        @Bean
    +        @ConfigurationProperties(prefix = "analytics.pubstack")
    +        PubstackAnalyticsProperties pubstackAnalyticsProperties() {
    +            return new PubstackAnalyticsProperties();
    +        }
    +
    +        @Validated
    +        @NoArgsConstructor
    +        @Data
    +        private static class PubstackAnalyticsProperties {
    +            @NotNull
    +            String endpoint;
    +
    +            @NotNull
    +            String scopeid;
    +
    +            @NotNull
    +            Boolean enabled;
    +
    +            @NotNull
    +            Long configurationRefreshDelayMs;
    +
    +            @NotNull
    +            Long timeoutMs;
    +
    +            @NotNull
    +            PubstackBufferProperties buffers;
    +
    +            public org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties toComponentProperties() {
    +                return org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties.builder()
    +                        .endpoint(getEndpoint())
    +                        .scopeId(getScopeid())
    +                        .enabled(getEnabled())
    +                        .configurationRefreshDelayMs(getConfigurationRefreshDelayMs())
    +                        .sizeBytes(getBuffers().getSizeBytes())
    +                        .count(getBuffers().getCount())
    +                        .timeoutMs(getTimeoutMs())
    +                        .reportTtlMs(getBuffers().getReportTtlMs())
    +                        .build();
    +            }
    +        }
    +
    +        @Validated
    +        @Data
    +        @NoArgsConstructor
    +        private static class PubstackBufferProperties {
    +            @NotNull
    +            Integer sizeBytes;
    +
    +            @NotNull
    +            Integer count;
    +
    +            @NotNull
    +            Long reportTtlMs;
    +        }
    +    }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/AopConfiguration.java b/src/main/java/org/prebid/server/spring/config/AopConfiguration.java
    new file mode 100644
    index 00000000000..bf3d00762e7
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/AopConfiguration.java
    @@ -0,0 +1,52 @@
    +package org.prebid.server.spring.config;
    +
    +import io.vertx.core.Future;
    +import org.aspectj.lang.ProceedingJoinPoint;
    +import org.aspectj.lang.annotation.Around;
    +import org.aspectj.lang.annotation.Aspect;
    +import org.prebid.server.health.HealthMonitor;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.stereotype.Component;
    +
    +@Configuration
    +public class AopConfiguration {
    +
    +    @Bean
    +    HealthMonitor healthMonitor() {
    +        return new HealthMonitor();
    +    }
    +
    +    @Aspect
    +    @Component
    +    static class HealthMonitorAspect {
    +
    +        @Autowired
    +        HealthMonitor healthMonitor;
    +
    +        @Around(value = "execution(* org.prebid.server.vertx.http.HttpClient.*(..)) "
    +                + "|| execution(* org.prebid.server.settings.ApplicationSettings.*(..)) "
    +                + "|| execution(* org.prebid.server.geolocation.GeoLocationService.*(..))")
    +        public Future around(ProceedingJoinPoint joinPoint) {
    +            try {
    +                return ((Future) joinPoint.proceed())
    +                        .map(this::handleSucceedRequest)
    +                        .recover(this::handleFailRequest);
    +            } catch (Throwable throwable) {
    +                throw new IllegalStateException("Error while processing health monitoring", throwable);
    +            }
    +        }
    +
    +        private  Future handleFailRequest(Throwable throwable) {
    +            healthMonitor.incTotal();
    +            return Future.failedFuture(throwable);
    +        }
    +
    +        private  T handleSucceedRequest(T result) {
    +            healthMonitor.incTotal();
    +            healthMonitor.incSuccess();
    +            return result;
    +        }
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/DealsConfiguration.java b/src/main/java/org/prebid/server/spring/config/DealsConfiguration.java
    new file mode 100644
    index 00000000000..52b19531049
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/DealsConfiguration.java
    @@ -0,0 +1,887 @@
    +package org.prebid.server.spring.config;
    +
    +import io.vertx.core.Vertx;
    +import io.vertx.core.eventbus.EventBus;
    +import lombok.Data;
    +import lombok.NoArgsConstructor;
    +import org.prebid.server.bidder.BidderCatalog;
    +import org.prebid.server.bidder.BidderErrorNotifier;
    +import org.prebid.server.bidder.BidderRequestCompletionTrackerFactory;
    +import org.prebid.server.bidder.DealsBidderRequestCompletionTrackerFactory;
    +import org.prebid.server.bidder.HttpBidderRequestEnricher;
    +import org.prebid.server.bidder.HttpBidderRequester;
    +import org.prebid.server.currency.CurrencyConversionService;
    +import org.prebid.server.deals.AdminCentralService;
    +import org.prebid.server.deals.AlertHttpService;
    +import org.prebid.server.deals.DealsProcessor;
    +import org.prebid.server.deals.DeliveryProgressReportFactory;
    +import org.prebid.server.deals.DeliveryProgressService;
    +import org.prebid.server.deals.DeliveryStatsService;
    +import org.prebid.server.deals.LineItemService;
    +import org.prebid.server.deals.PlannerService;
    +import org.prebid.server.deals.RegisterService;
    +import org.prebid.server.deals.Suspendable;
    +import org.prebid.server.deals.TargetingService;
    +import org.prebid.server.deals.UserService;
    +import org.prebid.server.deals.deviceinfo.DeviceInfoService;
    +import org.prebid.server.deals.events.AdminEventProcessor;
    +import org.prebid.server.deals.events.AdminEventService;
    +import org.prebid.server.deals.events.ApplicationEventProcessor;
    +import org.prebid.server.deals.events.ApplicationEventService;
    +import org.prebid.server.deals.events.EventServiceInitializer;
    +import org.prebid.server.deals.simulation.DealsSimulationAdminHandler;
    +import org.prebid.server.deals.simulation.SimulationAwareDeliveryProgressService;
    +import org.prebid.server.deals.simulation.SimulationAwareDeliveryStatsService;
    +import org.prebid.server.deals.simulation.SimulationAwareHttpBidderRequester;
    +import org.prebid.server.deals.simulation.SimulationAwareLineItemService;
    +import org.prebid.server.deals.simulation.SimulationAwarePlannerService;
    +import org.prebid.server.deals.simulation.SimulationAwareRegisterService;
    +import org.prebid.server.deals.simulation.SimulationAwareUserService;
    +import org.prebid.server.geolocation.GeoLocationService;
    +import org.prebid.server.health.HealthMonitor;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.log.CriteriaLogManager;
    +import org.prebid.server.log.CriteriaManager;
    +import org.prebid.server.metric.Metrics;
    +import org.prebid.server.settings.CachingApplicationSettings;
    +import org.prebid.server.settings.SettingsCache;
    +import org.prebid.server.util.ObjectUtils;
    +import org.prebid.server.vertx.ContextRunner;
    +import org.prebid.server.vertx.http.HttpClient;
    +import org.springframework.beans.BeansException;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.beans.factory.config.BeanPostProcessor;
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.scheduling.annotation.EnableScheduling;
    +import org.springframework.scheduling.annotation.Scheduled;
    +import org.springframework.validation.annotation.Validated;
    +
    +import javax.validation.constraints.NotBlank;
    +import javax.validation.constraints.NotNull;
    +import java.time.Clock;
    +import java.time.ZonedDateTime;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.stream.Collectors;
    +
    +@Configuration
    +@ConditionalOnProperty(prefix = "deals", name = "enabled", havingValue = "true")
    +public class DealsConfiguration {
    +
    +    @Configuration
    +    @ConditionalOnExpression("${deals.enabled} == true and ${deals.simulation.enabled} == false")
    +    public static class ProductionConfiguration {
    +
    +        @Bean
    +        PlannerService plannerService(
    +                PlannerProperties plannerProperties,
    +                DeploymentProperties deploymentProperties,
    +                DeliveryProgressService deliveryProgressService,
    +                LineItemService lineItemService,
    +                AlertHttpService alertHttpService,
    +                HttpClient httpClient,
    +                Metrics metrics,
    +                Clock clock,
    +                JacksonMapper mapper) {
    +
    +            return new PlannerService(
    +                    plannerProperties.toComponentProperties(),
    +                    deploymentProperties.toComponentProperties(),
    +                    lineItemService,
    +                    deliveryProgressService,
    +                    alertHttpService,
    +                    httpClient,
    +                    metrics,
    +                    clock,
    +                    mapper);
    +        }
    +
    +        @Bean
    +        RegisterService registerService(
    +                PlannerProperties plannerProperties,
    +                DeploymentProperties deploymentProperties,
    +                AdminEventService adminEventService,
    +                DeliveryProgressService deliveryProgressService,
    +                AlertHttpService alertHttpService,
    +                HealthMonitor healthMonitor,
    +                CurrencyConversionService currencyConversionService,
    +                HttpClient httpClient,
    +                Vertx vertx,
    +                JacksonMapper mapper) {
    +
    +            return new RegisterService(
    +                    plannerProperties.toComponentProperties(),
    +                    deploymentProperties.toComponentProperties(),
    +                    adminEventService,
    +                    deliveryProgressService,
    +                    alertHttpService,
    +                    healthMonitor,
    +                    currencyConversionService,
    +                    httpClient,
    +                    vertx,
    +                    mapper);
    +        }
    +
    +        @Bean
    +        DeliveryStatsService deliveryStatsService(
    +                DeliveryStatsProperties deliveryStatsProperties,
    +                DeliveryProgressReportFactory deliveryProgressReportFactory,
    +                AlertHttpService alertHttpService,
    +                HttpClient httpClient,
    +                Metrics metrics,
    +                Clock clock,
    +                Vertx vertx,
    +                JacksonMapper mapper) {
    +
    +            return new DeliveryStatsService(
    +                    deliveryStatsProperties.toComponentProperties(),
    +                    deliveryProgressReportFactory,
    +                    alertHttpService,
    +                    httpClient,
    +                    metrics,
    +                    clock,
    +                    vertx,
    +                    mapper);
    +        }
    +
    +        @Bean
    +        LineItemService lineItemService(
    +                @Value("${deals.max-deals-per-bidder}") int maxDealsPerBidder,
    +                TargetingService targetingService,
    +                BidderCatalog bidderCatalog,
    +                CurrencyConversionService conversionService,
    +                ApplicationEventService applicationEventService,
    +                @Value("${auction.ad-server-currency}") String adServerCurrency,
    +                Clock clock,
    +                CriteriaLogManager criteriaLogManager) {
    +
    +            return new LineItemService(maxDealsPerBidder,
    +                    targetingService,
    +                    bidderCatalog,
    +                    conversionService,
    +                    applicationEventService,
    +                    adServerCurrency,
    +                    clock,
    +                    criteriaLogManager);
    +        }
    +
    +        @Bean
    +        DeliveryProgressService deliveryProgressService(
    +                DeliveryProgressProperties deliveryProgressProperties,
    +                LineItemService lineItemService,
    +                DeliveryStatsService deliveryStatsService,
    +                DeliveryProgressReportFactory deliveryProgressReportFactory,
    +                Clock clock,
    +                CriteriaLogManager criteriaLogManager) {
    +
    +            return new DeliveryProgressService(
    +                    deliveryProgressProperties.toComponentProperties(),
    +                    lineItemService,
    +                    deliveryStatsService,
    +                    deliveryProgressReportFactory,
    +                    clock,
    +                    criteriaLogManager);
    +        }
    +
    +        @Bean
    +        UserService userService(
    +                UserDetailsProperties userDetailsProperties,
    +                @Value("${datacenter-region}") String dataCenterRegion,
    +                LineItemService lineItemService,
    +                HttpClient httpClient,
    +                Clock clock,
    +                Metrics metrics,
    +                JacksonMapper mapper) {
    +
    +            return new UserService(
    +                    userDetailsProperties.toComponentProperties(),
    +                    dataCenterRegion,
    +                    lineItemService,
    +                    httpClient,
    +                    clock,
    +                    metrics,
    +                    mapper);
    +        }
    +    }
    +
    +    @Configuration
    +    @ConditionalOnExpression("${deals.enabled} == true and ${deals.simulation.enabled} == false")
    +    @EnableScheduling
    +    public static class SchedulerConfiguration {
    +
    +        @Bean
    +        GeneralPlannerScheduler generalPlannerScheduler(PlannerService plannerService,
    +                                                        ContextRunner contextRunner) {
    +            return new GeneralPlannerScheduler(plannerService, contextRunner);
    +        }
    +
    +        @Bean
    +        @ConditionalOnExpression(
    +                "'${deals.delivery-stats.delivery-period}'"
    +                        + ".equals('${deals.delivery-progress.report-reset-period}')")
    +        ImmediateDeliveryScheduler immediateDeliveryScheduler(DeliveryProgressService deliveryProgressService,
    +                                                              DeliveryStatsService deliveryStatsService,
    +                                                              Clock clock,
    +                                                              ContextRunner contextRunner) {
    +            return new ImmediateDeliveryScheduler(deliveryProgressService, deliveryStatsService, clock,
    +                    contextRunner);
    +        }
    +
    +        @Bean
    +        @ConditionalOnExpression(
    +                "not '${deals.delivery-stats.delivery-period}'"
    +                        + ".equals('${deals.delivery-progress.report-reset-period}')")
    +        DeliveryScheduler deliveryScheduler(DeliveryProgressService deliveryProgressService,
    +                                            DeliveryStatsService deliveryStatsService,
    +                                            Clock clock,
    +                                            ContextRunner contextRunner) {
    +            return new DeliveryScheduler(deliveryProgressService, deliveryStatsService, clock,
    +                    contextRunner);
    +        }
    +
    +        @Bean
    +        AdvancePlansScheduler advancePlansScheduler(LineItemService lineItemService,
    +                                                    ContextRunner contextRunner,
    +                                                    Clock clock) {
    +            return new AdvancePlansScheduler(lineItemService, contextRunner, clock);
    +        }
    +
    +        private static class GeneralPlannerScheduler {
    +
    +            private final PlannerService plannerService;
    +            private final ContextRunner contextRunner;
    +
    +            GeneralPlannerScheduler(PlannerService plannerService, ContextRunner contextRunner) {
    +                this.plannerService = plannerService;
    +                this.contextRunner = contextRunner;
    +            }
    +
    +            @Scheduled(cron = "${deals.planner.update-period}")
    +            public void fetchPlansFromGeneralPlanner() {
    +                contextRunner.runOnServiceContext(future -> {
    +                    plannerService.updateLineItemMetaData();
    +                    future.complete();
    +                });
    +            }
    +        }
    +
    +        private static class AdvancePlansScheduler {
    +            private final LineItemService lineItemService;
    +            private final ContextRunner contextRunner;
    +            private final Clock clock;
    +
    +            AdvancePlansScheduler(LineItemService lineItemService, ContextRunner contextRunner, Clock clock) {
    +                this.lineItemService = lineItemService;
    +                this.contextRunner = contextRunner;
    +                this.clock = clock;
    +            }
    +
    +            @Scheduled(cron = "${deals.planner.plan-advance-period}")
    +            public void advancePlans() {
    +                contextRunner.runOnServiceContext(future -> {
    +                    lineItemService.advanceToNextPlan(ZonedDateTime.now(clock));
    +                    future.complete();
    +                });
    +            }
    +        }
    +
    +        private static class ImmediateDeliveryScheduler {
    +
    +            private final DeliveryProgressService deliveryProgressService;
    +            private final DeliveryStatsService deliveryStatsService;
    +            private final Clock clock;
    +            private final ContextRunner contextRunner;
    +
    +            ImmediateDeliveryScheduler(DeliveryProgressService deliveryProgressService,
    +                                       DeliveryStatsService deliveryStatsService,
    +                                       Clock clock,
    +                                       ContextRunner contextRunner) {
    +                this.deliveryProgressService = deliveryProgressService;
    +                this.deliveryStatsService = deliveryStatsService;
    +                this.clock = clock;
    +                this.contextRunner = contextRunner;
    +            }
    +
    +            @Scheduled(cron = "${deals.delivery-stats.delivery-period}")
    +            public void createAndSendDeliveryReport() {
    +                contextRunner.runOnServiceContext(future -> {
    +                    final ZonedDateTime now = ZonedDateTime.now(clock);
    +                    deliveryProgressService.createDeliveryProgressReports(now);
    +                    deliveryStatsService.sendDeliveryProgressReports(now);
    +                    future.complete();
    +                });
    +            }
    +        }
    +    }
    +
    +    private static class DeliveryScheduler {
    +
    +        private final DeliveryProgressService deliveryProgressService;
    +        private final DeliveryStatsService deliveryStatsService;
    +        private final Clock clock;
    +        private final ContextRunner contextRunner;
    +
    +        DeliveryScheduler(DeliveryProgressService deliveryProgressService,
    +                          DeliveryStatsService deliveryStatsService,
    +                          Clock clock,
    +                          ContextRunner contextRunner) {
    +            this.deliveryProgressService = deliveryProgressService;
    +            this.deliveryStatsService = deliveryStatsService;
    +            this.clock = clock;
    +            this.contextRunner = contextRunner;
    +        }
    +
    +        @Scheduled(cron = "${deals.delivery-progress.report-reset-period}")
    +        public void createDeliveryReport() {
    +            contextRunner.runOnServiceContext(future -> {
    +                deliveryProgressService.createDeliveryProgressReports(ZonedDateTime.now(clock));
    +                future.complete();
    +            });
    +        }
    +
    +        @Scheduled(cron = "${deals.delivery-stats.delivery-period}")
    +        public void sendDeliveryReport() {
    +            contextRunner.runOnServiceContext(future -> {
    +                deliveryStatsService.sendDeliveryProgressReports(ZonedDateTime.now(clock));
    +                future.complete();
    +            });
    +        }
    +    }
    +
    +    @Configuration
    +    @ConditionalOnExpression("${deals.enabled} == true and ${deals.simulation.enabled} == true")
    +    public static class SimulationConfiguration {
    +
    +        @Bean
    +        @ConditionalOnProperty(prefix = "deals", name = "call-real-bidders-in-simulation", havingValue = "false",
    +                matchIfMissing = true)
    +        SimulationAwareHttpBidderRequester simulationAwareHttpBidderRequester(
    +                HttpClient httpClient,
    +                BidderRequestCompletionTrackerFactory completionTrackerFactory,
    +                BidderErrorNotifier bidderErrorNotifier,
    +                HttpBidderRequestEnricher requestEnricher,
    +                LineItemService lineItemService,
    +                JacksonMapper mapper) {
    +
    +            return new SimulationAwareHttpBidderRequester(
    +                    httpClient, completionTrackerFactory, bidderErrorNotifier, requestEnricher, lineItemService,
    +                    mapper);
    +        }
    +
    +        @Bean
    +        SimulationAwarePlannerService plannerService(
    +                PlannerProperties plannerProperties,
    +                DeploymentProperties deploymentProperties,
    +                DeliveryProgressService deliveryProgressService,
    +                SimulationAwareLineItemService lineItemService,
    +                AlertHttpService alertHttpService,
    +                HttpClient httpClient,
    +                Metrics metrics,
    +                Clock clock,
    +                JacksonMapper mapper) {
    +
    +            return new SimulationAwarePlannerService(
    +                    plannerProperties.toComponentProperties(),
    +                    deploymentProperties.toComponentProperties(),
    +                    lineItemService,
    +                    deliveryProgressService,
    +                    alertHttpService,
    +                    httpClient,
    +                    metrics,
    +                    clock,
    +                    mapper);
    +        }
    +
    +        @Bean
    +        SimulationAwareRegisterService registerService(
    +                PlannerProperties plannerProperties,
    +                DeploymentProperties deploymentProperties,
    +                AdminEventService adminEventService,
    +                DeliveryProgressService deliveryProgressService,
    +                AlertHttpService alertHttpService,
    +                HealthMonitor healthMonitor,
    +                CurrencyConversionService currencyConversionService,
    +                HttpClient httpClient,
    +                Vertx vertx,
    +                JacksonMapper mapper) {
    +
    +            return new SimulationAwareRegisterService(
    +                    plannerProperties.toComponentProperties(),
    +                    deploymentProperties.toComponentProperties(),
    +                    adminEventService,
    +                    deliveryProgressService,
    +                    alertHttpService,
    +                    healthMonitor,
    +                    currencyConversionService,
    +                    httpClient,
    +                    vertx,
    +                    mapper);
    +        }
    +
    +        @Bean
    +        SimulationAwareDeliveryStatsService deliveryStatsService(
    +                DeliveryStatsProperties deliveryStatsProperties,
    +                DeliveryProgressReportFactory deliveryProgressReportFactory,
    +                AlertHttpService alertHttpService,
    +                HttpClient httpClient,
    +                Metrics metrics,
    +                Clock clock,
    +                Vertx vertx,
    +                JacksonMapper mapper) {
    +
    +            return new SimulationAwareDeliveryStatsService(
    +                    deliveryStatsProperties.toComponentProperties(),
    +                    deliveryProgressReportFactory,
    +                    alertHttpService,
    +                    httpClient,
    +                    metrics,
    +                    clock,
    +                    vertx,
    +                    mapper);
    +        }
    +
    +        @Bean
    +        SimulationAwareLineItemService lineItemService(
    +                @Value("${deals.max-deals-per-bidder}") int maxDealsPerBidder,
    +                TargetingService targetingService,
    +                BidderCatalog bidderCatalog,
    +                CurrencyConversionService conversionService,
    +                ApplicationEventService applicationEventService,
    +                @Value("${auction.ad-server-currency}") String adServerCurrency,
    +                Clock clock,
    +                CriteriaLogManager criteriaLogManager) {
    +
    +            return new SimulationAwareLineItemService(
    +                    maxDealsPerBidder,
    +                    targetingService,
    +                    bidderCatalog,
    +                    conversionService,
    +                    applicationEventService,
    +                    adServerCurrency,
    +                    clock,
    +                    criteriaLogManager);
    +        }
    +
    +        @Bean
    +        SimulationAwareDeliveryProgressService deliveryProgressService(
    +                DeliveryProgressProperties deliveryProgressProperties,
    +                LineItemService lineItemService,
    +                DeliveryStatsService deliveryStatsService,
    +                DeliveryProgressReportFactory deliveryProgressReportFactory,
    +                @Value("${deals.simulation.ready-at-adjustment-ms}") long readyAtAdjustment,
    +                Clock clock,
    +                CriteriaLogManager criteriaLogManager) {
    +
    +            return new SimulationAwareDeliveryProgressService(
    +                    deliveryProgressProperties.toComponentProperties(),
    +                    lineItemService,
    +                    deliveryStatsService,
    +                    deliveryProgressReportFactory,
    +                    readyAtAdjustment,
    +                    clock,
    +                    criteriaLogManager);
    +        }
    +
    +        @Bean
    +        SimulationAwareUserService userService(
    +                UserDetailsProperties userDetailsProperties,
    +                SimulationProperties simulationProperties,
    +                @Value("${datacenter-region}") String dataCenterRegion,
    +                LineItemService lineItemService,
    +                HttpClient httpClient,
    +                Clock clock,
    +                Metrics metrics,
    +                JacksonMapper mapper) {
    +
    +            return new SimulationAwareUserService(
    +                    userDetailsProperties.toComponentProperties(),
    +                    simulationProperties.toComponentProperties(),
    +                    dataCenterRegion,
    +                    lineItemService,
    +                    httpClient,
    +                    clock,
    +                    metrics,
    +                    mapper);
    +        }
    +
    +        @Bean
    +        DealsSimulationAdminHandler dealsSimulationAdminHandler(
    +                SimulationAwareRegisterService registerService,
    +                SimulationAwarePlannerService plannerService,
    +                SimulationAwareDeliveryProgressService deliveryProgressService,
    +                SimulationAwareDeliveryStatsService deliveryStatsService,
    +                @Autowired(required = false) SimulationAwareHttpBidderRequester httpBidderRequester,
    +                JacksonMapper mapper,
    +                @Value("${admin-endpoints.e2eadmin.path}") String path) {
    +
    +            return new DealsSimulationAdminHandler(
    +                    registerService,
    +                    plannerService,
    +                    deliveryProgressService,
    +                    deliveryStatsService,
    +                    httpBidderRequester,
    +                    mapper,
    +                    path);
    +        }
    +
    +        @Bean
    +        BeanPostProcessor simulationCustomizationBeanPostProcessor(
    +                @Autowired(required = false) SimulationAwareHttpBidderRequester httpBidderRequester) {
    +
    +            return new BeanPostProcessor() {
    +                @Override
    +                public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    +                    // there are HttpBidderRequester and SimulationAwareHttpBidderRequester in context by now, we would
    +                    // like to replace former with latter everywhere
    +                    if (httpBidderRequester != null && bean.getClass().isAssignableFrom(HttpBidderRequester.class)
    +                            && !(bean instanceof SimulationAwareHttpBidderRequester)) {
    +                        return httpBidderRequester;
    +                    }
    +
    +                    return bean;
    +                }
    +            };
    +        }
    +    }
    +
    +    @Bean
    +    @ConfigurationProperties
    +    DeploymentProperties deploymentProperties() {
    +        return new DeploymentProperties();
    +    }
    +
    +    @Bean
    +    @ConfigurationProperties(prefix = "deals.planner")
    +    PlannerProperties plannerProperties() {
    +        return new PlannerProperties();
    +    }
    +
    +    @Bean
    +    @ConfigurationProperties(prefix = "deals.delivery-stats")
    +    DeliveryStatsProperties deliveryStatsProperties() {
    +        return new DeliveryStatsProperties();
    +    }
    +
    +    @Bean
    +    @ConfigurationProperties(prefix = "deals.delivery-progress")
    +    DeliveryProgressProperties deliveryProgressProperties() {
    +        return new DeliveryProgressProperties();
    +    }
    +
    +    @Bean
    +    @ConfigurationProperties(prefix = "deals.user-data")
    +    UserDetailsProperties userDetailsProperties() {
    +        return new UserDetailsProperties();
    +    }
    +
    +    @Bean
    +    @ConfigurationProperties(prefix = "deals.alert-proxy")
    +    AlertProxyProperties alertProxyProperties() {
    +        return new AlertProxyProperties();
    +    }
    +
    +    @Bean
    +    @ConfigurationProperties(prefix = "deals.simulation")
    +    SimulationProperties simulationProperties() {
    +        return new SimulationProperties();
    +    }
    +
    +    @Bean
    +    BidderRequestCompletionTrackerFactory bidderRequestCompletionTrackerFactory() {
    +        return new DealsBidderRequestCompletionTrackerFactory();
    +    }
    +
    +    @Bean
    +    DealsProcessor dealsProcessor(
    +            LineItemService lineItemService,
    +            @Autowired(required = false) DeviceInfoService deviceInfoService,
    +            @Autowired(required = false) GeoLocationService geoLocationService,
    +            UserService userService,
    +            Clock clock,
    +            JacksonMapper mapper,
    +            CriteriaLogManager criteriaLogManager) {
    +
    +        return new DealsProcessor(
    +                lineItemService, deviceInfoService, geoLocationService, userService, clock, mapper, criteriaLogManager);
    +    }
    +
    +    @Bean
    +    DeliveryProgressReportFactory deliveryProgressReportFactory(
    +            DeploymentProperties deploymentProperties,
    +            @Value("${deals.delivery-progress-report.competitors-number}") int competitorsNumber,
    +            LineItemService lineItemService) {
    +
    +        return new DeliveryProgressReportFactory(
    +                deploymentProperties.toComponentProperties(), competitorsNumber, lineItemService);
    +    }
    +
    +    @Bean
    +    AlertHttpService alertHttpService(JacksonMapper mapper,
    +                                      HttpClient httpClient,
    +                                      Clock clock,
    +                                      DeploymentProperties deploymentProperties,
    +                                      AlertProxyProperties alertProxyProperties) {
    +        return new AlertHttpService(mapper, httpClient, clock, deploymentProperties.toComponentProperties(),
    +                alertProxyProperties.toComponentProperties());
    +    }
    +
    +    @Bean
    +    TargetingService targetingService(JacksonMapper mapper) {
    +        return new TargetingService(mapper);
    +    }
    +
    +    @Bean
    +    AdminCentralService adminCentralService(
    +            CriteriaManager criteriaManager,
    +            LineItemService lineItemService,
    +            DeliveryProgressService deliveryProgressService,
    +            @Autowired(required = false) @Qualifier("settingsCache") SettingsCache settingsCache,
    +            @Autowired(required = false) @Qualifier("ampSettingsCache") SettingsCache ampSettingsCache,
    +            @Autowired(required = false) CachingApplicationSettings cachingApplicationSettings,
    +            JacksonMapper mapper,
    +            List suspendables) {
    +        return new AdminCentralService(criteriaManager, lineItemService, deliveryProgressService,
    +                settingsCache, ampSettingsCache, cachingApplicationSettings, mapper, suspendables);
    +    }
    +
    +    @Bean
    +    ApplicationEventService applicationEventService(EventBus eventBus) {
    +        return new ApplicationEventService(eventBus);
    +    }
    +
    +    @Bean
    +    AdminEventService adminEventService(EventBus eventBus) {
    +        return new AdminEventService(eventBus);
    +    }
    +
    +    @Bean
    +    EventServiceInitializer eventServiceInitializer(List applicationEventProcessors,
    +                                                    List adminEventProcessors,
    +                                                    EventBus eventBus) {
    +        return new EventServiceInitializer(applicationEventProcessors, adminEventProcessors, eventBus);
    +    }
    +
    +    @Validated
    +    @Data
    +    @NoArgsConstructor
    +    private static class DeploymentProperties {
    +
    +        @NotBlank
    +        private String hostId;
    +
    +        @NotBlank
    +        private String datacenterRegion;
    +
    +        @NotBlank
    +        private String vendor;
    +
    +        @NotBlank
    +        private String profile;
    +
    +        @NotBlank
    +        private String infra;
    +
    +        @NotBlank
    +        private String dataCenter;
    +
    +        @NotBlank
    +        private String system;
    +
    +        @NotBlank
    +        private String subSystem;
    +
    +        public org.prebid.server.deals.model.DeploymentProperties toComponentProperties() {
    +            return org.prebid.server.deals.model.DeploymentProperties.builder()
    +                    .pbsHostId(getHostId()).pbsRegion(getDatacenterRegion()).pbsVendor(getVendor())
    +                    .profile(getProfile()).infra(getInfra()).dataCenter(getDataCenter()).system(getSystem())
    +                    .subSystem(getSubSystem()).build();
    +        }
    +    }
    +
    +    @Validated
    +    @Data
    +    @NoArgsConstructor
    +    private static class PlannerProperties {
    +
    +        @NotBlank
    +        private String planEndpoint;
    +        @NotBlank
    +        private String registerEndpoint;
    +        @NotNull
    +        private Long timeoutMs;
    +        @NotNull
    +        private Long registerPeriodSec;
    +        @NotBlank
    +        private String username;
    +        @NotBlank
    +        private String password;
    +
    +        public org.prebid.server.deals.model.PlannerProperties toComponentProperties() {
    +            return org.prebid.server.deals.model.PlannerProperties.builder()
    +                    .planEndpoint(getPlanEndpoint())
    +                    .registerEndpoint(getRegisterEndpoint())
    +                    .timeoutMs(getTimeoutMs())
    +                    .registerPeriodSeconds(getRegisterPeriodSec())
    +                    .username(getUsername())
    +                    .password(getPassword())
    +                    .build();
    +        }
    +    }
    +
    +    @Validated
    +    @Data
    +    @NoArgsConstructor
    +    private static class DeliveryStatsProperties {
    +
    +        @NotBlank
    +        private String endpoint;
    +        @NotNull
    +        private Integer cachedReportsNumber;
    +        @NotNull
    +        private Long timeoutMs;
    +        @NotNull
    +        private Integer lineItemsPerReport;
    +        @NotNull
    +        private Integer reportsIntervalMs;
    +        @NotNull
    +        private Integer batchesIntervalMs;
    +        @NotNull
    +        private Boolean requestCompressionEnabled;
    +        @NotBlank
    +        private String username;
    +        @NotBlank
    +        private String password;
    +
    +        public org.prebid.server.deals.model.DeliveryStatsProperties toComponentProperties() {
    +            return org.prebid.server.deals.model.DeliveryStatsProperties.builder()
    +                    .endpoint(getEndpoint())
    +                    .cachedReportsNumber(getCachedReportsNumber())
    +                    .timeoutMs(getTimeoutMs())
    +                    .lineItemsPerReport(getLineItemsPerReport())
    +                    .reportsIntervalMs(getReportsIntervalMs())
    +                    .batchesIntervalMs(getBatchesIntervalMs())
    +                    .requestCompressionEnabled(getRequestCompressionEnabled())
    +                    .username(getUsername())
    +                    .password(getPassword())
    +                    .build();
    +        }
    +    }
    +
    +    @Validated
    +    @Data
    +    @NoArgsConstructor
    +    private static class DeliveryProgressProperties {
    +
    +        @NotNull
    +        private Long lineItemStatusTtlSec;
    +        @NotNull
    +        private Integer cachedPlansNumber;
    +
    +        public org.prebid.server.deals.model.DeliveryProgressProperties toComponentProperties() {
    +            return org.prebid.server.deals.model.DeliveryProgressProperties.of(getLineItemStatusTtlSec(),
    +                    getCachedPlansNumber());
    +        }
    +    }
    +
    +    @Validated
    +    @Data
    +    @NoArgsConstructor
    +    private static class UserDetailsProperties {
    +
    +        @NotBlank
    +        private String userDetailsEndpoint;
    +        @NotBlank
    +        private String winEventEndpoint;
    +        @NotNull
    +        private Long timeout;
    +        @NotNull
    +        private List userIds;
    +
    +        public org.prebid.server.deals.model.UserDetailsProperties toComponentProperties() {
    +            final List componentUserIds = getUserIds().stream()
    +                    .map(DealsConfiguration.UserIdRule::toComponentProperties)
    +                    .collect(Collectors.toList());
    +
    +            return org.prebid.server.deals.model.UserDetailsProperties.of(
    +                    getUserDetailsEndpoint(), getWinEventEndpoint(), getTimeout(), componentUserIds);
    +        }
    +    }
    +
    +    @Validated
    +    @Data
    +    @NoArgsConstructor
    +    private static class AlertProxyProperties {
    +
    +        @NotNull
    +        private boolean enabled;
    +
    +        @NotBlank
    +        private String url;
    +
    +        @NotNull
    +        private Integer timeoutSec;
    +
    +        Map alertTypes;
    +
    +        @NotBlank
    +        private String username;
    +
    +        @NotBlank
    +        private String password;
    +
    +        public org.prebid.server.deals.model.AlertProxyProperties toComponentProperties() {
    +            return org.prebid.server.deals.model.AlertProxyProperties.builder()
    +                    .enabled(isEnabled()).url(getUrl()).timeoutSec(getTimeoutSec())
    +                    .alertTypes(ObjectUtils.firstNonNull(this::getAlertTypes, HashMap::new))
    +                    .username(getUsername())
    +                    .password(getPassword()).build();
    +        }
    +    }
    +
    +    @Validated
    +    @NoArgsConstructor
    +    @Data
    +    private static class UserIdRule {
    +
    +        @NotBlank
    +        private String type;
    +
    +        @NotBlank
    +        private String source;
    +
    +        @NotBlank
    +        private String location;
    +
    +        org.prebid.server.deals.model.UserIdRule toComponentProperties() {
    +            return org.prebid.server.deals.model.UserIdRule.of(getType(), getSource(), getLocation());
    +        }
    +    }
    +
    +    @Validated
    +    @NoArgsConstructor
    +    @Data
    +    private static class SimulationProperties {
    +
    +        @NotNull
    +        boolean enabled;
    +
    +        Boolean winEventsEnabled;
    +
    +        Boolean userDetailsEnabled;
    +
    +        org.prebid.server.deals.model.SimulationProperties toComponentProperties() {
    +            return org.prebid.server.deals.model.SimulationProperties.builder()
    +                    .enabled(isEnabled())
    +                    .winEventsEnabled(getWinEventsEnabled() != null ? getWinEventsEnabled() : false)
    +                    .userDetailsEnabled(getUserDetailsEnabled() != null ? getUserDetailsEnabled() : false)
    +                    .build();
    +        }
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java
    new file mode 100644
    index 00000000000..bffc5ee32f0
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java
    @@ -0,0 +1,60 @@
    +package org.prebid.server.spring.config;
    +
    +import io.vertx.core.Vertx;
    +import lombok.Data;
    +import lombok.NoArgsConstructor;
    +import org.prebid.server.execution.TimeoutFactory;
    +import org.prebid.server.hooks.execution.HookCatalog;
    +import org.prebid.server.hooks.execution.HookStageExecutor;
    +import org.prebid.server.hooks.v1.Module;
    +import org.prebid.server.json.JacksonMapper;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.validation.annotation.Validated;
    +
    +import java.time.Clock;
    +import java.util.Collection;
    +
    +@Configuration
    +public class HooksConfiguration {
    +
    +    @Bean
    +    HookCatalog hookCatalog(Collection modules) {
    +        return new HookCatalog(modules);
    +    }
    +
    +    @Bean
    +    HookStageExecutor hookStageExecutor(HooksConfigurationProperties hooksConfiguration,
    +                                        HookCatalog hookCatalog,
    +                                        TimeoutFactory timeoutFactory,
    +                                        Vertx vertx,
    +                                        Clock clock,
    +                                        JacksonMapper mapper) {
    +
    +        return HookStageExecutor.create(
    +                hooksConfiguration.getHostExecutionPlan(),
    +                hooksConfiguration.getDefaultAccountExecutionPlan(),
    +                hookCatalog,
    +                timeoutFactory,
    +                vertx,
    +                clock,
    +                mapper);
    +    }
    +
    +    @Bean
    +    @ConfigurationProperties("hooks")
    +    HooksConfigurationProperties hooksConfigurationProperties() {
    +        return new HooksConfigurationProperties();
    +    }
    +
    +    @Validated
    +    @Data
    +    @NoArgsConstructor
    +    private static class HooksConfigurationProperties {
    +
    +        String hostExecutionPlan;
    +
    +        String defaultAccountExecutionPlan;
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/JacksonConfiguration.java b/src/main/java/org/prebid/server/spring/config/JacksonConfiguration.java
    deleted file mode 100644
    index 52239591ca5..00000000000
    --- a/src/main/java/org/prebid/server/spring/config/JacksonConfiguration.java
    +++ /dev/null
    @@ -1,15 +0,0 @@
    -package org.prebid.server.spring.config;
    -
    -import org.prebid.server.json.JacksonMapper;
    -import org.prebid.server.json.ObjectMapperProvider;
    -import org.springframework.context.annotation.Bean;
    -import org.springframework.context.annotation.Configuration;
    -
    -@Configuration
    -public class JacksonConfiguration {
    -
    -    @Bean
    -    JacksonMapper jacksonMapper() {
    -        return new JacksonMapper(ObjectMapperProvider.mapper());
    -    }
    -}
    diff --git a/src/main/java/org/prebid/server/spring/config/JsonConfiguration.java b/src/main/java/org/prebid/server/spring/config/JsonConfiguration.java
    new file mode 100644
    index 00000000000..eda4e2fb32d
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/JsonConfiguration.java
    @@ -0,0 +1,21 @@
    +package org.prebid.server.spring.config;
    +
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.json.JsonMerger;
    +import org.prebid.server.json.ObjectMapperProvider;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +
    +@Configuration
    +public class JsonConfiguration {
    +
    +    @Bean
    +    JacksonMapper jacksonMapper() {
    +        return new JacksonMapper(ObjectMapperProvider.mapper());
    +    }
    +
    +    @Bean
    +    JsonMerger jsonMerger(JacksonMapper mapper) {
    +        return new JsonMerger(mapper);
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/MetricsConfiguration.java b/src/main/java/org/prebid/server/spring/config/MetricsConfiguration.java
    index b6ad9702fe4..4027c169cbe 100644
    --- a/src/main/java/org/prebid/server/spring/config/MetricsConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/MetricsConfiguration.java
    @@ -6,6 +6,8 @@
     import com.codahale.metrics.SharedMetricRegistries;
     import com.codahale.metrics.graphite.Graphite;
     import com.codahale.metrics.graphite.GraphiteReporter;
    +import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
    +import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
     import com.izettle.metrics.influxdb.InfluxDbHttpSender;
     import com.izettle.metrics.influxdb.InfluxDbReporter;
     import com.izettle.metrics.influxdb.InfluxDbSender;
    @@ -20,7 +22,6 @@
     import lombok.Data;
     import lombok.NoArgsConstructor;
     import org.apache.commons.lang3.ObjectUtils;
    -import org.prebid.server.bidder.BidderCatalog;
     import org.prebid.server.metric.AccountMetricsVerbosity;
     import org.prebid.server.metric.CounterType;
     import org.prebid.server.metric.Metrics;
    @@ -107,13 +108,21 @@ ScheduledReporter consoleReporter(ConsoleProperties consoleProperties, MetricReg
     
         @Bean
         Metrics metrics(@Value("${metrics.metricType}") CounterType counterType, MetricRegistry metricRegistry,
    -                    AccountMetricsVerbosity accountMetricsVerbosity, BidderCatalog bidderCatalog) {
    -        return new Metrics(metricRegistry, counterType, accountMetricsVerbosity, bidderCatalog);
    +                    AccountMetricsVerbosity accountMetricsVerbosity) {
    +        return new Metrics(metricRegistry, counterType, accountMetricsVerbosity);
         }
     
         @Bean
         MetricRegistry metricRegistry() {
    -        return SharedMetricRegistries.getOrCreate(METRIC_REGISTRY_NAME);
    +        final boolean alreadyExists = SharedMetricRegistries.names().contains(METRIC_REGISTRY_NAME);
    +        final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(METRIC_REGISTRY_NAME);
    +
    +        if (!alreadyExists) {
    +            metricRegistry.register("jvm.gc", new GarbageCollectorMetricSet());
    +            metricRegistry.register("jvm.memory", new MemoryUsageGaugeSet());
    +        }
    +
    +        return metricRegistry;
         }
     
         @Bean
    diff --git a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java
    index bdafd23afc8..f38787a5f0c 100644
    --- a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java
    @@ -7,7 +7,6 @@
     import org.prebid.server.geolocation.GeoLocationService;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.metric.Metrics;
    -import org.prebid.server.privacy.gdpr.GdprService;
     import org.prebid.server.privacy.gdpr.Tcf2Service;
     import org.prebid.server.privacy.gdpr.TcfDefinerService;
     import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.PurposeEightStrategy;
    @@ -27,7 +26,6 @@
     import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.PurposeTwoBasicEnforcePurposeStrategy;
     import org.prebid.server.privacy.gdpr.tcfstrategies.specialfeature.SpecialFeaturesOneStrategy;
     import org.prebid.server.privacy.gdpr.tcfstrategies.specialfeature.SpecialFeaturesStrategy;
    -import org.prebid.server.privacy.gdpr.vendorlist.VendorListServiceV1;
     import org.prebid.server.privacy.gdpr.vendorlist.VendorListServiceV2;
     import org.prebid.server.settings.model.GdprConfig;
     import org.prebid.server.settings.model.Purpose;
    @@ -49,36 +47,6 @@
     @Configuration
     public class PrivacyServiceConfiguration {
     
    -    @Bean
    -    VendorListServiceV1 vendorListServiceV1(
    -            @Value("${gdpr.vendorlist.v1.cache-dir}") String cacheDir,
    -            @Value("${gdpr.vendorlist.v1.http-endpoint-template}") String endpointTemplate,
    -            @Value("${gdpr.vendorlist.default-timeout-ms}") int defaultTimeoutMs,
    -            @Value("${gdpr.vendorlist.v1.refresh-missing-list-period-ms}") int refreshMissingListPeriodMs,
    -            @Value("${gdpr.host-vendor-id:#{null}}") Integer hostVendorId,
    -            @Value("${gdpr.vendorlist.v1.fallback-vendor-list-path:#{null}}") String fallbackVendorListPath,
    -            BidderCatalog bidderCatalog,
    -            Vertx vertx,
    -            FileSystem fileSystem,
    -            HttpClient httpClient,
    -            Metrics metrics,
    -            JacksonMapper mapper) {
    -
    -        return new VendorListServiceV1(
    -                cacheDir,
    -                endpointTemplate,
    -                defaultTimeoutMs,
    -                refreshMissingListPeriodMs,
    -                hostVendorId,
    -                fallbackVendorListPath,
    -                bidderCatalog,
    -                vertx,
    -                fileSystem,
    -                httpClient,
    -                metrics,
    -                mapper);
    -    }
    -
         @Bean
         VendorListServiceV2 vendorListServiceV2(
                 @Value("${gdpr.vendorlist.v2.cache-dir}") String cacheDir,
    @@ -87,6 +55,7 @@ VendorListServiceV2 vendorListServiceV2(
                 @Value("${gdpr.vendorlist.v2.refresh-missing-list-period-ms}") int refreshMissingListPeriodMs,
                 @Value("${gdpr.host-vendor-id:#{null}}") Integer hostVendorId,
                 @Value("${gdpr.vendorlist.v2.fallback-vendor-list-path:#{null}}") String fallbackVendorListPath,
    +            @Value("${gdpr.vendorlist.v2.deprecated}") boolean deprecated,
                 BidderCatalog bidderCatalog,
                 Vertx vertx,
                 FileSystem fileSystem,
    @@ -99,6 +68,7 @@ VendorListServiceV2 vendorListServiceV2(
                     endpointTemplate,
                     defaultTimeoutMs,
                     refreshMissingListPeriodMs,
    +                deprecated,
                     hostVendorId,
                     fallbackVendorListPath,
                     bidderCatalog,
    @@ -109,11 +79,6 @@ VendorListServiceV2 vendorListServiceV2(
                     mapper);
         }
     
    -    @Bean
    -    GdprService gdprService(VendorListServiceV1 vendorListServiceV1) {
    -        return new GdprService(vendorListServiceV1);
    -    }
    -
         @Bean
         Tcf2Service tcf2Service(GdprConfig gdprConfig,
                                 List purposeStrategies,
    @@ -129,7 +94,6 @@ Tcf2Service tcf2Service(GdprConfig gdprConfig,
         TcfDefinerService tcfDefinerService(
                 GdprConfig gdprConfig,
                 @Value("${gdpr.eea-countries}") String eeaCountriesAsString,
    -            GdprService gdprService,
                 Tcf2Service tcf2Service,
                 @Autowired(required = false) GeoLocationService geoLocationService,
                 BidderCatalog bidderCatalog,
    @@ -141,7 +105,6 @@ TcfDefinerService tcfDefinerService(
             return new TcfDefinerService(
                     gdprConfig,
                     eeaCountries,
    -                gdprService,
                     tcf2Service,
                     geoLocationService,
                     bidderCatalog,
    diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java
    index 25e3a2626e5..5a5e17e2f62 100644
    --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java
    @@ -1,14 +1,12 @@
     package org.prebid.server.spring.config;
     
    -import com.iab.openrtb.request.BidRequest;
     import de.malkusch.whoisServerList.publicSuffixList.PublicSuffixList;
     import de.malkusch.whoisServerList.publicSuffixList.PublicSuffixListFactory;
     import io.vertx.core.Vertx;
    +import io.vertx.core.file.FileSystem;
     import io.vertx.core.http.HttpClientOptions;
     import io.vertx.core.net.JksOptions;
    -import org.prebid.server.auction.AmpRequestFactory;
     import org.prebid.server.auction.AmpResponsePostProcessor;
    -import org.prebid.server.auction.AuctionRequestFactory;
     import org.prebid.server.auction.BidResponseCreator;
     import org.prebid.server.auction.BidResponsePostProcessor;
     import org.prebid.server.auction.ExchangeService;
    @@ -17,30 +15,41 @@
     import org.prebid.server.auction.InterstitialProcessor;
     import org.prebid.server.auction.IpAddressHelper;
     import org.prebid.server.auction.OrtbTypesResolver;
    -import org.prebid.server.auction.PreBidRequestContextFactory;
     import org.prebid.server.auction.PrivacyEnforcementService;
    +import org.prebid.server.auction.SchainResolver;
     import org.prebid.server.auction.StoredRequestProcessor;
     import org.prebid.server.auction.StoredResponseProcessor;
     import org.prebid.server.auction.TimeoutResolver;
    -import org.prebid.server.auction.VideoRequestFactory;
     import org.prebid.server.auction.VideoResponseFactory;
     import org.prebid.server.auction.VideoStoredRequestProcessor;
    +import org.prebid.server.auction.WinningBidComparatorFactory;
    +import org.prebid.server.auction.requestfactory.AmpRequestFactory;
    +import org.prebid.server.auction.requestfactory.AuctionRequestFactory;
    +import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver;
    +import org.prebid.server.auction.requestfactory.Ortb2RequestFactory;
    +import org.prebid.server.auction.requestfactory.VideoRequestFactory;
     import org.prebid.server.bidder.BidderCatalog;
     import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.BidderErrorNotifier;
     import org.prebid.server.bidder.BidderRequestCompletionTrackerFactory;
    -import org.prebid.server.bidder.HttpAdapterConnector;
    +import org.prebid.server.bidder.HttpBidderRequestEnricher;
     import org.prebid.server.bidder.HttpBidderRequester;
     import org.prebid.server.cache.CacheService;
     import org.prebid.server.cache.model.CacheTtl;
     import org.prebid.server.cookie.UidsCookieService;
     import org.prebid.server.currency.CurrencyConversionService;
    +import org.prebid.server.deals.DealsProcessor;
    +import org.prebid.server.deals.events.ApplicationEventService;
     import org.prebid.server.events.EventsService;
     import org.prebid.server.execution.TimeoutFactory;
    +import org.prebid.server.hooks.execution.HookStageExecutor;
     import org.prebid.server.identity.IdGenerator;
    -import org.prebid.server.identity.IdGeneratorType;
     import org.prebid.server.identity.NoneIdGenerator;
     import org.prebid.server.identity.UUIDIdGenerator;
     import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.json.JsonMerger;
    +import org.prebid.server.log.CriteriaLogManager;
    +import org.prebid.server.log.CriteriaManager;
     import org.prebid.server.log.HttpInteractionLogger;
     import org.prebid.server.log.LoggerControlKnob;
     import org.prebid.server.metric.Metrics;
    @@ -48,13 +57,16 @@
     import org.prebid.server.privacy.PrivacyExtractor;
     import org.prebid.server.privacy.gdpr.TcfDefinerService;
     import org.prebid.server.settings.ApplicationSettings;
    +import org.prebid.server.settings.model.BidValidationEnforcement;
     import org.prebid.server.spring.config.model.CircuitBreakerProperties;
     import org.prebid.server.spring.config.model.ExternalConversionProperties;
     import org.prebid.server.spring.config.model.HttpClientProperties;
    +import org.prebid.server.util.VersionInfo;
     import org.prebid.server.validation.BidderParamValidator;
     import org.prebid.server.validation.RequestValidator;
     import org.prebid.server.validation.ResponseBidValidator;
     import org.prebid.server.validation.VideoRequestValidator;
    +import org.prebid.server.vast.VastModifier;
     import org.prebid.server.vertx.http.BasicHttpClient;
     import org.prebid.server.vertx.http.CircuitBreakerSecuredHttpClient;
     import org.prebid.server.vertx.http.HttpClient;
    @@ -89,6 +101,7 @@ CacheService cacheService(
                 @Value("${cache.query}") String query,
                 @Value("${cache.banner-ttl-seconds:#{null}}") Integer bannerCacheTtl,
                 @Value("${cache.video-ttl-seconds:#{null}}") Integer videoCacheTtl,
    +            VastModifier vastModifier,
                 EventsService eventsService,
                 HttpClient httpClient,
                 Metrics metrics,
    @@ -100,12 +113,18 @@ CacheService cacheService(
                     httpClient,
                     CacheService.getCacheEndpointUrl(scheme, host, path),
                     CacheService.getCachedAssetUrlTemplate(scheme, host, path, query),
    +                vastModifier,
                     eventsService,
                     metrics,
                     clock,
                     mapper);
         }
     
    +    @Bean
    +    VastModifier vastModifier(BidderCatalog bidderCatalog, EventsService eventsService, Metrics metrics) {
    +        return new VastModifier(bidderCatalog, eventsService, metrics);
    +    }
    +
         @Bean
         ImplicitParametersExtractor implicitParametersExtractor(PublicSuffixList psl) {
             return new ImplicitParametersExtractor(psl);
    @@ -122,13 +141,21 @@ IpAddressHelper ipAddressHelper(@Value("${ipv6.always-mask-right}") int ipv6Alwa
         }
     
         @Bean
    -    FpdResolver fpdResolver(JacksonMapper mapper) {
    -        return new FpdResolver(mapper);
    +    FpdResolver fpdResolver(JacksonMapper mapper, JsonMerger jsonMerger) {
    +        return new FpdResolver(mapper, jsonMerger);
         }
     
         @Bean
    -    OrtbTypesResolver ortbTypesResolver(JacksonMapper jacksonMapper) {
    -        return new OrtbTypesResolver(jacksonMapper);
    +    OrtbTypesResolver ortbTypesResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) {
    +        return new OrtbTypesResolver(jacksonMapper, jsonMerger);
    +    }
    +
    +    @Bean
    +    SchainResolver schainResolver(
    +            @Value("${auction.host-schain-node}") String globalSchainNode,
    +            JacksonMapper mapper) {
    +
    +        return SchainResolver.create(globalSchainNode, mapper);
         }
     
         @Bean
    @@ -159,95 +186,117 @@ TimeoutResolver ampTimeoutResolver(
         }
     
         @Bean
    -    PreBidRequestContextFactory preBidRequestContextFactory(
    -            TimeoutResolver timeoutResolver,
    +    Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver(
    +            @Value("${auction.cache.only-winning-bids}") boolean shouldCacheOnlyWinningBids,
    +            @Value("${auction.ad-server-currency}") String adServerCurrency,
    +            @Value("${auction.blacklisted-apps}") String blacklistedAppsString,
                 ImplicitParametersExtractor implicitParametersExtractor,
                 IpAddressHelper ipAddressHelper,
    -            ApplicationSettings applicationSettings,
    -            UidsCookieService uidsCookieService,
    -            TimeoutFactory timeoutFactory,
    +            IdGenerator sourceIdGenerator,
                 JacksonMapper mapper) {
     
    -        return new PreBidRequestContextFactory(
    -                timeoutResolver,
    +        final List blacklistedApps = splitToList(blacklistedAppsString);
    +
    +        return new Ortb2ImplicitParametersResolver(
    +                shouldCacheOnlyWinningBids,
    +                adServerCurrency,
    +                blacklistedApps,
                     implicitParametersExtractor,
                     ipAddressHelper,
    -                applicationSettings,
    -                uidsCookieService,
    -                timeoutFactory,
    +                sourceIdGenerator,
                     mapper);
         }
     
         @Bean
    -    AuctionRequestFactory auctionRequestFactory(
    -            @Value("${auction.max-request-size}") @Min(0) int maxRequestSize,
    +    Ortb2RequestFactory openRtb2RequestFactory(
                 @Value("${settings.enforce-valid-account}") boolean enforceValidAccount,
    -            @Value("${auction.cache.only-winning-bids}") boolean shouldCacheOnlyWinningBids,
    -            @Value("${auction.ad-server-currency}") String adServerCurrency,
    -            @Value("${auction.blacklisted-apps}") String blacklistedAppsString,
                 @Value("${auction.blacklisted-accounts}") String blacklistedAccountsString,
    -            StoredRequestProcessor storedRequestProcessor,
    -            ImplicitParametersExtractor implicitParametersExtractor,
    -            IpAddressHelper ipAddressHelper,
                 UidsCookieService uidsCookieService,
    -            BidderCatalog bidderCatalog,
                 RequestValidator requestValidator,
    -            OrtbTypesResolver ortbTypesResolver,
                 TimeoutResolver timeoutResolver,
                 TimeoutFactory timeoutFactory,
    +            StoredRequestProcessor storedRequestProcessor,
                 ApplicationSettings applicationSettings,
    -            PrivacyEnforcementService privacyEnforcementService,
    -            IdGenerator idGenerator,
    -            JacksonMapper mapper) {
    +            IpAddressHelper ipAddressHelper,
    +            HookStageExecutor hookStageExecutor,
    +            @Autowired(required = false) DealsProcessor dealsProcessor,
    +            Clock clock) {
     
    -        final List blacklistedApps = splitCommaSeparatedString(blacklistedAppsString);
    -        final List blacklistedAccounts = splitCommaSeparatedString(blacklistedAccountsString);
    +        final List blacklistedAccounts = splitToList(blacklistedAccountsString);
     
    -        return new AuctionRequestFactory(
    -                maxRequestSize,
    +        return new Ortb2RequestFactory(
                     enforceValidAccount,
    -                shouldCacheOnlyWinningBids,
    -                adServerCurrency,
    -                blacklistedApps,
                     blacklistedAccounts,
    -                storedRequestProcessor,
    -                implicitParametersExtractor,
    -                ipAddressHelper,
                     uidsCookieService,
    -                bidderCatalog,
                     requestValidator,
    -                new InterstitialProcessor(),
    -                ortbTypesResolver,
                     timeoutResolver,
                     timeoutFactory,
    +                storedRequestProcessor,
                     applicationSettings,
    -                idGenerator,
    +                ipAddressHelper,
    +                hookStageExecutor,
    +                dealsProcessor,
    +                clock);
    +    }
    +
    +    @Bean
    +    AuctionRequestFactory auctionRequestFactory(
    +            @Value("${auction.max-request-size}") @Min(0) int maxRequestSize,
    +            Ortb2RequestFactory ortb2RequestFactory,
    +            StoredRequestProcessor storedRequestProcessor,
    +            ImplicitParametersExtractor implicitParametersExtractor,
    +            Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver,
    +            OrtbTypesResolver ortbTypesResolver,
    +            PrivacyEnforcementService privacyEnforcementService,
    +            TimeoutResolver timeoutResolver,
    +            JacksonMapper mapper) {
    +
    +        return new AuctionRequestFactory(
    +                maxRequestSize,
    +                ortb2RequestFactory,
    +                storedRequestProcessor,
    +                implicitParametersExtractor,
    +                ortb2ImplicitParametersResolver,
    +                new InterstitialProcessor(),
    +                ortbTypesResolver,
                     privacyEnforcementService,
    +                timeoutResolver,
                     mapper);
         }
     
         @Bean
    -    IdGenerator idGenerator(@Value("${auction.id-generator-type}") IdGeneratorType idGeneratorType) {
    -        return idGeneratorType == IdGeneratorType.uuid
    +    IdGenerator bidIdGenerator(@Value("${auction.generate-bid-id}") boolean generateBidId) {
    +        return generateBidId
    +                ? new UUIDIdGenerator()
    +                : new NoneIdGenerator();
    +    }
    +
    +    @Bean
    +    IdGenerator sourceIdGenerator(@Value("${auction.generate-source-tid}") boolean generateSourceTid) {
    +        return generateSourceTid
                     ? new UUIDIdGenerator()
                     : new NoneIdGenerator();
         }
     
         @Bean
         AmpRequestFactory ampRequestFactory(StoredRequestProcessor storedRequestProcessor,
    -                                        AuctionRequestFactory auctionRequestFactory,
    +                                        Ortb2RequestFactory ortb2RequestFactory,
                                             OrtbTypesResolver ortbTypesResolver,
                                             ImplicitParametersExtractor implicitParametersExtractor,
    +                                        Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver,
                                             FpdResolver fpdResolver,
    +                                        PrivacyEnforcementService privacyEnforcementService,
                                             TimeoutResolver timeoutResolver,
                                             JacksonMapper mapper) {
     
             return new AmpRequestFactory(
                     storedRequestProcessor,
    -                auctionRequestFactory,
    +                ortb2RequestFactory,
                     ortbTypesResolver,
                     implicitParametersExtractor,
    +                ortb2ImplicitParametersResolver,
                     fpdResolver,
    +                privacyEnforcementService,
                     timeoutResolver,
                     mapper);
         }
    @@ -255,13 +304,23 @@ AmpRequestFactory ampRequestFactory(StoredRequestProcessor storedRequestProcesso
         @Bean
         VideoRequestFactory videoRequestFactory(
                 @Value("${auction.max-request-size}") int maxRequestSize,
    -            @Value("${auction.video.stored-required:#{false}}") boolean enforceStoredRequest,
    +            @Value("${video.stored-request-required}") boolean enforceStoredRequest,
                 VideoStoredRequestProcessor storedRequestProcessor,
    -            AuctionRequestFactory auctionRequestFactory,
    -            TimeoutResolver timeoutResolver, JacksonMapper mapper) {
    +            Ortb2RequestFactory ortb2RequestFactory,
    +            Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver,
    +            PrivacyEnforcementService privacyEnforcementService,
    +            TimeoutResolver timeoutResolver,
    +            JacksonMapper mapper) {
     
    -        return new VideoRequestFactory(maxRequestSize, enforceStoredRequest, storedRequestProcessor,
    -                auctionRequestFactory, timeoutResolver, mapper);
    +        return new VideoRequestFactory(
    +                maxRequestSize,
    +                enforceStoredRequest,
    +                ortb2RequestFactory,
    +                ortb2ImplicitParametersResolver,
    +                storedRequestProcessor,
    +                privacyEnforcementService,
    +                timeoutResolver,
    +                mapper);
         }
     
         @Bean
    @@ -271,27 +330,39 @@ VideoResponseFactory videoResponseFactory(JacksonMapper mapper) {
     
         @Bean
         VideoStoredRequestProcessor videoStoredRequestProcessor(
    -            ApplicationSettings applicationSettings,
    -            @Value("${auction.video.stored-required:#{false}}") boolean enforceStoredRequest,
    +            @Value("${video.stored-request-required}") boolean enforceStoredRequest,
                 @Value("${auction.blacklisted-accounts}") String blacklistedAccountsString,
    -            BidRequest defaultVideoBidRequest,
    +            @Value("${video.stored-requests-timeout-ms}") long defaultTimeoutMs,
    +            @Value("${auction.ad-server-currency:#{null}}") String adServerCurrency,
    +            @Value("${default-request.file.path:#{null}}") String defaultBidRequestPath,
    +            FileSystem fileSystem,
    +            ApplicationSettings applicationSettings,
    +            VideoRequestValidator videoRequestValidator,
                 Metrics metrics,
                 TimeoutFactory timeoutFactory,
                 TimeoutResolver timeoutResolver,
    -            @Value("${video.stored-requests-timeout-ms}") long defaultTimeoutMs,
    -            @Value("${auction.ad-server-currency:#{null}}") String adServerCurrency,
    -            JacksonMapper mapper) {
    -
    -        final List blacklistedAccounts = splitCommaSeparatedString(blacklistedAccountsString);
    +            JacksonMapper mapper,
    +            JsonMerger jsonMerger) {
     
    -        return new VideoStoredRequestProcessor(applicationSettings, new VideoRequestValidator(), enforceStoredRequest,
    -                blacklistedAccounts, defaultVideoBidRequest, metrics, timeoutFactory, timeoutResolver, defaultTimeoutMs,
    -                adServerCurrency, mapper);
    +        return VideoStoredRequestProcessor.create(
    +                enforceStoredRequest,
    +                splitToList(blacklistedAccountsString),
    +                defaultTimeoutMs,
    +                adServerCurrency,
    +                defaultBidRequestPath,
    +                fileSystem,
    +                applicationSettings,
    +                videoRequestValidator,
    +                metrics,
    +                timeoutFactory,
    +                timeoutResolver,
    +                mapper,
    +                jsonMerger);
         }
     
         @Bean
    -    BidRequest defaultVideoBidRequest() {
    -        return BidRequest.builder().build();
    +    VideoRequestValidator videoRequestValidator() {
    +        return new VideoRequestValidator();
         }
     
         @Bean
    @@ -402,18 +473,50 @@ BidderCatalog bidderCatalog(List bidderDeps) {
         @Bean
         HttpBidderRequester httpBidderRequester(
                 HttpClient httpClient,
    -            @Autowired(required = false) BidderRequestCompletionTrackerFactory bidderRequestCompletionTrackerFactory) {
    +            @Autowired(required = false) BidderRequestCompletionTrackerFactory bidderRequestCompletionTrackerFactory,
    +            BidderErrorNotifier bidderErrorNotifier,
    +            HttpBidderRequestEnricher requestEnricher) {
    +
    +        return new HttpBidderRequester(httpClient,
    +                bidderRequestCompletionTrackerFactory,
    +                bidderErrorNotifier,
    +                requestEnricher);
    +    }
    +
    +    @Bean
    +    HttpBidderRequestEnricher httpBidderRequestEnricher(VersionInfo versionInfo) {
     
    -        return new HttpBidderRequester(httpClient, bidderRequestCompletionTrackerFactory);
    +        return new HttpBidderRequestEnricher(versionInfo.getVersion());
    +    }
    +
    +    @Bean
    +    BidderErrorNotifier bidderErrorNotifier(
    +            @Value("${auction.timeout-notification.timeout-ms}") int timeoutNotificationTimeoutMs,
    +            @Value("${auction.timeout-notification.log-result}") boolean logTimeoutNotificationResult,
    +            @Value("${auction.timeout-notification.log-failure-only}") boolean logTimeoutNotificationFailureOnly,
    +            @Value("${auction.timeout-notification.log-sampling-rate}") double logTimeoutNotificationSamplingRate,
    +            HttpClient httpClient,
    +            Metrics metrics) {
    +
    +        return new BidderErrorNotifier(
    +                timeoutNotificationTimeoutMs,
    +                logTimeoutNotificationResult,
    +                logTimeoutNotificationFailureOnly,
    +                logTimeoutNotificationSamplingRate,
    +                httpClient,
    +                metrics);
         }
     
         @Bean
         BidResponseCreator bidResponseCreator(
                 CacheService cacheService,
                 BidderCatalog bidderCatalog,
    +            VastModifier vastModifier,
                 EventsService eventsService,
                 StoredRequestProcessor storedRequestProcessor,
    -            @Value("${auction.generate-bid-id}") boolean generateBidId,
    +            WinningBidComparatorFactory winningBidComparatorFactory,
    +            IdGenerator bidIdGenerator,
    +            HookStageExecutor hookStageExecutor,
                 @Value("${settings.targeting.truncate-attr-chars}") int truncateAttrChars,
                 Clock clock,
                 JacksonMapper mapper) {
    @@ -421,9 +524,12 @@ BidResponseCreator bidResponseCreator(
             return new BidResponseCreator(
                     cacheService,
                     bidderCatalog,
    +                vastModifier,
                     eventsService,
                     storedRequestProcessor,
    -                generateBidId,
    +                winningBidComparatorFactory,
    +                bidIdGenerator,
    +                hookStageExecutor,
                     truncateAttrChars,
                     clock,
                     mapper);
    @@ -436,15 +542,19 @@ ExchangeService exchangeService(
                 StoredResponseProcessor storedResponseProcessor,
                 PrivacyEnforcementService privacyEnforcementService,
                 FpdResolver fpdResolver,
    +            SchainResolver schainResolver,
                 HttpBidderRequester httpBidderRequester,
                 ResponseBidValidator responseBidValidator,
                 CurrencyConversionService currencyConversionService,
                 BidResponseCreator bidResponseCreator,
                 BidResponsePostProcessor bidResponsePostProcessor,
    +            HookStageExecutor hookStageExecutor,
    +            @Autowired(required = false) ApplicationEventService applicationEventService,
                 HttpInteractionLogger httpInteractionLogger,
                 Metrics metrics,
                 Clock clock,
    -            JacksonMapper mapper) {
    +            JacksonMapper mapper,
    +            CriteriaLogManager criteriaLogManager) {
     
             return new ExchangeService(
                     expectedCacheTimeMs,
    @@ -452,34 +562,56 @@ ExchangeService exchangeService(
                     storedResponseProcessor,
                     privacyEnforcementService,
                     fpdResolver,
    +                schainResolver,
                     httpBidderRequester,
                     responseBidValidator,
                     currencyConversionService,
                     bidResponseCreator,
                     bidResponsePostProcessor,
    +                hookStageExecutor,
    +                applicationEventService,
                     httpInteractionLogger,
                     metrics,
                     clock,
    -                mapper);
    +                mapper,
    +                criteriaLogManager);
         }
     
         @Bean
         StoredRequestProcessor storedRequestProcessor(
                 @Value("${auction.stored-requests-timeout-ms}") long defaultTimeoutMs,
    +            @Value("${default-request.file.path:#{null}}") String defaultBidRequestPath,
    +            @Value("${settings.generate-storedrequest-bidrequest-id}") boolean generateBidRequestId,
    +            FileSystem fileSystem,
                 ApplicationSettings applicationSettings,
                 Metrics metrics,
                 TimeoutFactory timeoutFactory,
    -            JacksonMapper mapper) {
    +            JacksonMapper mapper,
    +            JsonMerger jsonMerger) {
    +
    +        return StoredRequestProcessor.create(
    +                defaultTimeoutMs,
    +                defaultBidRequestPath,
    +                generateBidRequestId,
    +                fileSystem,
    +                applicationSettings,
    +                new UUIDIdGenerator(),
    +                metrics,
    +                timeoutFactory,
    +                mapper,
    +                jsonMerger);
    +    }
     
    -        return new StoredRequestProcessor(defaultTimeoutMs, applicationSettings, metrics, timeoutFactory, mapper);
    +    @Bean
    +    WinningBidComparatorFactory winningBidComparatorFactory() {
    +        return new WinningBidComparatorFactory();
         }
     
         @Bean
         StoredResponseProcessor storedResponseProcessor(ApplicationSettings applicationSettings,
    -                                                    BidderCatalog bidderCatalog,
                                                         JacksonMapper mapper) {
     
    -        return new StoredResponseProcessor(applicationSettings, bidderCatalog, mapper);
    +        return new StoredResponseProcessor(applicationSettings, mapper);
         }
     
         @Bean
    @@ -487,12 +619,21 @@ PrivacyEnforcementService privacyEnforcementService(
                 BidderCatalog bidderCatalog,
                 PrivacyExtractor privacyExtractor,
                 TcfDefinerService tcfDefinerService,
    +            ImplicitParametersExtractor implicitParametersExtractor,
                 IpAddressHelper ipAddressHelper,
                 Metrics metrics,
    -            @Value("${ccpa.enforce}") boolean ccpaEnforce) {
    +            @Value("${ccpa.enforce}") boolean ccpaEnforce,
    +            @Value("${lmt.enforce}") boolean lmtEnforce) {
     
             return new PrivacyEnforcementService(
    -                bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, ccpaEnforce);
    +                bidderCatalog,
    +                privacyExtractor,
    +                tcfDefinerService,
    +                implicitParametersExtractor,
    +                ipAddressHelper,
    +                metrics,
    +                ccpaEnforce,
    +                lmtEnforce);
         }
     
         @Bean
    @@ -501,12 +642,8 @@ PrivacyExtractor privacyExtractor() {
         }
     
         @Bean
    -    HttpAdapterConnector httpAdapterConnector(HttpClient httpClient,
    -                                              PrivacyExtractor privacyExtractor,
    -                                              Clock clock,
    -                                              JacksonMapper mapper) {
    -
    -        return new HttpAdapterConnector(httpClient, privacyExtractor, clock, mapper);
    +    VersionInfo versionInfo(JacksonMapper jacksonMapper) {
    +        return VersionInfo.create("git-revision.json", jacksonMapper);
         }
     
         @Bean
    @@ -523,8 +660,25 @@ BidderParamValidator bidderParamValidator(BidderCatalog bidderCatalog, JacksonMa
         }
     
         @Bean
    -    ResponseBidValidator responseValidator() {
    -        return new ResponseBidValidator();
    +    ResponseBidValidator responseValidator(
    +            @Value("${auction.validations.banner-creative-max-size}") BidValidationEnforcement bannerMaxSizeEnforcement,
    +            @Value("${auction.validations.secure-markup}") BidValidationEnforcement secureMarkupEnforcement,
    +            Metrics metrics,
    +            JacksonMapper mapper,
    +            @Value("${deals.enabled}") boolean dealsEnabled) {
    +
    +        return new ResponseBidValidator(bannerMaxSizeEnforcement, secureMarkupEnforcement, metrics, mapper,
    +                dealsEnabled);
    +    }
    +
    +    @Bean
    +    CriteriaLogManager criteriaLogManager(JacksonMapper mapper) {
    +        return new CriteriaLogManager(mapper);
    +    }
    +
    +    @Bean
    +    CriteriaManager criteriaManager(CriteriaLogManager criteriaLogManager, Vertx vertx) {
    +        return new CriteriaManager(criteriaLogManager, vertx);
         }
     
         @Bean
    @@ -563,6 +717,7 @@ AmpResponsePostProcessor ampResponsePostProcessor() {
         @Bean
         CurrencyConversionService currencyConversionService(
                 @Autowired(required = false) ExternalConversionProperties externalConversionProperties) {
    +
             return new CurrencyConversionService(externalConversionProperties);
         }
     
    @@ -570,13 +725,26 @@ CurrencyConversionService currencyConversionService(
         @ConditionalOnProperty(prefix = "currency-converter.external-rates", name = "enabled", havingValue = "true")
         ExternalConversionProperties externalConversionProperties(
                 @Value("${currency-converter.external-rates.url}") String currencyServerUrl,
    -            @Value("${currency-converter.external-rates.default-timeout-ms}") long defaultTimeout,
    -            @Value("${currency-converter.external-rates.refresh-period-ms}") long refreshPeriod,
    +            @Value("${currency-converter.external-rates.default-timeout-ms}") long defaultTimeoutMs,
    +            @Value("${currency-converter.external-rates.refresh-period-ms}") long refreshPeriodMs,
    +            @Value("${currency-converter.external-rates.stale-after-ms}") long staleAfterMs,
    +            @Value("${currency-converter.external-rates.stale-period-ms:#{null}}") Long stalePeriodMs,
                 Vertx vertx,
                 HttpClient httpClient,
    +            Metrics metrics,
    +            Clock clock,
                 JacksonMapper mapper) {
     
    -        return new ExternalConversionProperties(currencyServerUrl, defaultTimeout, refreshPeriod, vertx, httpClient,
    +        return new ExternalConversionProperties(
    +                currencyServerUrl,
    +                defaultTimeoutMs,
    +                refreshPeriodMs,
    +                staleAfterMs,
    +                stalePeriodMs,
    +                vertx,
    +                httpClient,
    +                metrics,
    +                clock,
                     mapper);
         }
     
    @@ -590,9 +758,11 @@ LoggerControlKnob loggerControlKnob(Vertx vertx) {
             return new LoggerControlKnob(vertx);
         }
     
    -    private static List splitCommaSeparatedString(String listString) {
    -        return Stream.of(listString.split(","))
    +    private static List splitToList(String listAsString) {
    +        return listAsString != null
    +                ? Stream.of(listAsString.split(","))
                     .map(String::trim)
    -                .collect(Collectors.toList());
    +                .collect(Collectors.toList())
    +                : null;
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
    index d4c80580f46..8b2cc36dced 100644
    --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
    @@ -10,10 +10,13 @@
     import org.apache.commons.lang3.ObjectUtils;
     import org.prebid.server.execution.TimeoutFactory;
     import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.json.JsonMerger;
    +import org.prebid.server.metric.MetricName;
     import org.prebid.server.metric.Metrics;
     import org.prebid.server.settings.ApplicationSettings;
     import org.prebid.server.settings.CachingApplicationSettings;
     import org.prebid.server.settings.CompositeApplicationSettings;
    +import org.prebid.server.settings.EnrichingApplicationSettings;
     import org.prebid.server.settings.FileApplicationSettings;
     import org.prebid.server.settings.HttpApplicationSettings;
     import org.prebid.server.settings.JdbcApplicationSettings;
    @@ -72,14 +75,20 @@ static class DatabaseSettingsConfiguration {
     
             @Bean
             JdbcApplicationSettings jdbcApplicationSettings(
    +                @Value("${settings.database.account-query}") String accountQuery,
                     @Value("${settings.database.stored-requests-query}") String storedRequestsQuery,
                     @Value("${settings.database.amp-stored-requests-query}") String ampStoredRequestsQuery,
    -                @Value("${settings.database.stored-responses-query}") String storedResponseQuery,
    +                @Value("${settings.database.stored-responses-query}") String storedResponsesQuery,
                     JdbcClient jdbcClient,
                     JacksonMapper jacksonMapper) {
     
                 return new JdbcApplicationSettings(
    -                    jdbcClient, jacksonMapper, storedRequestsQuery, ampStoredRequestsQuery, storedResponseQuery);
    +                    jdbcClient,
    +                    jacksonMapper,
    +                    accountQuery,
    +                    storedRequestsQuery,
    +                    ampStoredRequestsQuery,
    +                    storedResponsesQuery);
             }
     
             @Bean
    @@ -229,7 +238,8 @@ public HttpPeriodicRefreshService ampHttpPeriodicRefreshService(
         }
     
         @Configuration
    -    @ConditionalOnProperty(prefix = "settings.in-memory-cache.jdbc-update",
    +    @ConditionalOnProperty(
    +            prefix = "settings.in-memory-cache.jdbc-update",
                 name = {"refresh-rate", "timeout", "init-query", "update-query", "amp-init-query", "amp-update-query"})
         static class JdbcPeriodicRefreshServiceConfiguration {
     
    @@ -248,24 +258,50 @@ static class JdbcPeriodicRefreshServiceConfiguration {
             @Autowired
             TimeoutFactory timeoutFactory;
     
    +        @Autowired
    +        Metrics metrics;
    +
    +        @Autowired
    +        Clock clock;
    +
             @Bean
             public JdbcPeriodicRefreshService jdbcPeriodicRefreshService(
    -                SettingsCache settingsCache,
    +                @Qualifier("settingsCache") SettingsCache settingsCache,
                     @Value("${settings.in-memory-cache.jdbc-update.init-query}") String initQuery,
                     @Value("${settings.in-memory-cache.jdbc-update.update-query}") String updateQuery) {
     
    -            return new JdbcPeriodicRefreshService(settingsCache, vertx, jdbcClient, refreshPeriod,
    -                    initQuery, updateQuery, timeoutFactory, timeout);
    +            return new JdbcPeriodicRefreshService(
    +                    initQuery,
    +                    updateQuery,
    +                    refreshPeriod,
    +                    timeout,
    +                    MetricName.stored_request,
    +                    settingsCache,
    +                    vertx,
    +                    jdbcClient,
    +                    timeoutFactory,
    +                    metrics,
    +                    clock);
             }
     
             @Bean
             public JdbcPeriodicRefreshService ampJdbcPeriodicRefreshService(
    -                SettingsCache settingsCache,
    +                @Qualifier("ampSettingsCache") SettingsCache ampSettingsCache,
                     @Value("${settings.in-memory-cache.jdbc-update.amp-init-query}") String ampInitQuery,
                     @Value("${settings.in-memory-cache.jdbc-update.amp-update-query}") String ampUpdateQuery) {
     
    -            return new JdbcPeriodicRefreshService(settingsCache, vertx, jdbcClient, refreshPeriod,
    -                    ampInitQuery, ampUpdateQuery, timeoutFactory, timeout);
    +            return new JdbcPeriodicRefreshService(
    +                    ampInitQuery,
    +                    ampUpdateQuery,
    +                    refreshPeriod,
    +                    timeout,
    +                    MetricName.amp_stored_request,
    +                    ampSettingsCache,
    +                    vertx,
    +                    jdbcClient,
    +                    timeoutFactory,
    +                    metrics,
    +                    clock);
             }
         }
     
    @@ -292,23 +328,38 @@ CompositeApplicationSettings compositeApplicationSettings(
             }
         }
     
    +    @Configuration
    +    static class EnrichingSettingsConfiguration {
    +
    +        @Bean
    +        EnrichingApplicationSettings enrichingApplicationSettings(
    +                @Value("${settings.default-account-config:#{null}}") String defaultAccountConfig,
    +                CompositeApplicationSettings compositeApplicationSettings,
    +                JsonMerger jsonMerger) {
    +
    +            return new EnrichingApplicationSettings(defaultAccountConfig, compositeApplicationSettings, jsonMerger);
    +        }
    +    }
    +
         @Configuration
         static class CachingSettingsConfiguration {
     
             @Bean
             @ConditionalOnProperty(prefix = "settings.in-memory-cache", name = {"ttl-seconds", "cache-size"})
             CachingApplicationSettings cachingApplicationSettings(
    -                CompositeApplicationSettings compositeApplicationSettings,
    +                EnrichingApplicationSettings enrichingApplicationSettings,
                     ApplicationSettingsCacheProperties cacheProperties,
                     @Qualifier("settingsCache") SettingsCache cache,
                     @Qualifier("ampSettingsCache") SettingsCache ampCache,
    -                @Qualifier("videoSettingCache") SettingsCache videoCache) {
    +                @Qualifier("videoSettingCache") SettingsCache videoCache,
    +                Metrics metrics) {
     
                 return new CachingApplicationSettings(
    -                    compositeApplicationSettings,
    +                    enrichingApplicationSettings,
                         cache,
                         ampCache,
                         videoCache,
    +                    metrics,
                         cacheProperties.getTtlSeconds(),
                         cacheProperties.getCacheSize());
             }
    @@ -320,8 +371,8 @@ static class ApplicationSettingsConfiguration {
             @Bean
             ApplicationSettings applicationSettings(
                     @Autowired(required = false) CachingApplicationSettings cachingApplicationSettings,
    -                @Autowired(required = false) CompositeApplicationSettings compositeApplicationSettings) {
    -            return ObjectUtils.defaultIfNull(cachingApplicationSettings, compositeApplicationSettings);
    +                EnrichingApplicationSettings enrichingApplicationSettings) {
    +            return ObjectUtils.defaultIfNull(cachingApplicationSettings, enrichingApplicationSettings);
             }
         }
     
    diff --git a/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java b/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java
    index b7942ac54e7..c4d515b1695 100644
    --- a/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java
    @@ -2,10 +2,14 @@
     
     import io.vertx.core.Vertx;
     import io.vertx.core.VertxOptions;
    +import io.vertx.core.eventbus.EventBus;
     import io.vertx.core.file.FileSystem;
     import io.vertx.ext.dropwizard.DropwizardMetricsOptions;
    +import io.vertx.ext.dropwizard.Match;
    +import io.vertx.ext.dropwizard.MatchType;
     import io.vertx.ext.web.handler.BodyHandler;
     import org.prebid.server.vertx.ContextRunner;
    +import org.prebid.server.vertx.LocalMessageCodec;
     import org.springframework.beans.factory.annotation.Value;
     import org.springframework.context.annotation.Bean;
     import org.springframework.context.annotation.Configuration;
    @@ -14,12 +18,28 @@
     public class VertxConfiguration {
     
         @Bean
    -    Vertx vertx(@Value("${vertx.worker-pool-size}") int workerPoolSize) {
    -        return Vertx.vertx(new VertxOptions()
    +    Vertx vertx(@Value("${vertx.worker-pool-size}") int workerPoolSize,
    +                @Value("${vertx.enable-per-client-endpoint-metrics}") boolean enablePerClientEndpointMetrics) {
    +        final DropwizardMetricsOptions metricsOptions = new DropwizardMetricsOptions()
    +                .setEnabled(true)
    +                .setRegistryName(MetricsConfiguration.METRIC_REGISTRY_NAME);
    +        if (enablePerClientEndpointMetrics) {
    +            metricsOptions.addMonitoredHttpClientEndpoint(new Match().setValue(".*").setType(MatchType.REGEX));
    +        }
    +
    +        final VertxOptions vertxOptions = new VertxOptions()
                     .setWorkerPoolSize(workerPoolSize)
    -                .setMetricsOptions(new DropwizardMetricsOptions()
    -                        .setEnabled(true)
    -                        .setRegistryName(MetricsConfiguration.METRIC_REGISTRY_NAME)));
    +                .setMetricsOptions(metricsOptions);
    +
    +        return Vertx.vertx(vertxOptions);
    +    }
    +
    +    @Bean
    +    EventBus eventBus(Vertx vertx) {
    +        final EventBus eventBus = vertx.eventBus();
    +        eventBus.registerCodec(LocalMessageCodec.create());
    +
    +        return eventBus;
         }
     
         @Bean
    diff --git a/src/main/java/org/prebid/server/spring/config/WebConfiguration.java b/src/main/java/org/prebid/server/spring/config/WebConfiguration.java
    index 203703465c5..1e6b11ea3a6 100644
    --- a/src/main/java/org/prebid/server/spring/config/WebConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/WebConfiguration.java
    @@ -13,21 +13,20 @@
     import io.vertx.ext.web.handler.StaticHandler;
     import lombok.Data;
     import lombok.NoArgsConstructor;
    -import org.prebid.server.analytics.CompositeAnalyticsReporter;
    -import org.prebid.server.auction.AmpRequestFactory;
    +import org.prebid.server.analytics.AnalyticsReporterDelegator;
     import org.prebid.server.auction.AmpResponsePostProcessor;
    -import org.prebid.server.auction.AuctionRequestFactory;
     import org.prebid.server.auction.ExchangeService;
    -import org.prebid.server.auction.PreBidRequestContextFactory;
     import org.prebid.server.auction.PrivacyEnforcementService;
    -import org.prebid.server.auction.VideoRequestFactory;
     import org.prebid.server.auction.VideoResponseFactory;
    +import org.prebid.server.auction.requestfactory.AmpRequestFactory;
    +import org.prebid.server.auction.requestfactory.AuctionRequestFactory;
    +import org.prebid.server.auction.requestfactory.VideoRequestFactory;
     import org.prebid.server.bidder.BidderCatalog;
    -import org.prebid.server.bidder.HttpAdapterConnector;
     import org.prebid.server.cache.CacheService;
     import org.prebid.server.cookie.UidsCookieService;
    +import org.prebid.server.deals.UserService;
    +import org.prebid.server.deals.events.ApplicationEventService;
     import org.prebid.server.execution.TimeoutFactory;
    -import org.prebid.server.handler.AuctionHandler;
     import org.prebid.server.handler.BidderParamHandler;
     import org.prebid.server.handler.CookieSyncHandler;
     import org.prebid.server.handler.CustomizedAdminEndpoint;
    @@ -121,6 +120,7 @@ HttpServerOptions httpServerOptions(@Value("${http.max-headers-size}") int maxHe
                     .setHandle100ContinueAutomatically(true)
                     .setMaxHeaderSize(maxHeaderSize)
                     .setCompressionSupported(true)
    +                .setDecompressionSupported(true)
                     .setIdleTimeout(10); // kick off long processing requests
     
             if (ssl) {
    @@ -145,7 +145,6 @@ ExceptionHandler exceptionHandler(Metrics metrics) {
         Router router(BodyHandler bodyHandler,
                       NoCacheHandler noCacheHandler,
                       CorsHandler corsHandler,
    -                  AuctionHandler auctionHandler,
                       org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler,
                       AmpHandler openrtbAmpHandler,
                       VideoHandler openrtbVideoHandler,
    @@ -166,7 +165,6 @@ Router router(BodyHandler bodyHandler,
             router.route().handler(bodyHandler);
             router.route().handler(noCacheHandler);
             router.route().handler(corsHandler);
    -        router.post("/auction").handler(auctionHandler);
             router.post("/openrtb2/auction").handler(openrtbAuctionHandler);
             router.get("/openrtb2/amp").handler(openrtbAmpHandler);
             router.post("/openrtb2/video").handler(openrtbVideoHandler);
    @@ -210,40 +208,11 @@ CorsHandler corsHandler() {
                             HttpMethod.OPTIONS)));
         }
     
    -    @Bean
    -    AuctionHandler auctionHandler(
    -            ApplicationSettings applicationSettings,
    -            BidderCatalog bidderCatalog,
    -            PreBidRequestContextFactory preBidRequestContextFactory,
    -            CacheService cacheService,
    -            Metrics metrics,
    -            HttpAdapterConnector httpAdapterConnector,
    -            Clock clock,
    -            TcfDefinerService tcfDefinerService,
    -            PrivacyEnforcementService privacyEnforcementService,
    -            JacksonMapper mapper,
    -            @Value("${gdpr.host-vendor-id:#{null}}") Integer hostVendorId) {
    -
    -        return new AuctionHandler(
    -                applicationSettings,
    -                bidderCatalog,
    -                preBidRequestContextFactory,
    -                cacheService,
    -                metrics,
    -                httpAdapterConnector,
    -                clock,
    -                tcfDefinerService,
    -                privacyEnforcementService,
    -                mapper,
    -                hostVendorId
    -        );
    -    }
    -
         @Bean
         org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler(
                 ExchangeService exchangeService,
                 AuctionRequestFactory auctionRequestFactory,
    -            CompositeAnalyticsReporter analyticsReporter,
    +            AnalyticsReporterDelegator analyticsReporter,
                 Metrics metrics,
                 Clock clock,
                 HttpInteractionLogger httpInteractionLogger,
    @@ -263,7 +232,7 @@ org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler(
         AmpHandler openrtbAmpHandler(
                 AmpRequestFactory ampRequestFactory,
                 ExchangeService exchangeService,
    -            CompositeAnalyticsReporter analyticsReporter,
    +            AnalyticsReporterDelegator analyticsReporter,
                 Metrics metrics,
                 Clock clock,
                 BidderCatalog bidderCatalog,
    @@ -290,7 +259,7 @@ VideoHandler openrtbVideoHandler(
                 VideoRequestFactory videoRequestFactory,
                 VideoResponseFactory videoResponseFactory,
                 ExchangeService exchangeService,
    -            CompositeAnalyticsReporter analyticsReporter,
    +            AnalyticsReporterDelegator analyticsReporter,
                 Metrics metrics,
                 Clock clock,
                 JacksonMapper mapper) {
    @@ -320,13 +289,14 @@ CookieSyncHandler cookieSyncHandler(
                 PrivacyEnforcementService privacyEnforcementService,
                 @Value("${gdpr.host-vendor-id:#{null}}") Integer hostVendorId,
                 @Value("${cookie-sync.coop-sync.default}") boolean defaultCoopSync,
    -            CompositeAnalyticsReporter analyticsReporter,
    +            AnalyticsReporterDelegator analyticsReporterDelegator,
                 Metrics metrics,
                 TimeoutFactory timeoutFactory,
                 JacksonMapper mapper) {
             return new CookieSyncHandler(externalUrl, defaultTimeoutMs, uidsCookieService, applicationSettings,
                     bidderCatalog, tcfDefinerService, privacyEnforcementService, hostVendorId,
    -                defaultCoopSync, coopSyncPriorities.getPri(), analyticsReporter, metrics, timeoutFactory, mapper);
    +                defaultCoopSync, coopSyncPriorities.getPri(), analyticsReporterDelegator, metrics, timeoutFactory,
    +                mapper);
         }
     
         @Bean
    @@ -338,7 +308,7 @@ SetuidHandler setuidHandler(
                 PrivacyEnforcementService privacyEnforcementService,
                 TcfDefinerService tcfDefinerService,
                 @Value("${gdpr.host-vendor-id:#{null}}") Integer hostVendorId,
    -            CompositeAnalyticsReporter analyticsReporter,
    +            AnalyticsReporterDelegator analyticsReporter,
                 Metrics metrics,
                 TimeoutFactory timeoutFactory) {
     
    @@ -363,7 +333,8 @@ GetuidsHandler getuidsHandler(UidsCookieService uidsCookieService, JacksonMapper
         @Bean
         VtrackHandler vtrackHandler(
                 @Value("${vtrack.default-timeout-ms}") int defaultTimeoutMs,
    -            @Value("${vtrack.allow-unkonwn-bidder}") boolean allowUnknownBidder,
    +            @Value("${vtrack.allow-unknown-bidder}") boolean allowUnknownBidder,
    +            @Value("${vtrack.modify-vast-for-unknown-bidder}") boolean modifyVastForUnknownBidder,
                 ApplicationSettings applicationSettings,
                 BidderCatalog bidderCatalog,
                 CacheService cacheService,
    @@ -373,6 +344,7 @@ VtrackHandler vtrackHandler(
             return new VtrackHandler(
                     defaultTimeoutMs,
                     allowUnknownBidder,
    +                modifyVastForUnknownBidder,
                     applicationSettings,
                     bidderCatalog,
                     cacheService,
    @@ -412,10 +384,23 @@ BidderDetailsHandler bidderDetailsHandler(BidderCatalog bidderCatalog, JacksonMa
         }
     
         @Bean
    -    NotificationEventHandler eventNotificationHandler(CompositeAnalyticsReporter compositeAnalyticsReporter,
    -                                                      TimeoutFactory timeoutFactory,
    -                                                      ApplicationSettings applicationSettings) {
    -        return new NotificationEventHandler(compositeAnalyticsReporter, timeoutFactory, applicationSettings);
    +    NotificationEventHandler notificationEventHandler(
    +            UidsCookieService uidsCookieService,
    +            @Autowired(required = false) ApplicationEventService applicationEventService,
    +            @Autowired(required = false) UserService userService,
    +            AnalyticsReporterDelegator analyticsReporterDelegator,
    +            TimeoutFactory timeoutFactory,
    +            ApplicationSettings applicationSettings,
    +            @Value("${deals.enabled}") boolean dealsEnabled) {
    +
    +        return new NotificationEventHandler(
    +                uidsCookieService,
    +                applicationEventService,
    +                userService,
    +                analyticsReporterDelegator,
    +                timeoutFactory,
    +                applicationSettings,
    +                dealsEnabled);
         }
     
         @Bean
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AcuityadsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AcuityadsConfiguration.java
    new file mode 100644
    index 00000000000..361aa204d08
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AcuityadsConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.acuityads.AcuityadsBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/acuityads.yaml", factory = YamlPropertySourceFactory.class)
    +public class AcuityadsConfiguration {
    +
    +    private static final String BIDDER_NAME = "acuityads";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("acuityadsConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("acuityadsConfigurationProperties")
    +    @ConfigurationProperties("adapters.acuityads")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps acuityadsBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AcuityadsBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdfConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdfConfiguration.java
    new file mode 100644
    index 00000000000..ec73724820b
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdfConfiguration.java
    @@ -0,0 +1,52 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.adf.AdfBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/adf.yaml", factory = YamlPropertySourceFactory.class)
    +public class AdfConfiguration {
    +
    +    private static final String BIDDER_NAME = "adf";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("adfConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("adfConfigurationProperties")
    +    @ConfigurationProperties("adapters.adf")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps adfBidderDeps() {
    +
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdfBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdformConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdformConfiguration.java
    index 46d5ae6f49d..9086202958c 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdformConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdformConfiguration.java
    @@ -1,13 +1,10 @@
     package org.prebid.server.spring.config.bidder;
     
     import org.prebid.server.bidder.BidderDeps;
    -import org.prebid.server.bidder.adform.AdformAdapter;
     import org.prebid.server.bidder.adform.AdformBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,15 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps adformBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new AdformBidder(configProperties.getEndpoint(), mapper))
    -                .adapterCreator(() -> new AdformAdapter(usersync.getCookieFamilyName(),
    -                        configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdformBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdgenerationConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdgenerationConfiguration.java
    index 969e5ff1632..fae95e0926d 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdgenerationConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdgenerationConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps adgenerationBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AdgenerationBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdgenerationBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdheseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdheseConfiguration.java
    index 929952e4599..9865075ff3b 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdheseConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdheseConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps adheseBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AdheseBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdheseBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdkernelAdnConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdkernelAdnConfiguration.java
    index cdf86d35c1c..262c6163b07 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdkernelAdnConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdkernelAdnConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps adkernelAdnBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AdkernelAdnBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdkernelAdnBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdkernelConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdkernelConfiguration.java
    index 1687767fa78..ca5c089a8a5 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdkernelConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdkernelConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps adkernelBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AdkernelBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdkernelBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdmanConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdmanConfiguration.java
    index 37f1fed97c2..fadaedb31bd 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdmanConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdmanConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.adman.AdmanBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,11 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps admanBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
     
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new AdmanBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdmanBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdmixerConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdmixerConfiguration.java
    index 3d8706b1dda..9404f361c41 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdmixerConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdmixerConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.admixer.AdmixerBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps admixerBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new AdmixerBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdmixerBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdoceanConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdoceanConfiguration.java
    index 1f9ecf51159..736203977f1 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdoceanConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdoceanConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps adoceanBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AdoceanBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdoceanBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdopplerConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdopplerConfiguration.java
    index 94a49031a04..511c4e37cc4 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdopplerConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdopplerConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps adopplerBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AdopplerBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdopplerBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdotConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdotConfiguration.java
    new file mode 100644
    index 00000000000..2d161533883
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdotConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.adot.AdotBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/adot.yaml", factory = YamlPropertySourceFactory.class)
    +public class AdotConfiguration {
    +
    +    private static final String BIDDER_NAME = "adot";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("adotConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("adotConfigurationProperties")
    +    @ConfigurationProperties("adapters.adot")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps adotBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdotBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdponeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdponeConfiguration.java
    index 4bf7664983d..134e656ac41 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdponeConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdponeConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps adponeBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AdponeBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdponeBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdprimeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdprimeConfiguration.java
    new file mode 100644
    index 00000000000..94caa6a2ed9
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdprimeConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.adprime.AdprimeBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/adprime.yaml", factory = YamlPropertySourceFactory.class)
    +public class AdprimeConfiguration {
    +
    +    private static final String BIDDER_NAME = "adprime";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("adprimeConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("adprimeConfigurationProperties")
    +    @ConfigurationProperties("adapters.adprime")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps adprimeBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdprimeBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdtargetConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdtargetConfiguration.java
    index 21b2af0b168..0303208de9b 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdtargetConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdtargetConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps adtargetBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AdtargetBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdtargetBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdtelligentConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdtelligentConfiguration.java
    index 9d7a9e8f653..c13c559ae9a 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdtelligentConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdtelligentConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps adtelligentBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AdtelligentBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdtelligentBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdvangelistsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdvangelistsConfiguration.java
    index c4e23f53981..49a4ea62db0 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AdvangelistsConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdvangelistsConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps advangelistsBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AdvangelistsBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdvangelistsBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdxcgConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdxcgConfiguration.java
    new file mode 100644
    index 00000000000..8a1659243a8
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdxcgConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.adxcg.AdxcgBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/adxcg.yaml", factory = YamlPropertySourceFactory.class)
    +public class AdxcgConfiguration {
    +
    +    private static final String BIDDER_NAME = "adxcg";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("adxcgConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("adxcgConfigurationProperties")
    +    @ConfigurationProperties("adapters.adxcg")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps adxcgBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdxcgBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdyoulikeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdyoulikeConfiguration.java
    new file mode 100644
    index 00000000000..e8e9d6e9954
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdyoulikeConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.adyoulike.AdyoulikeBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/adyoulike.yaml", factory = YamlPropertySourceFactory.class)
    +public class AdyoulikeConfiguration {
    +
    +    private static final String BIDDER_NAME = "adyoulike";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("adyoulikeConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("adyoulikeConfigurationProperties")
    +    @ConfigurationProperties("adapters.adyoulike")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps adyoulileBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AdyoulikeBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AjaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AjaConfiguration.java
    index 675385dfa8a..c0ff4b12730 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AjaConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AjaConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps ajaBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AjaBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AjaBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AlgorixConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AlgorixConfiguration.java
    new file mode 100644
    index 00000000000..b45418b5676
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AlgorixConfiguration.java
    @@ -0,0 +1,53 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.algorix.AlgorixBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/algorix.yaml",
    +        factory = YamlPropertySourceFactory.class)
    +public class AlgorixConfiguration {
    +
    +    private static final String BIDDER_NAME = "algorix";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("algorixConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("algorixConfigurationProperties")
    +    @ConfigurationProperties("adapters.algorix")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps algorixBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AlgorixBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java
    new file mode 100644
    index 00000000000..40242d3c37e
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AmxConfiguration.java
    @@ -0,0 +1,52 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.amx.AmxBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/amx.yaml", factory = YamlPropertySourceFactory.class)
    +public class AmxConfiguration {
    +
    +    private static final String BIDDER_NAME = "amx";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("amxConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("amxConfigurationProperties")
    +    @ConfigurationProperties("adapters.amx")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps amxBidderDeps() {
    +
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AmxBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ApplogyConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ApplogyConfiguration.java
    index 5ea812a80b6..decd29ca15f 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/ApplogyConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/ApplogyConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps applogyBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new ApplogyBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new ApplogyBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AppnexusConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AppnexusConfiguration.java
    index 8ad404f7fd9..36faefc6970 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AppnexusConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AppnexusConfiguration.java
    @@ -1,13 +1,10 @@
     package org.prebid.server.spring.config.bidder;
     
     import org.prebid.server.bidder.BidderDeps;
    -import org.prebid.server.bidder.appnexus.AppnexusAdapter;
     import org.prebid.server.bidder.appnexus.AppnexusBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,15 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps appnexusBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new AppnexusBidder(configProperties.getEndpoint(), mapper))
    -                .adapterCreator(() -> new AppnexusAdapter(usersync.getCookieFamilyName(),
    -                        configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AppnexusBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AvocetConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AvocetConfiguration.java
    index f969df09f37..a87164a1112 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/AvocetConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AvocetConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,10 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps avocetBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new AvocetBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AvocetBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
    -
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AxonixConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AxonixConfiguration.java
    new file mode 100644
    index 00000000000..d0f4b6bcad5
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/AxonixConfiguration.java
    @@ -0,0 +1,52 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.axonix.AxonixBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/axonix.yaml", factory = YamlPropertySourceFactory.class)
    +public class AxonixConfiguration {
    +
    +    private static final String BIDDER_NAME = "axonix";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("axonixConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("axonixConfigurationProperties")
    +    @ConfigurationProperties("adapters.axonix")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps axonixBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new AxonixBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    +
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BeachfrontConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BeachfrontConfiguration.java
    index f9654db662e..9f361445026 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/BeachfrontConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/BeachfrontConfiguration.java
    @@ -8,7 +8,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -47,12 +46,13 @@ BeachfrontConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps beachfrontBidderDeps() {
    -        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new BeachfrontBidder(configProperties.getEndpoint(),
    -                        configProperties.getVideoEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new BeachfrontBidder(
    +                        config.getEndpoint(),
    +                        config.getVideoEndpoint(),
    +                        mapper))
                     .assemble();
         }
     
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BeintooConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BeintooConfiguration.java
    index c426720bc11..40ef37304be 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/BeintooConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/BeintooConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.beintoo.BeintooBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps beintooBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new BeintooBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new BeintooBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BetweenConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BetweenConfiguration.java
    new file mode 100644
    index 00000000000..4295db5b595
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/BetweenConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.between.BetweenBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/between.yaml", factory = YamlPropertySourceFactory.class)
    +public class BetweenConfiguration {
    +
    +    private static final String BIDDER_NAME = "between";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("betweenConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("betweenConfigurationProperties")
    +    @ConfigurationProperties("adapters.between")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps betweenBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new BetweenBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidmachineConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidmachineConfiguration.java
    new file mode 100644
    index 00000000000..4b2dc2a6e4f
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidmachineConfiguration.java
    @@ -0,0 +1,52 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.bidmachine.BidmachineBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/bidmachine.yaml", factory = YamlPropertySourceFactory.class)
    +public class BidmachineConfiguration {
    +
    +    private static final String BIDDER_NAME = "bidmachine";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("bidmachineConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("bidmachineConfigurationProperties")
    +    @ConfigurationProperties("adapters.bidmachine")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps bidmachineBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new BidmachineBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidmyadzConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidmyadzConfiguration.java
    new file mode 100644
    index 00000000000..d1d999100d4
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidmyadzConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.bidmyadz.BidmyadzBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/bidmyadz.yaml", factory = YamlPropertySourceFactory.class)
    +public class BidmyadzConfiguration {
    +
    +    private static final String BIDDER_NAME = "bidmyadz";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("bidmyadzConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("bidmyadzConfigurationProperties")
    +    @ConfigurationProperties("adapters.bidmyadz")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps bidmyadzBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new BidmyadzBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidscubeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidscubeConfiguration.java
    new file mode 100644
    index 00000000000..c25c610a0c6
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidscubeConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.bidscube.BidscubeBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/bidscube.yaml", factory = YamlPropertySourceFactory.class)
    +public class BidscubeConfiguration {
    +
    +    private static final String BIDDER_NAME = "bidscube";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("bidscubeConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("bidscubeConfigurationProperties")
    +    @ConfigurationProperties("adapters.bidscube")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps bidscubeBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new BidscubeBidder(mapper, config.getEndpoint()))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BmtmConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BmtmConfiguration.java
    new file mode 100644
    index 00000000000..27bee9dd6ea
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/BmtmConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.bmtm.BmtmBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/bmtm.yaml", factory = YamlPropertySourceFactory.class)
    +public class BmtmConfiguration {
    +
    +    private static final String BIDDER_NAME = "bmtm";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("bmtmConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("bmtmConfigurationProperties")
    +    @ConfigurationProperties("adapters.bmtm")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps bmtmBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new BmtmBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BrightrollConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BrightrollConfiguration.java
    index bf8084cfff5..00302be6203 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/BrightrollConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/BrightrollConfiguration.java
    @@ -10,7 +10,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -57,19 +56,20 @@ BidderDeps brightrollBidderDeps() {
             final Map publisherIdToOverride = configProperties.getAccounts() == null
                     ? Collections.emptyMap()
                     : configProperties.getAccounts().stream()
    -                        .collect(Collectors.toMap(BidderAccount::getId, this::toPublisherOverride));
    +                .collect(Collectors.toMap(BidderAccount::getId, this::toPublisherOverride));
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new BrightrollBidder(configProperties.getEndpoint(), mapper,
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new BrightrollBidder(
    +                        config.getEndpoint(),
    +                        mapper,
                             publisherIdToOverride))
                     .assemble();
         }
     
         private PublisherOverride toPublisherOverride(BidderAccount bidderAccount) {
             return PublisherOverride.of(bidderAccount.getBadv(), bidderAccount.getBcat(), bidderAccount.getImpBattr(),
    -               bidderAccount.getBidFloor());
    +                bidderAccount.getBidFloor());
         }
     
         @Validated
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ColossusConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ColossusConfiguration.java
    new file mode 100644
    index 00000000000..58830106cce
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/ColossusConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.colossus.ColossusBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/colossus.yaml", factory = YamlPropertySourceFactory.class)
    +public class ColossusConfiguration {
    +
    +    private static final String BIDDER_NAME = "colossus";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("colossusConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("colossusConfigurationProperties")
    +    @ConfigurationProperties("adapters.colossus")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps colossusBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new ColossusBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ConfigurationYeahmobi.java b/src/main/java/org/prebid/server/spring/config/bidder/ConfigurationYeahmobi.java
    index f64616b80a2..06e754ea443 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/ConfigurationYeahmobi.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/ConfigurationYeahmobi.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.yeahmobi.YeahmobiBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,9 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps yeahmobiBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
    -                .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new YeahmobiBidder(configProperties.getEndpoint(), mapper))
    +                .withConfig(configProperties).usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new YeahmobiBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ConnectAdConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ConnectAdConfiguration.java
    new file mode 100644
    index 00000000000..2cbfb25e7ad
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/ConnectAdConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.connectad.ConnectadBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/connectad.yaml", factory = YamlPropertySourceFactory.class)
    +public class ConnectAdConfiguration {
    +
    +    private static final String BIDDER_NAME = "connectad";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("connectadConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("connectadConfigurationProperties")
    +    @ConfigurationProperties("adapters.connectad")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps connectadBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new ConnectadBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ConsumableConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ConsumableConfiguration.java
    index 4945703dea7..d79d0c766bf 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/ConsumableConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/ConsumableConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps consumableBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new ConsumableBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new ConsumableBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ConversantConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ConversantConfiguration.java
    index 91dc61f7823..86be8426e13 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/ConversantConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/ConversantConfiguration.java
    @@ -1,13 +1,13 @@
     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.conversant.ConversantAdapter;
     import org.prebid.server.bidder.conversant.ConversantBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -17,8 +17,10 @@
     import org.springframework.context.annotation.Bean;
     import org.springframework.context.annotation.Configuration;
     import org.springframework.context.annotation.PropertySource;
    +import org.springframework.validation.annotation.Validated;
     
     import javax.validation.constraints.NotBlank;
    +import javax.validation.constraints.NotNull;
     
     @Configuration
     @PropertySource(value = "classpath:/bidder-config/conversant.yaml", factory = YamlPropertySourceFactory.class)
    @@ -35,25 +37,31 @@ public class ConversantConfiguration {
     
         @Autowired
         @Qualifier("conversantConfigurationProperties")
    -    private BidderConfigurationProperties configProperties;
    +    private ConversantConfigurationProperties configProperties;
     
         @Bean("conversantConfigurationProperties")
         @ConfigurationProperties("adapters.conversant")
    -    BidderConfigurationProperties configurationProperties() {
    -        return new BidderConfigurationProperties();
    +    ConversantConfigurationProperties configurationProperties() {
    +        return new ConversantConfigurationProperties();
         }
     
         @Bean
         BidderDeps conversantBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new ConversantBidder(configProperties.getEndpoint(), mapper))
    -                .adapterCreator(() -> new ConversantAdapter(usersync.getCookieFamilyName(),
    -                        configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new ConversantBidder(config.getEndpoint(), configProperties.getGenerateBidId(),
    +                        mapper))
                     .assemble();
         }
    +
    +    @Validated
    +    @Data
    +    @EqualsAndHashCode(callSuper = true)
    +    @NoArgsConstructor
    +    private static class ConversantConfigurationProperties extends BidderConfigurationProperties {
    +
    +        @NotNull
    +        private Boolean generateBidId;
    +    }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/CpmStarConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/CpmStarConfiguration.java
    index 3035cec5f2b..68e93e1d3f9 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/CpmStarConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/CpmStarConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps cpmstarBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new CpmStarBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new CpmStarBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/CriteoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/CriteoConfiguration.java
    new file mode 100644
    index 00000000000..0647b7d574e
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/CriteoConfiguration.java
    @@ -0,0 +1,67 @@
    +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.criteo.CriteoBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +import org.springframework.validation.annotation.Validated;
    +
    +import javax.validation.constraints.NotBlank;
    +import javax.validation.constraints.NotNull;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/criteo.yaml", factory = YamlPropertySourceFactory.class)
    +public class CriteoConfiguration {
    +
    +    private static final String BIDDER_NAME = "criteo";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("criteoConfigurationProperties")
    +    private CriteoConfigurationProperties configProperties;
    +
    +    @Bean("criteoConfigurationProperties")
    +    @ConfigurationProperties("adapters.criteo")
    +    CriteoConfigurationProperties configurationProperties() {
    +        return new CriteoConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps criteoBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config ->
    +                        new CriteoBidder(config.getEndpoint(), mapper, configProperties.getGenerateSlotId()))
    +                .assemble();
    +    }
    +
    +    @Validated
    +    @Data
    +    @EqualsAndHashCode(callSuper = true)
    +    @NoArgsConstructor
    +    private static class CriteoConfigurationProperties extends BidderConfigurationProperties {
    +
    +        @NotNull
    +        private Boolean generateSlotId;
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DatablocksConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DatablocksConfiguration.java
    index 4eaa10950f3..c0be7355116 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/DatablocksConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/DatablocksConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps datablocksBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new DatablocksBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new DatablocksBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DecenteradsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DecenteradsConfiguration.java
    new file mode 100644
    index 00000000000..403771912c8
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/DecenteradsConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.decenterads.DecenteradsBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/decenterads.yaml", factory = YamlPropertySourceFactory.class)
    +public class DecenteradsConfiguration {
    +
    +    private static final String BIDDER_NAME = "decenterads";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("decenteradsConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("decenteradsConfigurationProperties")
    +    @ConfigurationProperties("adapters.decenterads")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps decenteradsBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new DecenteradsBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DeepintentConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DeepintentConfiguration.java
    new file mode 100644
    index 00000000000..5c0f9f360fa
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/DeepintentConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.deepintent.DeepintentBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/deepintent.yaml", factory = YamlPropertySourceFactory.class)
    +public class DeepintentConfiguration {
    +
    +    private static final String BIDDER_NAME = "deepintent";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("deepintentConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("deepintentConfigurationProperties")
    +    @ConfigurationProperties("adapters.deepintent")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps deepintentBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new DeepintentBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/DmxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/DmxConfiguration.java
    index cd27128ed33..a3440843a64 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/DmxConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/DmxConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.dmx.DmxBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps dmxBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new DmxBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new DmxBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EmxDigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EmxDigitalConfiguration.java
    index c5b36ff9d85..c4b42e27680 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/EmxDigitalConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/EmxDigitalConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps emxDigitalBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new EmxDigitalBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new EmxDigitalBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EngagebdrConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EngagebdrConfiguration.java
    index 1b0541c953d..98f0ed029c1 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/EngagebdrConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/EngagebdrConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps engagebdrBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new EngagebdrBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new EngagebdrBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EplanningConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EplanningConfiguration.java
    index c60575a7c23..be155807061 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/EplanningConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/EplanningConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps eplanningBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new EplanningBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new EplanningBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EpomConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EpomConfiguration.java
    new file mode 100644
    index 00000000000..a9a9c0d9834
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/EpomConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.epom.EpomBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/epom.yaml", factory = YamlPropertySourceFactory.class)
    +public class EpomConfiguration {
    +
    +    private static final String BIDDER_NAME = "epom";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("epomConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("epomConfigurationProperties")
    +    @ConfigurationProperties("adapters.epom")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps epomBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new EpomBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EvolutionConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EvolutionConfiguration.java
    new file mode 100644
    index 00000000000..59c981a5fc8
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/EvolutionConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.evolution.EvolutionBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/evolution.yaml", factory = YamlPropertySourceFactory.class)
    +public class EvolutionConfiguration {
    +
    +    private static final String BIDDER_NAME = "e_volution";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("evolutionConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("evolutionConfigurationProperties")
    +    @ConfigurationProperties("adapters.evolution")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps evolutionBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new EvolutionBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FacebookConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FacebookConfiguration.java
    index fe8cf6876ee..0779fd44715 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/FacebookConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/FacebookConfiguration.java
    @@ -7,9 +7,7 @@
     import org.prebid.server.bidder.facebook.FacebookBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -43,15 +41,15 @@ FacebookConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps facebookBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
    -        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, null))
    +                .usersyncerCreator(UsersyncerCreator.create(null))
                     .bidderCreator(configProperties.getEnabled()
    -                        ? () -> new FacebookBidder(configProperties.getEndpoint(), configProperties.getPlatformId(),
    -                        configProperties.getAppSecret(), mapper)
    +                        ? config -> new FacebookBidder(
    +                        config.getEndpoint(),
    +                        config.getPlatformId(),
    +                        config.getAppSecret(),
    +                        configProperties.getTimeoutNotificationUrlTemplate(), mapper)
                             : null)
                     .assemble();
         }
    @@ -67,5 +65,8 @@ private static class FacebookConfigurationProperties extends BidderConfiguration
     
             @NotNull
             private String appSecret;
    +
    +        @NotNull
    +        private String timeoutNotificationUrlTemplate;
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GammaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GammaConfiguration.java
    index 45ba40e34ff..3435b15e208 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/GammaConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/GammaConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps gammaBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new GammaBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new GammaBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GamoshiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GamoshiConfiguration.java
    index 42ce780f2d5..44ee27fef48 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/GamoshiConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/GamoshiConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps gamoshiBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new GamoshiBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new GamoshiBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GridConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GridConfiguration.java
    index f3932540321..3810f471c4e 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/GridConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/GridConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps gridBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new GridBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new GridBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/GumgumConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/GumgumConfiguration.java
    index 0509e7e55ac..c1706eb9953 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/GumgumConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/GumgumConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps gumGumOneBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new GumgumBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new GumgumBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ImprovedigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ImprovedigitalConfiguration.java
    index 24294d22683..4db80aeebbe 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/ImprovedigitalConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/ImprovedigitalConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps improvedigitalBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new ImprovedigitalBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new ImprovedigitalBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/InmobiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/InmobiConfiguration.java
    similarity index 76%
    rename from src/main/java/org/prebid/server/spring/config/InmobiConfiguration.java
    rename to src/main/java/org/prebid/server/spring/config/bidder/InmobiConfiguration.java
    index 9a063b2db5c..1dafdd159ee 100644
    --- a/src/main/java/org/prebid/server/spring/config/InmobiConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/InmobiConfiguration.java
    @@ -1,12 +1,10 @@
    -package org.prebid.server.spring.config;
    +package org.prebid.server.spring.config.bidder;
     
     import org.prebid.server.bidder.BidderDeps;
     import org.prebid.server.bidder.inmobi.InmobiBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps inmobiBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new InmobiBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new InmobiBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/InteractiveOffersConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/InteractiveOffersConfiguration.java
    new file mode 100644
    index 00000000000..1fe16a2ae38
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/InteractiveOffersConfiguration.java
    @@ -0,0 +1,52 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.interactiveoffers.InteractiveOffersBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/interactiveoffers.yaml", factory = YamlPropertySourceFactory.class)
    +public class InteractiveOffersConfiguration {
    +
    +    private static final String BIDDER_NAME = "interactiveoffers";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("interactiveoffersConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("interactiveoffersConfigurationProperties")
    +    @ConfigurationProperties("adapters.interactiveoffers")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps interactiveoffersBidderDeps() {
    +
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new InteractiveOffersBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java
    new file mode 100644
    index 00000000000..ecdc684aa2f
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/InvibesConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.invibes.InvibesBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/invibes.yaml", factory = YamlPropertySourceFactory.class)
    +public class InvibesConfiguration {
    +
    +    private static final String BIDDER_NAME = "invibes";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("invibesConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("invibesConfigurationProperties")
    +    @ConfigurationProperties("adapters.invibes")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps invibesBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new InvibesBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/IxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/IxConfiguration.java
    index cfab7086df8..1f93961d506 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/IxConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/IxConfiguration.java
    @@ -1,13 +1,10 @@
     package org.prebid.server.spring.config.bidder;
     
     import org.prebid.server.bidder.BidderDeps;
    -import org.prebid.server.bidder.ix.IxAdapter;
     import org.prebid.server.bidder.ix.IxBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,15 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps ixBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new IxBidder(configProperties.getEndpoint(), mapper))
    -                .adapterCreator(() -> new IxAdapter(usersync.getCookieFamilyName(), configProperties.getEndpoint(),
    -                        mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new IxBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/JixieConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/JixieConfiguration.java
    new file mode 100644
    index 00000000000..6283dd4b7ac
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/JixieConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.jixie.JixieBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/jixie.yaml", factory = YamlPropertySourceFactory.class)
    +public class JixieConfiguration {
    +
    +    private static final String BIDDER_NAME = "jixie";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("jixieConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("jixieConfigurationProperties")
    +    @ConfigurationProperties("adapters.jixie")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps jixieBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new JixieBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/KayzenConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KayzenConfiguration.java
    new file mode 100644
    index 00000000000..c1cdcfdf51b
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/KayzenConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.kayzen.KayzenBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/kayzen.yaml", factory = YamlPropertySourceFactory.class)
    +public class KayzenConfiguration {
    +
    +    private static final String BIDDER_NAME = "kayzen";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("kayzenConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("kayzenConfigurationProperties")
    +    @ConfigurationProperties("adapters.kayzen")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps kayzenBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new KayzenBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/KidozConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KidozConfiguration.java
    index 97b4af511dd..cb3ce6090bf 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/KidozConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/KidozConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps kidozBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new KidozBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new KidozBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/KrushmediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KrushmediaConfiguration.java
    new file mode 100644
    index 00000000000..f5c1f939aa4
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/KrushmediaConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.krushmedia.KrushmediaBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/krushmedia.yaml", factory = YamlPropertySourceFactory.class)
    +public class KrushmediaConfiguration {
    +
    +    private static final String BIDDER_NAME = "krushmedia";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("krushmediaConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("krushmediaConfigurationProperties")
    +    @ConfigurationProperties("adapters.krushmedia")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps krushmediaBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new KrushmediaBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/KubientConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KubientConfiguration.java
    index bcc49f59d89..2e9c58faa19 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/KubientConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/KubientConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps kubientBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new KubientBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new KubientBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LifestreetConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LifestreetConfiguration.java
    deleted file mode 100644
    index bbc8912b6b6..00000000000
    --- a/src/main/java/org/prebid/server/spring/config/bidder/LifestreetConfiguration.java
    +++ /dev/null
    @@ -1,59 +0,0 @@
    -package org.prebid.server.spring.config.bidder;
    -
    -import org.prebid.server.bidder.BidderDeps;
    -import org.prebid.server.bidder.lifestreet.LifestreetAdapter;
    -import org.prebid.server.bidder.lifestreet.LifestreetBidder;
    -import org.prebid.server.json.JacksonMapper;
    -import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
    -import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    -import org.prebid.server.spring.env.YamlPropertySourceFactory;
    -import org.springframework.beans.factory.annotation.Autowired;
    -import org.springframework.beans.factory.annotation.Qualifier;
    -import org.springframework.beans.factory.annotation.Value;
    -import org.springframework.boot.context.properties.ConfigurationProperties;
    -import org.springframework.context.annotation.Bean;
    -import org.springframework.context.annotation.Configuration;
    -import org.springframework.context.annotation.PropertySource;
    -
    -import javax.validation.constraints.NotBlank;
    -
    -@Configuration
    -@PropertySource(value = "classpath:/bidder-config/lifestreet.yaml", factory = YamlPropertySourceFactory.class)
    -public class LifestreetConfiguration {
    -
    -    private static final String BIDDER_NAME = "lifestreet";
    -
    -    @Value("${external-url}")
    -    @NotBlank
    -    private String externalUrl;
    -
    -    @Autowired
    -    private JacksonMapper mapper;
    -
    -    @Autowired
    -    @Qualifier("lifestreetConfigurationProperties")
    -    private BidderConfigurationProperties configProperties;
    -
    -    @Bean("lifestreetConfigurationProperties")
    -    @ConfigurationProperties("adapters.lifestreet")
    -    BidderConfigurationProperties configurationProperties() {
    -        return new BidderConfigurationProperties();
    -    }
    -
    -    @Bean
    -    BidderDeps lifestreetBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
    -        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    -                .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new LifestreetBidder(configProperties.getEndpoint(), mapper))
    -                .adapterCreator(() -> new LifestreetAdapter(usersync.getCookieFamilyName(),
    -                        configProperties.getEndpoint(), mapper))
    -                .assemble();
    -    }
    -}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LockerdomeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LockerdomeConfiguration.java
    index 4d7acd2aca9..4fdeb3f5b58 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/LockerdomeConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/LockerdomeConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps lockerdomeBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new LockerdomeBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new LockerdomeBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LogicadConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LogicadConfiguration.java
    index adaf6cfe4d4..c6f06965cec 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/LogicadConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/LogicadConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.logicad.LogicadBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps logicadBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new LogicadBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new LogicadBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java
    new file mode 100644
    index 00000000000..15f081fc523
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.loopme.LoopmeBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/loopme.yaml", factory = YamlPropertySourceFactory.class)
    +public class LoopmeConfiguration {
    +
    +    private static final String BIDDER_NAME = "loopme";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("loopmeConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("loopmeConfigurationProperties")
    +    @ConfigurationProperties("adapters.loopme")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps loopmeBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new LoopmeBidder(configProperties.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LunamediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LunamediaConfiguration.java
    index 67b5e21af9c..c9ec2b01cfd 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/LunamediaConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/LunamediaConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.lunamedia.LunamediaBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps lunamediaBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new LunamediaBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new LunamediaBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MadvertiseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MadvertiseConfiguration.java
    new file mode 100644
    index 00000000000..c6cf8815390
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/MadvertiseConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.madvertise.MadvertiseBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/madvertise.yaml", factory = YamlPropertySourceFactory.class)
    +public class MadvertiseConfiguration {
    +
    +    private static final String BIDDER_NAME = "madvertise";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("madvertiseConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("madvertiseConfigurationProperties")
    +    @ConfigurationProperties("adapters.madvertise")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps madvertiseBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new MadvertiseBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MarsmediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MarsmediaConfiguration.java
    index b2684a89f84..2aa6bdefc67 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/MarsmediaConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/MarsmediaConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps marsmediaBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new MarsmediaBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new MarsmediaBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MedianetConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MedianetConfiguration.java
    new file mode 100644
    index 00000000000..90a061c6150
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/MedianetConfiguration.java
    @@ -0,0 +1,57 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.medianet.MedianetBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.prebid.server.util.HttpUtil;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/medianet.yaml", factory = YamlPropertySourceFactory.class)
    +public class MedianetConfiguration {
    +
    +    private static final String BIDDER_NAME = "medianet";
    +    private static final String EXTERNAL_URL_MACRO = "{{PREBID_SERVER_ENDPOINT}}";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("medianetConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("medianetConfigurationProperties")
    +    @ConfigurationProperties("adapters.medianet")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps medianetBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new MedianetBidder(resolveEndpoint(config.getEndpoint()), mapper))
    +                .assemble();
    +    }
    +
    +    private String resolveEndpoint(String configEndpoint) {
    +        return configEndpoint.replace(EXTERNAL_URL_MACRO, HttpUtil.encodeUrl(externalUrl));
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java
    index 333782c787a..fc9ab73ea5a 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/MgidConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps mgidBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new MgidBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new MgidBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MobfoxpbConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MobfoxpbConfiguration.java
    new file mode 100644
    index 00000000000..1bab192e03f
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/MobfoxpbConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.mobfoxpb.MobfoxpbBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/mobfoxpb.yaml", factory = YamlPropertySourceFactory.class)
    +public class MobfoxpbConfiguration {
    +
    +    private static final String BIDDER_NAME = "mobfoxpb";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("mobfoxpbConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("mobfoxpbConfigurationProperties")
    +    @ConfigurationProperties("adapters.mobfoxpb")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps mobfoxpbBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new MobfoxpbBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MobilefuseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MobilefuseConfiguration.java
    index d4bf6e3badb..f10a3b6f7db 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/MobilefuseConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/MobilefuseConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.mobilefuse.MobilefuseBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps mobilefuseBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new MobilefuseBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new MobilefuseBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/NanointeractiveConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/NanointeractiveConfiguration.java
    index adcfa4cc54b..4ef236cc5d0 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/NanointeractiveConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/NanointeractiveConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps nanointeractiveBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new NanointeractiveBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new NanointeractiveBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/NinthdecimalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/NinthdecimalConfiguration.java
    index afb349cf5f8..9e46fdce768 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/NinthdecimalConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/NinthdecimalConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps ninthdecimalBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new NinthdecimalBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new NinthdecimalBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java
    new file mode 100644
    index 00000000000..d61b953554f
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/NobidConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.nobid.NobidBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/nobid.yaml", factory = YamlPropertySourceFactory.class)
    +public class NobidConfiguration {
    +
    +    private static final String BIDDER_NAME = "nobid";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("nobidConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("nobidConfigurationProperties")
    +    @ConfigurationProperties("adapters.nobid")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps nobidBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new NobidBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OnetagConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OnetagConfiguration.java
    new file mode 100644
    index 00000000000..c72fa861929
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/OnetagConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.onetag.OnetagBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/onetag.yaml", factory = YamlPropertySourceFactory.class)
    +public class OnetagConfiguration {
    +
    +    private static final String BIDDER_NAME = "onetag";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("onetagConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("onetagConfigurationProperties")
    +    @ConfigurationProperties("adapters.onetag")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps onetagBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new OnetagBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OpenxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OpenxConfiguration.java
    index 5acd81d5e29..f22b3a4aa97 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/OpenxConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/OpenxConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties openxProperties() {
         BidderDeps openxBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new OpenxBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new OpenxBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OrbidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OrbidderConfiguration.java
    index 9436c25bd8d..ad1889633b9 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/OrbidderConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/OrbidderConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.orbidder.OrbidderBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps orbidderBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new OrbidderBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new OrbidderBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OutbrainConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OutbrainConfiguration.java
    new file mode 100644
    index 00000000000..a58f12c5ec6
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/OutbrainConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.outbrain.OutbrainBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/outbrain.yaml", factory = YamlPropertySourceFactory.class)
    +public class OutbrainConfiguration {
    +
    +    private static final String BIDDER_NAME = "outbrain";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("outbrainConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("outbrainConfigurationProperties")
    +    @ConfigurationProperties("adapters.outbrain")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps outbrainBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new OutbrainBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PangleConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PangleConfiguration.java
    new file mode 100644
    index 00000000000..26c04626faf
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/PangleConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.pangle.PangleBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/pangle.yaml", factory = YamlPropertySourceFactory.class)
    +public class PangleConfiguration {
    +
    +    private static final String BIDDER_NAME = "pangle";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("pangleConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("pangleConfigurationProperties")
    +    @ConfigurationProperties("adapters.pangle")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps pangleBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new PangleBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PubmaticConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PubmaticConfiguration.java
    index 390f967e65f..f003cb50281 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/PubmaticConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/PubmaticConfiguration.java
    @@ -1,13 +1,10 @@
     package org.prebid.server.spring.config.bidder;
     
     import org.prebid.server.bidder.BidderDeps;
    -import org.prebid.server.bidder.pubmatic.PubmaticAdapter;
     import org.prebid.server.bidder.pubmatic.PubmaticBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,15 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps pubmaticBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new PubmaticBidder(configProperties.getEndpoint(), mapper))
    -                .adapterCreator(() -> new PubmaticAdapter(usersync.getCookieFamilyName(),
    -                        configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new PubmaticBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PubnativeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PubnativeConfiguration.java
    index 6c36b9fdf60..f30c9fd4c22 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/PubnativeConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/PubnativeConfiguration.java
    @@ -2,10 +2,10 @@
     
     import org.prebid.server.bidder.BidderDeps;
     import org.prebid.server.bidder.pubnative.PubnativeBidder;
    +import org.prebid.server.currency.CurrencyConversionService;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -27,6 +27,9 @@ public class PubnativeConfiguration {
         @Autowired
         private JacksonMapper mapper;
     
    +    @Autowired
    +    private CurrencyConversionService currencyConversionService;
    +
         @Value("${external-url}")
         @NotBlank
         private String externalUrl;
    @@ -45,9 +48,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps pubnativeBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new PubnativeBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new PubnativeBidder(config.getEndpoint(), mapper, currencyConversionService))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PulsepointConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PulsepointConfiguration.java
    index 9effc1ba3e1..7f261d76cce 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/PulsepointConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/PulsepointConfiguration.java
    @@ -1,13 +1,10 @@
     package org.prebid.server.spring.config.bidder;
     
     import org.prebid.server.bidder.BidderDeps;
    -import org.prebid.server.bidder.pulsepoint.PulsepointAdapter;
     import org.prebid.server.bidder.pulsepoint.PulsepointBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,15 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps pulsepointBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new PulsepointBidder(configProperties.getEndpoint(), mapper))
    -                .adapterCreator(() -> new PulsepointAdapter(usersync.getCookieFamilyName(),
    -                        configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new PulsepointBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RevcontentConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RevcontentConfiguration.java
    new file mode 100644
    index 00000000000..5d57c74789c
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/RevcontentConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.revcontent.RevcontentBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/revcontent.yaml", factory = YamlPropertySourceFactory.class)
    +public class RevcontentConfiguration {
    +
    +    private static final String BIDDER_NAME = "revcontent";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("revcontentConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("revcontentConfigurationProperties")
    +    @ConfigurationProperties("adapters.revcontent")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps revcontentBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new RevcontentBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RhythmoneConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RhythmoneConfiguration.java
    index d75b646049b..34127b5698d 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/RhythmoneConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/RhythmoneConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps rhythmOneBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new RhythmoneBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new RhythmoneBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RtbhouseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RtbhouseConfiguration.java
    index affaa13d154..41eb67e486a 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/RtbhouseConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/RtbhouseConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps rtbhouseBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new RtbhouseBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new RtbhouseBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java
    index 048e1e79200..9bff280a227 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java
    @@ -4,13 +4,11 @@
     import lombok.EqualsAndHashCode;
     import lombok.NoArgsConstructor;
     import org.prebid.server.bidder.BidderDeps;
    -import org.prebid.server.bidder.rubicon.RubiconAdapter;
     import org.prebid.server.bidder.rubicon.RubiconBidder;
    +import org.prebid.server.currency.CurrencyConversionService;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -33,6 +31,9 @@ public class RubiconConfiguration {
         @Autowired
         private JacksonMapper mapper;
     
    +    @Autowired
    +    private CurrencyConversionService currencyConversionService;
    +
         @Autowired
         @Qualifier("rubiconConfigurationProperties")
         private RubiconConfigurationProperties configProperties;
    @@ -45,18 +46,17 @@ RubiconConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps rubiconBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
    -        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, null))
    -                .bidderCreator(() -> new RubiconBidder(configProperties.getEndpoint(),
    -                        configProperties.getXapi().getUsername(), configProperties.getXapi().getPassword(),
    -                        configProperties.getMetaInfo().getSupportedVendors(), configProperties.getGenerateBidId(),
    +                .usersyncerCreator(UsersyncerCreator.create(null))
    +                .bidderCreator(config -> new RubiconBidder(
    +                        config.getEndpoint(),
    +                        config.getXapi().getUsername(),
    +                        config.getXapi().getPassword(),
    +                        config.getMetaInfo().getSupportedVendors(),
    +                        config.getGenerateBidId(),
    +                        currencyConversionService,
                             mapper))
    -                .adapterCreator(() -> new RubiconAdapter(usersync.getCookieFamilyName(), configProperties.getEndpoint(),
    -                        configProperties.getXapi().getUsername(), configProperties.getXapi().getPassword(), mapper))
                     .assemble();
         }
     
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SaLunamediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SaLunamediaConfiguration.java
    new file mode 100644
    index 00000000000..1b58d2a8d57
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SaLunamediaConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.salunamedia.SaLunamediaBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/salunamedia.yaml", factory = YamlPropertySourceFactory.class)
    +public class SaLunamediaConfiguration {
    +
    +    private static final String BIDDER_NAME = "sa_lunamedia";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("salunamediaConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("salunamediaConfigurationProperties")
    +    @ConfigurationProperties("adapters.salunamedia")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps saLunamediaBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SaLunamediaBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SharethroughConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SharethroughConfiguration.java
    index 43b01c26552..f5aff9dd0ba 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/SharethroughConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SharethroughConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps sharethroughBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new SharethroughBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SharethroughBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java
    new file mode 100644
    index 00000000000..e40e1a0887d
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SilvermobConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.silvermob.SilvermobBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/silvermob.yaml", factory = YamlPropertySourceFactory.class)
    +public class SilvermobConfiguration {
    +
    +    private static final String BIDDER_NAME = "silvermob";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("silvermobConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("silvermobConfigurationProperties")
    +    @ConfigurationProperties("adapters.silvermob")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps silvermobBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SilvermobBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.java
    new file mode 100644
    index 00000000000..ea4b32242fc
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.smilewanted.SmileWantedBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/smilewanted.yaml", factory = YamlPropertySourceFactory.class)
    +public class SimpleWantedConfiguration {
    +
    +    private static final String BIDDER_NAME = "smilewanted";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("smilewantedConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("smilewantedConfigurationProperties")
    +    @ConfigurationProperties("adapters.smilewanted")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps smilewantedBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SmileWantedBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmaatoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmaatoConfiguration.java
    index 364493700f8..f21f0ee3aa9 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/SmaatoConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmaatoConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.smaato.SmaatoBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -18,6 +16,7 @@
     import org.springframework.context.annotation.PropertySource;
     
     import javax.validation.constraints.NotBlank;
    +import java.time.Clock;
     
     @Configuration
     @PropertySource(value = "classpath:/bidder-config/smaato.yaml", factory = YamlPropertySourceFactory.class)
    @@ -32,6 +31,9 @@ public class SmaatoConfiguration {
         @Autowired
         private JacksonMapper mapper;
     
    +    @Autowired
    +    private Clock clock;
    +
         @Autowired
         @Qualifier("smaatoConfigurationProperties")
         private BidderConfigurationProperties configProperties;
    @@ -44,13 +46,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps smaatoBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new SmaatoBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SmaatoBidder(config.getEndpoint(), mapper, clock))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java
    index 925bd6dda7f..13f71215b5f 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmartadserverConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.smartadserver.SmartadserverBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps smartadserverBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new SmartadserverBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SmartadserverBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmartrtbConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmartrtbConfiguration.java
    index c9b670fe582..61c918503ee 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/SmartrtbConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmartrtbConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps smartrtbBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new SmartrtbBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SmartrtbBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmartyAdsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmartyAdsConfiguration.java
    new file mode 100644
    index 00000000000..84ca5a69cd5
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmartyAdsConfiguration.java
    @@ -0,0 +1,51 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.smartyads.SmartyAdsBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/smartyads.yaml", factory = YamlPropertySourceFactory.class)
    +public class SmartyAdsConfiguration {
    +
    +    private static final String BIDDER_NAME = "smartyads";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("smartyadsConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("smartyadsConfigurationProperties")
    +    @ConfigurationProperties("adapters.smartyads")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps smartyadsBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SmartyAdsBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SomoaudienceConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SomoaudienceConfiguration.java
    index 9d546b0d666..e25f8962332 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/SomoaudienceConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SomoaudienceConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps somoaudienceBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new SomoaudienceBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SomoaudienceBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java
    index ac9092e18df..4ca7dc80b15 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps sonobiBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new SonobiBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SonobiBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SovrnConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SovrnConfiguration.java
    index 3660a30ab55..7d1bd4ee486 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/SovrnConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SovrnConfiguration.java
    @@ -1,13 +1,10 @@
     package org.prebid.server.spring.config.bidder;
     
     import org.prebid.server.bidder.BidderDeps;
    -import org.prebid.server.bidder.sovrn.SovrnAdapter;
     import org.prebid.server.bidder.sovrn.SovrnBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,15 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps sovrnBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new SovrnBidder(configProperties.getEndpoint(), mapper))
    -                .adapterCreator(() -> new SovrnAdapter(usersync.getCookieFamilyName(), configProperties.getEndpoint(),
    -                        mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SovrnBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SynacormediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SynacormediaConfiguration.java
    index f6e208cf12e..00f643b950a 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/SynacormediaConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/SynacormediaConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps synacormediaBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new SynacormediaBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new SynacormediaBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java
    index bb9722afb15..a9622617819 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps tappxBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new TappxBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new TappxBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TelariaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TelariaConfiguration.java
    index 31d8b30929a..de2d207077e 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/TelariaConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/TelariaConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps telariaBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new TelariaBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new TelariaBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TripleliftConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TripleliftConfiguration.java
    index fa9fc5f2c60..022a52154bb 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/TripleliftConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/TripleliftConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps tripleliftBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new TripleliftBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new TripleliftBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TripleliftNativeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TripleliftNativeConfiguration.java
    index d5848e023cd..e4e05980bd1 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/TripleliftNativeConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/TripleliftNativeConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,10 +44,11 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps tripleliftNativeBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new TripleliftNativeBidder(configProperties.getEndpoint(),
    -                        configProperties.getExtraInfo(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new TripleliftNativeBidder(
    +                        config.getEndpoint(),
    +                        config.getExtraInfo(),
    +                        mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TtxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TtxConfiguration.java
    index 1f31ac4d3e1..e89269d7995 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/TtxConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/TtxConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps ttxBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new TtxBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new TtxBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/UcfunnelConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/UcfunnelConfiguration.java
    index 2eecb506205..ddd90c2d904 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/UcfunnelConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/UcfunnelConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps ucfunnelBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new UcfunnelBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new UcfunnelBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/UnicornConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/UnicornConfiguration.java
    new file mode 100644
    index 00000000000..fcd9754f1c2
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/UnicornConfiguration.java
    @@ -0,0 +1,52 @@
    +package org.prebid.server.spring.config.bidder;
    +
    +import org.prebid.server.bidder.BidderDeps;
    +import org.prebid.server.bidder.unicorn.UnicornBidder;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.beans.factory.annotation.Qualifier;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.context.properties.ConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.PropertySource;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +@Configuration
    +@PropertySource(value = "classpath:/bidder-config/unicorn.yaml", factory = YamlPropertySourceFactory.class)
    +public class UnicornConfiguration {
    +
    +    private static final String BIDDER_NAME = "unicorn";
    +
    +    @Value("${external-url}")
    +    @NotBlank
    +    private String externalUrl;
    +
    +    @Autowired
    +    private JacksonMapper mapper;
    +
    +    @Autowired
    +    @Qualifier("unicornConfigurationProperties")
    +    private BidderConfigurationProperties configProperties;
    +
    +    @Bean("unicornConfigurationProperties")
    +    @ConfigurationProperties("adapters.unicorn")
    +    BidderConfigurationProperties configurationProperties() {
    +        return new BidderConfigurationProperties();
    +    }
    +
    +    @Bean
    +    BidderDeps unicornBidderDeps() {
    +        return BidderDepsAssembler.forBidder(BIDDER_NAME)
    +                .withConfig(configProperties)
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new UnicornBidder(config.getEndpoint(), mapper))
    +                .assemble();
    +    }
    +}
    +
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/UnrulyConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/UnrulyConfiguration.java
    index 55106c5c102..55845aad445 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/UnrulyConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/UnrulyConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps unrulyBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new UnrulyBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new UnrulyBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ValueImpressionConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ValueImpressionConfiguration.java
    index 22de2234417..c579e662316 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/ValueImpressionConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/ValueImpressionConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps valueimpressionBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new ValueImpressionBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new ValueImpressionBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VerizonmediaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VerizonmediaConfiguration.java
    index 748173ed5f3..0d9ff349da6 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/VerizonmediaConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/VerizonmediaConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps verizonmediaBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new VerizonmediaBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new VerizonmediaBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VisxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VisxConfiguration.java
    index 1bc27162e0e..1e543fae239 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/VisxConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/VisxConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps visxBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new VisxBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new VisxBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/VrtcalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VrtcalConfiguration.java
    index a13468fb0b3..047b2283a71 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/VrtcalConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/VrtcalConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps vrtcalBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new VrtcalBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new VrtcalBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YieldlabConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YieldlabConfiguration.java
    index 9b8d8080e36..cc4d0517374 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/YieldlabConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/YieldlabConfiguration.java
    @@ -4,9 +4,7 @@
     import org.prebid.server.bidder.yieldlab.YieldlabBidder;
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -44,13 +42,10 @@ BidderConfigurationProperties configurationProperties() {
     
         @Bean
         BidderDeps yieldlabBidderDeps() {
    -        final UsersyncConfigurationProperties usersync = configProperties.getUsersync();
    -
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
    -                .bidderCreator(() -> new YieldlabBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new YieldlabBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YieldmoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YieldmoConfiguration.java
    index 8dcd2fbd33d..0531bdd0777 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/YieldmoConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/YieldmoConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps yieldmoBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new YieldmoBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new YieldmoBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/YieldoneConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/YieldoneConfiguration.java
    index d54ae67461d..b7bc9a79902 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/YieldoneConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/YieldoneConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps yieldoneBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new YieldoneBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new YieldoneBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ZeroclickfraudConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ZeroclickfraudConfiguration.java
    index 3fb7e97d953..c794e627d70 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/ZeroclickfraudConfiguration.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/ZeroclickfraudConfiguration.java
    @@ -5,7 +5,6 @@
     import org.prebid.server.json.JacksonMapper;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
     import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
    -import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
     import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
     import org.prebid.server.spring.env.YamlPropertySourceFactory;
     import org.springframework.beans.factory.annotation.Autowired;
    @@ -45,9 +44,8 @@ BidderConfigurationProperties configurationProperties() {
         BidderDeps zeroclickfraudBidderDeps() {
             return BidderDepsAssembler.forBidder(BIDDER_NAME)
                     .withConfig(configProperties)
    -                .bidderInfo(BidderInfoCreator.create(configProperties))
    -                .usersyncerCreator(UsersyncerCreator.create(configProperties.getUsersync(), externalUrl))
    -                .bidderCreator(() -> new ZeroclickfraudBidder(configProperties.getEndpoint(), mapper))
    +                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
    +                .bidderCreator(config -> new ZeroclickfraudBidder(config.getEndpoint(), mapper))
                     .assemble();
         }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java
    index 3b15c821976..498265e6549 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java
    @@ -1,17 +1,16 @@
     package org.prebid.server.spring.config.bidder.model;
     
     import lombok.Data;
    -import lombok.NoArgsConstructor;
     import org.springframework.validation.annotation.Validated;
     
     import javax.validation.constraints.NotBlank;
     import javax.validation.constraints.NotNull;
    +import java.util.Collections;
     import java.util.List;
     import java.util.Map;
     
     @Validated
     @Data
    -@NoArgsConstructor
     public class BidderConfigurationProperties {
     
         @NotNull
    @@ -33,7 +32,7 @@ public class BidderConfigurationProperties {
         private List deprecatedNames;
     
         @NotNull
    -    private List aliases;
    +    private Map aliases = Collections.emptyMap();
     
         @NotNull
         private MetaInfo metaInfo;
    @@ -42,4 +41,10 @@ public class BidderConfigurationProperties {
         private UsersyncConfigurationProperties usersync;
     
         private Map extraInfo;
    +
    +    private final Class selfClass;
    +
    +    public BidderConfigurationProperties() {
    +        selfClass = this.getClass();
    +    }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/UsersyncConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/bidder/model/UsersyncConfigurationProperties.java
    index 1467a62ec3c..3e1c371e2c2 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/model/UsersyncConfigurationProperties.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/UsersyncConfigurationProperties.java
    @@ -24,4 +24,22 @@ public class UsersyncConfigurationProperties {
     
         @NotNull
         Boolean supportCors;
    +
    +    SecondaryConfigurationProperties secondary;
    +
    +    @Validated
    +    @Data
    +    @NoArgsConstructor
    +    public static class SecondaryConfigurationProperties {
    +
    +        String url;
    +
    +        String redirectUrl;
    +
    +        @NotBlank
    +        String type;
    +
    +        @NotNull
    +        Boolean supportCors;
    +    }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java
    index 78b16841ca5..6e7124f5422 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderDepsAssembler.java
    @@ -1,86 +1,179 @@
     package org.prebid.server.spring.config.bidder.util;
     
    -import org.prebid.server.bidder.Adapter;
    +import com.fasterxml.jackson.annotation.JsonInclude;
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.DeserializationFeature;
    +import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.ObjectMapper;
    +import com.github.fge.jsonpatch.JsonPatchException;
    +import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
    +import org.apache.commons.lang3.StringUtils;
     import org.prebid.server.bidder.Bidder;
     import org.prebid.server.bidder.BidderDeps;
    -import org.prebid.server.bidder.DisabledAdapter;
    +import org.prebid.server.bidder.BidderInstanceDeps;
     import org.prebid.server.bidder.DisabledBidder;
     import org.prebid.server.bidder.Usersyncer;
     import org.prebid.server.proto.response.BidderInfo;
     import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
    -
    +import org.prebid.server.spring.config.bidder.model.MetaInfo;
    +import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
    +import org.prebid.server.spring.env.YamlPropertySourceFactory;
    +import org.springframework.boot.context.properties.bind.Binder;
    +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
    +import org.springframework.core.io.InputStreamResource;
    +import org.yaml.snakeyaml.Yaml;
    +
    +import java.io.ByteArrayInputStream;
    +import java.util.ArrayList;
     import java.util.List;
    -import java.util.function.Supplier;
    +import java.util.Map;
    +import java.util.Properties;
    +import java.util.function.Function;
    +import java.util.stream.Collectors;
     
    -public class BidderDepsAssembler {
    +public class BidderDepsAssembler {
     
         private static final String ERROR_MESSAGE_TEMPLATE_FOR_DISABLED = "%s is not configured properly on this "
                 + "Prebid Server deploy. If you believe this should work, contact the company hosting the service "
                 + "and tell them to check their configuration.";
     
    +    private static final ObjectMapper MAPPER = new ObjectMapper()
    +            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    +            .setSerializationInclusion(JsonInclude.Include.NON_NULL);
    +
         private String bidderName;
    -    private boolean enabled;
    -    private List deprecatedNames;
    -    private List aliases;
    -    private BidderInfo bidderInfo;
    -    private Supplier usersyncerCreator;
    -    private Supplier> bidderCreator;
    -    private Supplier> adapterCreator;
    +    private CFG configProperties;
    +    private Function usersyncerCreator;
    +    private Function> bidderCreator;
     
         private BidderDepsAssembler() {
    -        enabled = false;
         }
     
    -    public static BidderDepsAssembler forBidder(String bidderName) {
    -        final BidderDepsAssembler assembler = new BidderDepsAssembler();
    +    public static  BidderDepsAssembler forBidder(String bidderName) {
    +        final BidderDepsAssembler assembler = new BidderDepsAssembler<>();
             assembler.bidderName = bidderName;
             return assembler;
         }
     
    -    public BidderDepsAssembler bidderInfo(BidderInfo bidderInfo) {
    -        this.bidderInfo = bidderInfo;
    -        return this;
    -    }
    +    public BidderDepsAssembler usersyncerCreator(
    +            Function usersyncerCreator) {
     
    -    public BidderDepsAssembler usersyncerCreator(Supplier usersyncerCreator) {
             this.usersyncerCreator = usersyncerCreator;
             return this;
         }
     
    -    public BidderDepsAssembler bidderCreator(Supplier> bidderCreator) {
    +    public BidderDepsAssembler bidderCreator(Function> bidderCreator) {
             this.bidderCreator = bidderCreator;
             return this;
         }
     
    -    public BidderDepsAssembler adapterCreator(Supplier> adapterCreator) {
    -        this.adapterCreator = adapterCreator;
    +    public BidderDepsAssembler withConfig(CFG configProperties) {
    +        this.configProperties = configProperties;
             return this;
         }
     
    -    public BidderDepsAssembler withConfig(BidderConfigurationProperties configProperties) {
    -        enabled = configProperties.getEnabled();
    -        deprecatedNames = configProperties.getDeprecatedNames();
    -        aliases = configProperties.getAliases();
    -        return this;
    +    public BidderDeps assemble() {
    +        return BidderDeps.of(coreAndAliasesDeps());
         }
     
    -    public BidderDeps assemble() {
    -        final Usersyncer usersyncer = enabled ? usersyncerCreator.get() : null;
    +    private List coreAndAliasesDeps() {
    +        final List deps = new ArrayList<>();
     
    -        final Bidder bidder = enabled ? bidderCreator.get()
    -                : new DisabledBidder(String.format(ERROR_MESSAGE_TEMPLATE_FOR_DISABLED, bidderName));
    +        deps.add(coreDeps());
    +        deps.addAll(aliasesDeps());
    +
    +        return deps;
    +    }
    +
    +    private BidderInstanceDeps coreDeps() {
    +        return deps(bidderName, BidderInfoCreator.create(configProperties), configProperties);
    +    }
    +
    +    private List aliasesDeps() {
    +        return configProperties.getAliases().entrySet().stream()
    +                .map(this::aliasDeps)
    +                .collect(Collectors.toList());
    +    }
    +
    +    private BidderInstanceDeps aliasDeps(Map.Entry entry) {
    +        final String alias = entry.getKey();
    +        final CFG aliasConfigProperties = mergeAliasConfiguration(entry.getValue(), configProperties);
    +
    +        validateCapabilities(alias, aliasConfigProperties, bidderName, configProperties);
     
    -        final Adapter adapter = enabled ? (adapterCreator != null ? adapterCreator.get() : null)
    -                : new DisabledAdapter(String.format(ERROR_MESSAGE_TEMPLATE_FOR_DISABLED, bidderName));
    +        return deps(alias, BidderInfoCreator.create(aliasConfigProperties, bidderName), aliasConfigProperties);
    +    }
     
    -        return BidderDeps.builder()
    +    private BidderInstanceDeps deps(String bidderName, BidderInfo bidderInfo, CFG configProperties) {
    +        return BidderInstanceDeps.builder()
                     .name(bidderName)
    -                .deprecatedNames(deprecatedNames)
    -                .aliases(aliases)
    +                .deprecatedNames(configProperties.getDeprecatedNames())
                     .bidderInfo(bidderInfo)
    -                .usersyncer(usersyncer)
    -                .bidder(bidder)
    -                .adapter(adapter)
    +                .usersyncer(usersyncer(configProperties))
    +                .bidder(bidder(configProperties))
                     .build();
         }
    +
    +    private Usersyncer usersyncer(CFG configProperties) {
    +        return configProperties.getEnabled() ? usersyncerCreator.apply(configProperties.getUsersync()) : null;
    +    }
    +
    +    private Bidder bidder(CFG configProperties) {
    +        return configProperties.getEnabled()
    +                ? bidderCreator.apply(configProperties)
    +                : new DisabledBidder(String.format(ERROR_MESSAGE_TEMPLATE_FOR_DISABLED, bidderName));
    +    }
    +
    +    private CFG mergeAliasConfiguration(Object aliasConfiguration, CFG coreConfiguration) {
    +        return mergeConfigurations(
    +                configurationAsPropertiesObject(aliasConfiguration, coreConfiguration.getSelfClass()),
    +                coreConfiguration);
    +    }
    +
    +    private void validateCapabilities(String alias, CFG aliasConfiguration, String coreBidder, CFG coreConfiguration) {
    +        final MetaInfo coreMetaInfo = coreConfiguration.getMetaInfo();
    +        final MetaInfo aliasMetaInfo = aliasConfiguration.getMetaInfo();
    +        final List coreAppMediaTypes = coreMetaInfo.getAppMediaTypes();
    +        final List coreSiteMediaTypes = coreMetaInfo.getSiteMediaTypes();
    +        final List aliasAppMediaTypes = aliasMetaInfo.getAppMediaTypes();
    +        final List aliasSiteMediaTypes = aliasMetaInfo.getSiteMediaTypes();
    +
    +        if (!coreAppMediaTypes.containsAll(aliasAppMediaTypes)
    +                || !coreSiteMediaTypes.containsAll(aliasSiteMediaTypes)) {
    +
    +            throw new IllegalArgumentException(String.format(
    +                    "Alias %s supports more capabilities (app: %s, site: %s) "
    +                            + "than the core bidder %s (app: %s, site: %s)",
    +                    alias,
    +                    aliasAppMediaTypes,
    +                    aliasSiteMediaTypes,
    +                    coreBidder,
    +                    coreAppMediaTypes,
    +                    coreSiteMediaTypes));
    +        }
    +    }
    +
    +    @SuppressWarnings({"unchecked", "rawtypes"})
    +    private CFG configurationAsPropertiesObject(Object configuration, Class targetClass) {
    +        final String configAsYamlString = new Yaml().dump(configuration);
    +
    +        final Properties configAsProperties = YamlPropertySourceFactory.readPropertiesFromYamlResource(
    +                new InputStreamResource(new ByteArrayInputStream(configAsYamlString.getBytes())));
    +        final Binder configurationBinder = new Binder(new MapConfigurationPropertySource(configAsProperties));
    +
    +        return (CFG) configurationBinder.bind(StringUtils.EMPTY, (Class) targetClass).get();
    +    }
    +
    +    @SuppressWarnings({"unchecked", "rawtypes"})
    +    private CFG mergeConfigurations(CFG aliasConfiguration, CFG coreConfiguration) {
    +        try {
    +            final JsonNode mergedNode = JsonMergePatch
    +                    .fromJson(MAPPER.valueToTree(aliasConfiguration))
    +                    .apply(MAPPER.valueToTree(coreConfiguration));
    +
    +            return (CFG) MAPPER.treeToValue(mergedNode, (Class) coreConfiguration.getSelfClass());
    +        } catch (JsonPatchException | JsonProcessingException e) {
    +            throw new IllegalArgumentException("Exception occurred while merging alias configuration", e);
    +        }
    +    }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java
    index 1633f0a5824..704cefde1e4 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java
    @@ -11,9 +11,15 @@ private BidderInfoCreator() {
         }
     
         public static BidderInfo create(BidderConfigurationProperties configurationProperties) {
    +        return create(configurationProperties, null);
    +    }
    +
    +    public static BidderInfo create(BidderConfigurationProperties configurationProperties, String aliasOf) {
             final MetaInfo metaInfo = configurationProperties.getMetaInfo();
             return BidderInfo.create(
                     configurationProperties.getEnabled(),
    +                configurationProperties.getEndpoint(),
    +                aliasOf,
                     metaInfo.getMaintainerEmail(),
                     metaInfo.getAppMediaTypes(),
                     metaInfo.getSiteMediaTypes(),
    diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java
    index 8a77403476e..0a060ff26c9 100644
    --- a/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java
    +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java
    @@ -1,23 +1,81 @@
     package org.prebid.server.spring.config.bidder.util;
     
    +import org.apache.commons.lang3.StringUtils;
     import org.prebid.server.bidder.Usersyncer;
     import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
    +import org.prebid.server.util.HttpUtil;
     
    -import java.util.function.Supplier;
    +import java.util.Objects;
    +import java.util.function.Function;
     
     public class UsersyncerCreator {
     
         private UsersyncerCreator() {
    +    }
     
    +    public static Function create(String externalUrl) {
    +        return usersyncConfig -> createAndValidate(usersyncConfig, externalUrl);
         }
     
    -    public static Supplier create(UsersyncConfigurationProperties usersync, String externalUrl) {
    -        return () -> new Usersyncer(
    +    private static Usersyncer createAndValidate(UsersyncConfigurationProperties usersync, String externalUrl) {
    +        final Usersyncer usersyncer = Usersyncer.of(
                     usersync.getCookieFamilyName(),
    +                toPrimaryMethod(usersync, externalUrl),
    +                toSecondaryMethod(usersync, externalUrl));
    +
    +        if (StringUtils.isBlank(usersyncer.getPrimaryMethod().getUsersyncUrl())
    +                && usersyncer.getSecondaryMethod() != null) {
    +            throw new IllegalArgumentException(String.format(
    +                    "Invalid usersync configuration: primary method is missing while secondary is present. "
    +                            + "Configuration: %s",
    +                    usersync.toString()));
    +        }
    +
    +        return usersyncer;
    +    }
    +
    +    private static Usersyncer.UsersyncMethod toPrimaryMethod(UsersyncConfigurationProperties usersync,
    +                                                             String externalUrl) {
    +        return toUsersyncMethod(
                     usersync.getUrl(),
                     usersync.getRedirectUrl(),
                     externalUrl,
                     usersync.getType(),
                     usersync.getSupportCors());
         }
    +
    +    private static Usersyncer.UsersyncMethod toSecondaryMethod(UsersyncConfigurationProperties usersync,
    +                                                               String externalUrl) {
    +
    +        final UsersyncConfigurationProperties.SecondaryConfigurationProperties secondaryMethodConfig =
    +                usersync.getSecondary();
    +
    +        return secondaryMethodConfig != null
    +                ? toUsersyncMethod(
    +                secondaryMethodConfig.getUrl(),
    +                secondaryMethodConfig.getRedirectUrl(),
    +                externalUrl,
    +                secondaryMethodConfig.getType(),
    +                secondaryMethodConfig.getSupportCors())
    +                : null;
    +    }
    +
    +    private static Usersyncer.UsersyncMethod toUsersyncMethod(String usersyncUrl,
    +                                                              String redirectUrl,
    +                                                              String externalUri,
    +                                                              String type,
    +                                                              boolean supportCORS) {
    +
    +        return Usersyncer.UsersyncMethod.of(
    +                type,
    +                Objects.requireNonNull(usersyncUrl),
    +                toRedirectUrl(redirectUrl, externalUri),
    +                supportCORS);
    +    }
    +
    +    private static String toRedirectUrl(String redirectUrl, String externalUri) {
    +        return StringUtils.isNotBlank(redirectUrl)
    +                ? HttpUtil.validateUrl(externalUri) + redirectUrl
    +                : StringUtils.EMPTY;
    +    }
     }
    diff --git a/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java b/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java
    index ac556347f51..b0206149f0d 100644
    --- a/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java
    +++ b/src/main/java/org/prebid/server/spring/config/model/ExternalConversionProperties.java
    @@ -4,12 +4,14 @@
     import lombok.AllArgsConstructor;
     import lombok.Data;
     import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.metric.Metrics;
     import org.prebid.server.vertx.http.HttpClient;
     import org.springframework.validation.annotation.Validated;
     
     import javax.validation.constraints.Min;
     import javax.validation.constraints.NotBlank;
     import javax.validation.constraints.NotNull;
    +import java.time.Clock;
     
     @Validated
     @Data
    @@ -21,11 +23,17 @@ public class ExternalConversionProperties {
     
         @NotNull
         @Min(2)
    -    Long defaultTimeout;
    +    Long defaultTimeoutMs;
     
         @NotNull
         @Min(2)
    -    Long refreshPeriod;
    +    Long refreshPeriodMs;
    +
    +    @NotNull
    +    Long staleAfterMs;
    +
    +    @Min(2)
    +    Long stalePeriodMs;
     
         @NotNull
         Vertx vertx;
    @@ -33,6 +41,12 @@ public class ExternalConversionProperties {
         @NotNull
         HttpClient httpClient;
     
    +    @NotNull
    +    Metrics metrics;
    +
    +    @NotNull
    +    Clock clock;
    +
         @NotNull
         JacksonMapper mapper;
     }
    diff --git a/src/main/java/org/prebid/server/spring/env/YamlPropertySourceFactory.java b/src/main/java/org/prebid/server/spring/env/YamlPropertySourceFactory.java
    index 2588be21a7a..6e599602b9e 100644
    --- a/src/main/java/org/prebid/server/spring/env/YamlPropertySourceFactory.java
    +++ b/src/main/java/org/prebid/server/spring/env/YamlPropertySourceFactory.java
    @@ -3,6 +3,7 @@
     import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
     import org.springframework.core.env.PropertiesPropertySource;
     import org.springframework.core.env.PropertySource;
    +import org.springframework.core.io.Resource;
     import org.springframework.core.io.support.EncodedResource;
     import org.springframework.core.io.support.PropertySourceFactory;
     
    @@ -28,12 +29,17 @@ public PropertySource createPropertySource(@Nullable String name, @Nonnull En
             return new PropertiesPropertySource(sourceName, propertiesFromYaml);
         }
     
    +    public static Properties readPropertiesFromYamlResource(Resource resource) {
    +        final YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
    +        factory.setResources(resource);
    +        factory.afterPropertiesSet();
    +
    +        return factory.getObject();
    +    }
    +
         private Properties loadYamlIntoProperties(EncodedResource resource) throws FileNotFoundException {
             try {
    -            final YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
    -            factory.setResources(resource.getResource());
    -            factory.afterPropertiesSet();
    -            return factory.getObject();
    +            return readPropertiesFromYamlResource(resource.getResource());
             } catch (IllegalStateException e) {
                 // for ignoreResourceNotFound
                 final Throwable cause = e.getCause();
    diff --git a/src/main/java/org/prebid/server/util/DealUtil.java b/src/main/java/org/prebid/server/util/DealUtil.java
    new file mode 100644
    index 00000000000..3eee5580973
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/util/DealUtil.java
    @@ -0,0 +1,24 @@
    +package org.prebid.server.util;
    +
    +import org.prebid.server.auction.BidderAliases;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDeal;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine;
    +
    +import java.util.Objects;
    +
    +public class DealUtil {
    +
    +    private DealUtil() {
    +    }
    +
    +    /**
    +     * Returns true if imp[].pmp.deal[].ext.line object has given bidder.
    +     */
    +    public static boolean isBidderHasDeal(String bidder, ExtDeal extDeal, BidderAliases aliases) {
    +        final ExtDealLine extDealLine = extDeal != null ? extDeal.getLine() : null;
    +        final String dealLineBidder = extDealLine != null ? extDealLine.getBidder() : null;
    +        return dealLineBidder == null || Objects.equals(dealLineBidder, bidder)
    +                || Objects.equals(aliases.resolveBidder(dealLineBidder), bidder)
    +                || Objects.equals(aliases.resolveBidder(bidder), dealLineBidder); // filter only PG related deals
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/util/HttpUtil.java b/src/main/java/org/prebid/server/util/HttpUtil.java
    index 3cf372b089f..613ab6d47e7 100644
    --- a/src/main/java/org/prebid/server/util/HttpUtil.java
    +++ b/src/main/java/org/prebid/server/util/HttpUtil.java
    @@ -1,14 +1,20 @@
     package org.prebid.server.util;
     
     import io.netty.handler.codec.http.HttpHeaderValues;
    -import io.netty.handler.codec.http.HttpResponseStatus;
    +import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
     import io.vertx.core.MultiMap;
     import io.vertx.core.http.Cookie;
     import io.vertx.core.http.HttpHeaders;
    -import io.vertx.core.http.HttpServerRequest;
     import io.vertx.core.http.HttpServerResponse;
    +import io.vertx.core.logging.Logger;
    +import io.vertx.core.logging.LoggerFactory;
     import io.vertx.ext.web.RoutingContext;
     import org.apache.commons.lang3.StringUtils;
    +import org.prebid.server.exception.PreBidException;
    +import org.prebid.server.log.ConditionalLogger;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
    +import org.prebid.server.model.Endpoint;
    +import org.prebid.server.model.HttpRequestContext;
     
     import java.io.UnsupportedEncodingException;
     import java.net.MalformedURLException;
    @@ -16,7 +22,16 @@
     import java.net.URLDecoder;
     import java.net.URLEncoder;
     import java.nio.charset.StandardCharsets;
    +import java.time.ZonedDateTime;
    +import java.util.Arrays;
    +import java.util.Base64;
    +import java.util.Collections;
    +import java.util.HashSet;
    +import java.util.List;
     import java.util.Map;
    +import java.util.Set;
    +import java.util.function.Consumer;
    +import java.util.function.Function;
     import java.util.stream.Collectors;
     
     /**
    @@ -24,8 +39,11 @@
      */
     public final class HttpUtil {
     
    +    private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class);
    +    private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger);
    +
         public static final String APPLICATION_JSON_CONTENT_TYPE =
    -            HttpHeaderValues.APPLICATION_JSON.toString() + ";" + HttpHeaderValues.CHARSET.toString() + "="
    +            HttpHeaderValues.APPLICATION_JSON + ";" + HttpHeaderValues.CHARSET + "="
                         + StandardCharsets.UTF_8.toString().toLowerCase();
     
         public static final CharSequence X_FORWARDED_FOR_HEADER = HttpHeaders.createOptimized("X-Forwarded-For");
    @@ -33,6 +51,7 @@ public final class HttpUtil {
         public static final CharSequence X_REQUEST_AGENT_HEADER = HttpHeaders.createOptimized("X-Request-Agent");
         public static final CharSequence ORIGIN_HEADER = HttpHeaders.createOptimized("Origin");
         public static final CharSequence ACCEPT_HEADER = HttpHeaders.createOptimized("Accept");
    +    public static final CharSequence SEC_GPC_HEADER = HttpHeaders.createOptimized("Sec-GPC");
         public static final CharSequence CONTENT_TYPE_HEADER = HttpHeaders.createOptimized("Content-Type");
         public static final CharSequence X_REQUESTED_WITH_HEADER = HttpHeaders.createOptimized("X-Requested-With");
         public static final CharSequence REFERER_HEADER = HttpHeaders.createOptimized("Referer");
    @@ -46,22 +65,16 @@ public final class HttpUtil {
         public static final CharSequence EXPIRES_HEADER = HttpHeaders.createOptimized("Expires");
         public static final CharSequence PRAGMA_HEADER = HttpHeaders.createOptimized("Pragma");
         public static final CharSequence LOCATION_HEADER = HttpHeaders.createOptimized("Location");
    +    public static final CharSequence CONNECTION_HEADER = HttpHeaders.createOptimized("Connection");
    +    public static final CharSequence ACCEPT_ENCODING_HEADER = HttpHeaders.createOptimized("Accept-Encoding");
    +    public static final CharSequence X_OPENRTB_VERSION_HEADER = HttpHeaders.createOptimized("x-openrtb-version");
    +    public static final CharSequence X_PREBID_HEADER = HttpHeaders.createOptimized("x-prebid");
    +    private static final Set SENSITIVE_HEADERS = new HashSet<>(Arrays.asList(AUTHORIZATION_HEADER.toString()));
    +    public static final CharSequence PG_TRX_ID = HttpHeaders.createOptimized("pg-trx-id");
     
    -    private HttpUtil() {
    -    }
    +    private static final String BASIC_AUTH_PATTERN = "Basic %s";
     
    -    /**
    -     * Detects whether browser is safari or not by user agent analysis.
    -     */
    -    public static boolean isSafari(String userAgent) {
    -        // this is a simple heuristic based on this article:
    -        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
    -        //
    -        // there are libraries available doing different kinds of User-Agent analysis but they impose performance
    -        // implications as well, example: https://github.com/nielsbasjes/yauaa
    -        return StringUtils.isNotBlank(userAgent)
    -                && userAgent.contains("AppleWebKit") && userAgent.contains("Safari")
    -                && !userAgent.contains("Chrome") && !userAgent.contains("Chromium");
    +    private HttpUtil() {
         }
     
         /**
    @@ -120,24 +133,29 @@ public static void addHeaderIfValueIsNotEmpty(MultiMap headers, CharSequence hea
             }
         }
     
    -    /**
    -     * Determines IP-Address by checking "X-Forwarded-For", "X-Real-IP" http headers or remote host address
    -     * if both are empty.
    -     */
    -    public static String ipFrom(HttpServerRequest request) {
    -        // X-Forwarded-For: client1, proxy1, proxy2
    -        String ip = StringUtils.trimToNull(
    -                StringUtils.substringBefore(request.headers().get("X-Forwarded-For"), ","));
    -        if (ip == null) {
    -            ip = StringUtils.trimToNull(request.headers().get("X-Real-IP"));
    +    public static ZonedDateTime getDateFromHeader(MultiMap headers, String header) {
    +        return getDateFromHeader(headers::get, header);
    +    }
    +
    +    public static ZonedDateTime getDateFromHeader(CaseInsensitiveMultiMap headers, String header) {
    +        return getDateFromHeader(headers::get, header);
    +    }
    +
    +    private static ZonedDateTime getDateFromHeader(Function headerGetter, String header) {
    +        final String isoTimeStamp = headerGetter.apply(header);
    +        if (isoTimeStamp == null) {
    +            return null;
             }
    -        if (ip == null) {
    -            ip = StringUtils.trimToNull(request.remoteAddress().host());
    +
    +        try {
    +            return ZonedDateTime.parse(isoTimeStamp);
    +        } catch (Exception e) {
    +            throw new PreBidException(String.format("%s header is not compatible to ISO-8601 format: %s",
    +                    header, isoTimeStamp));
             }
    -        return ip;
         }
     
    -    public static String getDomainFromUrl(String url) {
    +    public static String getHostFromUrl(String url) {
             if (StringUtils.isBlank(url)) {
                 return null;
             }
    @@ -148,8 +166,21 @@ public static String getDomainFromUrl(String url) {
             }
         }
     
    -    public static Map cookiesAsMap(RoutingContext context) {
    -        return context.cookieMap().entrySet().stream()
    +    public static Map cookiesAsMap(HttpRequestContext httpRequest) {
    +        final String cookieHeader = httpRequest.getHeaders().get(HttpHeaders.COOKIE);
    +        if (cookieHeader == null) {
    +            return Collections.emptyMap();
    +        }
    +
    +        return ServerCookieDecoder.STRICT.decode(cookieHeader).stream()
    +                .collect(Collectors.toMap(
    +                        io.netty.handler.codec.http.cookie.Cookie::name,
    +                        io.netty.handler.codec.http.cookie.Cookie::value));
    +
    +    }
    +
    +    public static Map cookiesAsMap(RoutingContext routingContext) {
    +        return routingContext.cookieMap().entrySet().stream()
                     .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().getValue()));
         }
     
    @@ -157,15 +188,58 @@ public static String toSetCookieHeaderValue(Cookie cookie) {
             return String.join("; ", cookie.encode(), "SameSite=None; Secure");
         }
     
    +    public static boolean executeSafely(RoutingContext routingContext, Endpoint endpoint,
    +                                        Consumer responseConsumer) {
    +        return executeSafely(routingContext, endpoint.value(), responseConsumer);
    +    }
    +
    +    public static boolean executeSafely(RoutingContext routingContext, String endpoint,
    +                                        Consumer responseConsumer) {
    +
    +        final HttpServerResponse response = routingContext.response();
    +
    +        if (response.closed()) {
    +            conditionalLogger.warn(
    +                    String.format("Client already closed connection, response to %s will be skipped", endpoint),
    +                    0.01);
    +            return false;
    +        }
    +
    +        try {
    +            responseConsumer.accept(response);
    +            return true;
    +        } catch (Throwable e) {
    +            logger.warn("Failed to send {0} response: {1}", endpoint, e.getMessage());
    +            return false;
    +        }
    +    }
    +
         /**
    -     * Sends HTTP response according to the given status and body
    +     * Creates standart basic auth header value
          */
    -    public static void respondWith(RoutingContext context, HttpResponseStatus status, String body) {
    -        final HttpServerResponse response = context.response().setStatusCode(status.code());
    -        if (body != null) {
    -            response.end(body);
    -        } else {
    -            response.end();
    -        }
    +    public static String makeBasicAuthHeaderValue(String username, String password) {
    +        return String.format(BASIC_AUTH_PATTERN, Base64.getEncoder().encodeToString((username + ':' + password)
    +                .getBytes()));
    +    }
    +
    +    /**
    +     * Converts {@link MultiMap} headers format to Map, where keys are headers names and values are lists
    +     * of header's values
    +     */
    +    public static Map> toDebugHeaders(MultiMap headers) {
    +        return headers != null
    +                ? headers.entries().stream()
    +                .filter(entry -> !isSensitiveHeader(entry.getKey()))
    +                .collect(Collectors.toMap(Map.Entry::getKey,
    +                        entry -> StringUtils.isNotBlank(entry.getValue())
    +                                ? Arrays.stream(entry.getValue().split(","))
    +                                .map(String::trim)
    +                                .collect(Collectors.toList())
    +                                : Collections.singletonList(entry.getValue())))
    +                : null;
    +    }
    +
    +    private static boolean isSensitiveHeader(String header) {
    +        return SENSITIVE_HEADERS.stream().anyMatch(header::equalsIgnoreCase);
         }
     }
    diff --git a/src/main/java/org/prebid/server/util/JsonMergeUtil.java b/src/main/java/org/prebid/server/util/JsonMergeUtil.java
    deleted file mode 100644
    index de5b4371095..00000000000
    --- a/src/main/java/org/prebid/server/util/JsonMergeUtil.java
    +++ /dev/null
    @@ -1,77 +0,0 @@
    -package org.prebid.server.util;
    -
    -import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.fasterxml.jackson.databind.JsonNode;
    -import com.github.fge.jsonpatch.JsonPatchException;
    -import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
    -import org.apache.commons.lang3.ObjectUtils;
    -import org.prebid.server.exception.InvalidRequestException;
    -import org.prebid.server.json.JacksonMapper;
    -
    -import java.io.IOException;
    -import java.util.Objects;
    -
    -// TODO: refactor to be instance instead of util
    -public class JsonMergeUtil {
    -
    -    private final JacksonMapper mapper;
    -
    -    public JsonMergeUtil(JacksonMapper mapper) {
    -        this.mapper = Objects.requireNonNull(mapper);
    -    }
    -
    -    /**
    -     * Merges passed object with json retrieved from stored data map by id
    -     * and cast it to appropriate class. In case of any exception during merging, throws {@link InvalidRequestException}
    -     * with reason message.
    -     */
    -    public  T merge(T originalObject, String storedData, String id, Class classToCast) {
    -        final JsonNode originJsonNode = mapper.mapper().valueToTree(originalObject);
    -        final JsonNode storedRequestJsonNode;
    -        try {
    -            storedRequestJsonNode = mapper.mapper().readTree(storedData);
    -        } catch (IOException e) {
    -            throw new InvalidRequestException(
    -                    String.format("Can't parse Json for stored request with id %s", id));
    -        }
    -        try {
    -            // Http request fields have higher priority and will override fields from stored requests
    -            // in case they have different values
    -            return mapper.mapper().treeToValue(JsonMergePatch.fromJson(originJsonNode).apply(storedRequestJsonNode),
    -                    classToCast);
    -        } catch (JsonPatchException e) {
    -            throw new InvalidRequestException(String.format(
    -                    "Couldn't create merge patch from origin object node for id %s: %s", id, e.getMessage()));
    -        } catch (JsonProcessingException e) {
    -            throw new InvalidRequestException(
    -                    String.format("Can't convert merging result for id %s: %s", id, e.getMessage()));
    -        }
    -    }
    -
    -    public  T merge(T originalObject, T mergingObject, Class classToCast) {
    -        if (!ObjectUtils.allNotNull(originalObject, mergingObject)) {
    -            return ObjectUtils.firstNonNull(originalObject, mergingObject);
    -        }
    -
    -        final JsonNode originJsonNode = mapper.mapper().valueToTree(originalObject);
    -        final JsonNode mergingObjectJsonNode = mapper.mapper().valueToTree(mergingObject);
    -        try {
    -            final JsonNode mergedNode = JsonMergePatch.fromJson(originJsonNode).apply(mergingObjectJsonNode);
    -            return mapper.mapper().treeToValue(mergedNode, classToCast);
    -        } catch (JsonPatchException e) {
    -            throw new InvalidRequestException(String.format(
    -                    "Couldn't create merge patch for objects with class %s", classToCast.getName()));
    -        } catch (JsonProcessingException e) {
    -            throw new InvalidRequestException(
    -                    String.format("Can't convert merging result class %s", classToCast.getName()));
    -        }
    -    }
    -
    -    public JsonNode merge(JsonNode originalObject, JsonNode mergingObject) {
    -        try {
    -            return JsonMergePatch.fromJson(originalObject).apply(mergingObject);
    -        } catch (JsonPatchException e) {
    -            throw new InvalidRequestException("Couldn't create merge patch for json nodes");
    -        }
    -    }
    -}
    diff --git a/src/main/java/org/prebid/server/util/LineItemUtil.java b/src/main/java/org/prebid/server/util/LineItemUtil.java
    new file mode 100644
    index 00000000000..f45cc0d6d5b
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/util/LineItemUtil.java
    @@ -0,0 +1,76 @@
    +package org.prebid.server.util;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.iab.openrtb.request.Deal;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Pmp;
    +import com.iab.openrtb.response.Bid;
    +import io.vertx.core.logging.Logger;
    +import io.vertx.core.logging.LoggerFactory;
    +import org.apache.commons.collections4.CollectionUtils;
    +import org.apache.commons.lang3.StringUtils;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDeal;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine;
    +
    +import java.util.List;
    +import java.util.Objects;
    +
    +public class LineItemUtil {
    +
    +    private static final Logger logger = LoggerFactory.getLogger(LineItemUtil.class);
    +
    +    private LineItemUtil() {
    +    }
    +
    +    /**
    +     * Extracts line item ID from the given {@link Bid}.
    +     */
    +    public static String lineItemIdFrom(Bid bid, List imps, JacksonMapper mapper) {
    +        if (StringUtils.isEmpty(bid.getDealid())) {
    +            return null;
    +        }
    +        final ExtDealLine extDealLine = extDealLineFrom(bid, imps, mapper);
    +        return extDealLine != null ? extDealLine.getLineItemId() : null;
    +    }
    +
    +    private static ExtDealLine extDealLineFrom(Bid bid, List imps, JacksonMapper mapper) {
    +        final Imp correspondingImp = imps.stream()
    +                .filter(imp -> Objects.equals(imp.getId(), bid.getImpid()))
    +                .findFirst()
    +                .orElse(null);
    +        return correspondingImp != null ? extDealLineFrom(bid, correspondingImp, mapper) : null;
    +    }
    +
    +    public static ExtDealLine extDealLineFrom(Bid bid, Imp imp, JacksonMapper mapper) {
    +        if (StringUtils.isEmpty(bid.getDealid())) {
    +            return null;
    +        }
    +
    +        final Pmp pmp = imp.getPmp();
    +        final List deals = pmp != null ? pmp.getDeals() : null;
    +        return CollectionUtils.isEmpty(deals)
    +                ? null
    +                : deals.stream()
    +                        .filter(Objects::nonNull)
    +                        .filter(deal -> Objects.equals(deal.getId(), bid.getDealid())) // find deal by ID
    +                        .map(Deal::getExt)
    +                        .filter(Objects::nonNull)
    +                        .map((ObjectNode ext) -> dealExt(ext, mapper))
    +                        .filter(Objects::nonNull)
    +                        .map(ExtDeal::getLine)
    +                        .findFirst()
    +                        .orElse(null);
    +    }
    +
    +    private static ExtDeal dealExt(JsonNode ext, JacksonMapper mapper) {
    +        try {
    +            return mapper.mapper().treeToValue(ext, ExtDeal.class);
    +        } catch (JsonProcessingException e) {
    +            logger.warn("Error decoding deal.ext: {0}", e, e.getMessage());
    +            return null;
    +        }
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/util/ObjectUtils.java b/src/main/java/org/prebid/server/util/ObjectUtils.java
    new file mode 100644
    index 00000000000..f925aa42321
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/util/ObjectUtils.java
    @@ -0,0 +1,25 @@
    +package org.prebid.server.util;
    +
    +import java.util.Objects;
    +import java.util.function.Function;
    +import java.util.function.Supplier;
    +import java.util.stream.Stream;
    +
    +public final class ObjectUtils {
    +
    +    private ObjectUtils() {
    +    }
    +
    +    @SafeVarargs
    +    public static  T firstNonNull(Supplier... suppliers) {
    +        return Stream.of(suppliers)
    +                .map(Supplier::get)
    +                .filter(Objects::nonNull)
    +                .findFirst()
    +                .orElse(null);
    +    }
    +
    +    public static  T getIfNotNull(S source, Function getter) {
    +        return source != null ? getter.apply(source) : null;
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/util/VersionInfo.java b/src/main/java/org/prebid/server/util/VersionInfo.java
    new file mode 100644
    index 00000000000..71b2fa87723
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/util/VersionInfo.java
    @@ -0,0 +1,61 @@
    +package org.prebid.server.util;
    +
    +import com.fasterxml.jackson.annotation.JsonProperty;
    +import io.vertx.core.logging.Logger;
    +import io.vertx.core.logging.LoggerFactory;
    +import lombok.AllArgsConstructor;
    +import lombok.Value;
    +import org.prebid.server.json.JacksonMapper;
    +
    +import java.io.IOException;
    +import java.util.regex.Matcher;
    +import java.util.regex.Pattern;
    +
    +@Value
    +public class VersionInfo {
    +
    +    private static final Logger logger = LoggerFactory.getLogger(VersionInfo.class);
    +    private static final String UNDEFINED = "undefined";
    +
    +    String version;
    +    String commitHash;
    +
    +    private VersionInfo(String version, String commitHash) {
    +        this.version = version;
    +        this.commitHash = commitHash;
    +    }
    +
    +    public static VersionInfo create(String revisionFilePath, JacksonMapper jacksonMapper) {
    +        Revision revision;
    +        try {
    +            revision = jacksonMapper.mapper().readValue(ResourceUtil.readFromClasspath(revisionFilePath),
    +                    Revision.class);
    +        } catch (IllegalArgumentException | IOException e) {
    +            logger.error("Was not able to read revision file {0}. Reason: {1}", revisionFilePath, e.getMessage());
    +            return new VersionInfo(UNDEFINED, UNDEFINED);
    +        }
    +        final String pbsVersion = revision.getPbsVersion();
    +        final String commitHash = revision.getCommitHash();
    +        return new VersionInfo(
    +                pbsVersion != null ? extractVersion(pbsVersion) : UNDEFINED,
    +                commitHash != null ? commitHash : UNDEFINED);
    +    }
    +
    +    private static String extractVersion(String buildVersion) {
    +        final Pattern versionPattern = Pattern.compile("\\d+\\.\\d+\\.\\d");
    +        final Matcher versionMatcher = versionPattern.matcher(buildVersion);
    +
    +        return versionMatcher.lookingAt() ? versionMatcher.group() : null;
    +    }
    +
    +    @AllArgsConstructor(staticName = "of")
    +    @Value
    +    private static class Revision {
    +
    +        @JsonProperty("git.commit.id")
    +        String commitHash;
    +
    +        @JsonProperty("git.build.version")
    +        String pbsVersion;
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/validation/BidderParamValidator.java b/src/main/java/org/prebid/server/validation/BidderParamValidator.java
    index 733baea9d72..13a405bd2db 100644
    --- a/src/main/java/org/prebid/server/validation/BidderParamValidator.java
    +++ b/src/main/java/org/prebid/server/validation/BidderParamValidator.java
    @@ -5,6 +5,7 @@
     import com.networknt.schema.JsonSchemaException;
     import com.networknt.schema.JsonSchemaFactory;
     import com.networknt.schema.ValidationMessage;
    +import org.apache.commons.lang3.ObjectUtils;
     import org.apache.commons.lang3.StringUtils;
     import org.prebid.server.bidder.BidderCatalog;
     import org.prebid.server.json.EncodeException;
    @@ -83,9 +84,8 @@ public static BidderParamValidator create(
     
             final Map bidderRawSchemas = new LinkedHashMap<>();
     
    -        bidderCatalog.names()
    -                .forEach(bidderRequester -> bidderRawSchemas.put(bidderRequester,
    -                        createSchemaNode(schemaDirectory, bidderRequester, mapper)));
    +        bidderCatalog.names().forEach(bidder -> bidderRawSchemas.put(
    +                bidder, createSchemaNode(schemaDirectory, maybeResolveAlias(bidderCatalog, bidder), mapper)));
     
             return new BidderParamValidator(toBidderSchemas(bidderRawSchemas), toSchemas(bidderRawSchemas, mapper));
         }
    @@ -113,6 +113,10 @@ private static JsonSchema toBidderSchema(JsonNode schema, String bidder) {
             return result;
         }
     
    +    private static String maybeResolveAlias(BidderCatalog bidderCatalog, String bidder) {
    +        return ObjectUtils.defaultIfNull(bidderCatalog.bidderInfoByName(bidder).getAliasOf(), bidder);
    +    }
    +
         private static JsonNode createSchemaNode(String schemaDirectory, String bidder, JacksonMapper mapper) {
             final JsonNode result;
             final String path = schemaDirectory + FILE_SEP + bidder + JSON_FILE_EXT;
    diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java
    index ab4a1f3d8d7..321665f3727 100644
    --- a/src/main/java/org/prebid/server/validation/RequestValidator.java
    +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java
    @@ -36,23 +36,31 @@
     import org.apache.commons.lang3.StringUtils;
     import org.prebid.server.bidder.BidderCatalog;
     import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.proto.openrtb.ext.request.BidAdjustmentMediaType;
     import org.prebid.server.proto.openrtb.ext.request.ExtDevice;
     import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt;
     import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid;
     import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange;
    +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
     import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity;
     import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity;
     import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidadjustmentfactors;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
     import org.prebid.server.proto.openrtb.ext.request.ExtSite;
    +import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse;
    +import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse;
     import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEid;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid;
     import org.prebid.server.proto.openrtb.ext.response.BidType;
    +import org.prebid.server.util.StreamUtil;
     import org.prebid.server.validation.model.ValidationResult;
     
     import java.io.IOException;
    @@ -67,6 +75,7 @@
     import java.util.Map;
     import java.util.Objects;
     import java.util.Set;
    +import java.util.stream.Collectors;
     import java.util.stream.Stream;
     
     /**
    @@ -76,8 +85,11 @@
     public class RequestValidator {
     
         private static final String PREBID_EXT = "prebid";
    -    private static final String CONTEXT_EXT = "context";
    +    private static final String BIDDER_EXT = "bidder";
    +    private static final String ASTERISK = "*";
         private static final Locale LOCALE = Locale.US;
    +    private static final Integer NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND = 500;
    +
         private static final String DOCUMENTATION = "https://iabtechlab.com/wp-content/uploads/2016/07/"
                 + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf";
     
    @@ -127,9 +139,9 @@ public ValidationResult validate(BidRequest bidRequest) {
                     }
                     aliases = ObjectUtils.defaultIfNull(extRequestPrebid.getAliases(), Collections.emptyMap());
                     validateAliases(aliases);
    -                validateBidAdjustmentFactors(
    -                        ObjectUtils.defaultIfNull(extRequestPrebid.getBidadjustmentfactors(), Collections.emptyMap()),
    -                        aliases);
    +                validateBidAdjustmentFactors(extRequestPrebid.getBidadjustmentfactors(), aliases);
    +                validateExtBidPrebidData(extRequestPrebid.getData(), aliases);
    +                validateSchains(extRequestPrebid.getSchains());
                 }
     
                 if (CollectionUtils.isEmpty(bidRequest.getImp())) {
    @@ -158,10 +170,8 @@ public ValidationResult validate(BidRequest bidRequest) {
                     validateImp(bidRequest.getImp().get(index), aliases, index);
                 }
     
    -            if ((bidRequest.getSite() == null && bidRequest.getApp() == null)
    -                    || (bidRequest.getSite() != null && bidRequest.getApp() != null)) {
    -
    -                throw new ValidationException("request.site or request.app must be defined, but not both");
    +            if (bidRequest.getSite() == null && bidRequest.getApp() == null) {
    +                throw new ValidationException("request.site or request.app must be defined");
                 }
                 validateSite(bidRequest.getSite());
                 validateDevice(bidRequest.getDevice());
    @@ -183,10 +193,14 @@ private void validateCur(List currencies) throws ValidationException {
             }
         }
     
    -    private void validateBidAdjustmentFactors(Map adjustmentFactors, Map aliases)
    -            throws ValidationException {
    +    private void validateBidAdjustmentFactors(ExtRequestBidadjustmentfactors adjustmentFactors,
    +                                              Map aliases) throws ValidationException {
     
    -        for (Map.Entry bidderAdjustment : adjustmentFactors.entrySet()) {
    +        final Map bidderAdjustments = adjustmentFactors != null
    +                ? adjustmentFactors.getAdjustments()
    +                : Collections.emptyMap();
    +
    +        for (Map.Entry bidderAdjustment : bidderAdjustments.entrySet()) {
                 final String bidder = bidderAdjustment.getKey();
     
                 if (isUnknownBidderOrAlias(bidder, aliases)) {
    @@ -201,6 +215,132 @@ private void validateBidAdjustmentFactors(Map adjustmentFact
                             bidder, format(adjustmentFactor));
                 }
             }
    +        final Map> adjustmentsMediaTypeFactors =
    +                adjustmentFactors != null
    +                        ? adjustmentFactors.getMediatypes()
    +                        : null;
    +
    +        if (adjustmentsMediaTypeFactors == null) {
    +            return;
    +        }
    +
    +        for (Map.Entry> entry
    +                : adjustmentsMediaTypeFactors.entrySet()) {
    +            validateBidAdjustmentFactorsByMediatype(entry.getKey(), entry.getValue(), aliases);
    +        }
    +    }
    +
    +    private void validateBidAdjustmentFactorsByMediatype(BidAdjustmentMediaType mediaType,
    +                                                         Map bidderAdjustments,
    +                                                         Map aliases) throws ValidationException {
    +
    +        for (Map.Entry bidderAdjustment : bidderAdjustments.entrySet()) {
    +            final String bidder = bidderAdjustment.getKey();
    +
    +            if (isUnknownBidderOrAlias(bidder, aliases)) {
    +                throw new ValidationException(
    +                        "request.ext.prebid.bidadjustmentfactors.%s.%s is not a known bidder or alias",
    +                        mediaType, bidder);
    +            }
    +
    +            final BigDecimal adjustmentFactor = bidderAdjustment.getValue();
    +            if (adjustmentFactor.compareTo(BigDecimal.ZERO) <= 0) {
    +                throw new ValidationException(
    +                        "request.ext.prebid.bidadjustmentfactors.%s.%s must be a positive number. Got %s",
    +                        mediaType, bidder, format(adjustmentFactor));
    +            }
    +        }
    +    }
    +
    +    private void validateSchains(List schains) throws ValidationException {
    +        if (schains == null) {
    +            return;
    +        }
    +
    +        final Set schainBidders = new HashSet<>();
    +        for (final ExtRequestPrebidSchain schain : schains) {
    +            if (schain == null) {
    +                continue;
    +            }
    +
    +            final List bidders = schain.getBidders();
    +            if (bidders == null) {
    +                continue;
    +            }
    +
    +            for (final String bidder : bidders) {
    +                if (schainBidders.contains(bidder)) {
    +                    throw new ValidationException(
    +                            "request.ext.prebid.schains contains multiple schains for bidder %s; "
    +                                    + "it must contain no more than one per bidder.",
    +                            bidder);
    +                }
    +
    +                schainBidders.add(bidder);
    +            }
    +        }
    +    }
    +
    +    private void validateExtBidPrebidData(ExtRequestPrebidData data, Map aliases)
    +            throws ValidationException {
    +        if (data != null) {
    +            validateEidPermissions(data.getEidPermissions(), aliases);
    +        }
    +    }
    +
    +    private void validateEidPermissions(List eidPermissions,
    +                                        Map aliases) throws ValidationException {
    +        if (eidPermissions != null) {
    +            final Set uniqueEidsSources = new HashSet<>();
    +            for (ExtRequestPrebidDataEidPermissions eidPermission : eidPermissions) {
    +                validateEidPermission(eidPermission, aliases, uniqueEidsSources);
    +            }
    +        }
    +    }
    +
    +    private void validateEidPermission(ExtRequestPrebidDataEidPermissions eidPermission,
    +                                       Map aliases,
    +                                       Set uniqueEidsSources)
    +            throws ValidationException {
    +        if (eidPermission == null) {
    +            throw new ValidationException("request.ext.prebid.data.eidpermissions[] can't be null");
    +        }
    +        final String eidPermissionSource = eidPermission.getSource();
    +
    +        validateEidPermissionSource(eidPermissionSource);
    +        validateDuplicatedSources(uniqueEidsSources, eidPermissionSource);
    +        validateEidPermissionBidders(eidPermission.getBidders(), aliases);
    +    }
    +
    +    private void validateEidPermissionSource(String source) throws ValidationException {
    +        if (StringUtils.isEmpty(source)) {
    +            throw new ValidationException("Missing required value request.ext.prebid.data.eidPermissions[].source");
    +        }
    +    }
    +
    +    private void validateDuplicatedSources(Set uniqueEidsSources, String eidSource) throws ValidationException {
    +        if (uniqueEidsSources.contains(eidSource)) {
    +            throw new ValidationException(String.format(
    +                    "Duplicate source %s in request.ext.prebid.data.eidpermissions[]", eidSource));
    +        }
    +        uniqueEidsSources.add(eidSource);
    +    }
    +
    +    private void validateEidPermissionBidders(List bidders,
    +                                              Map aliases) throws ValidationException {
    +
    +        if (CollectionUtils.isEmpty(bidders)) {
    +            throw new ValidationException("request.ext.prebid.data.eidpermissions[].bidders[] required values"
    +                    + " but was empty or null");
    +        }
    +
    +        for (String bidder : bidders) {
    +            if (!bidderCatalog.isValidName(bidder) && !bidderCatalog.isValidName(aliases.get(bidder))
    +                    && ObjectUtils.notEqual(bidder, ASTERISK)) {
    +                throw new ValidationException(
    +                        "request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: '%s'", bidder);
    +            }
    +        }
         }
     
         private boolean isUnknownBidderOrAlias(String bidder, Map aliases) {
    @@ -286,27 +426,37 @@ private static void validateGranularityRanges(List ranges)
                 throw new ValidationException("Price granularity error: empty granularity definition supplied");
             }
     
    -        final Iterator rangeIterator = ranges.iterator();
    -        ExtGranularityRange range = rangeIterator.next();
    -        validateGranularityRangeIncrement(range);
    +        BigDecimal previousRangeMax = null;
    +        for (ExtGranularityRange range : ranges) {
    +            final BigDecimal rangeMax = range.getMax();
     
    -        while (rangeIterator.hasNext()) {
    -            final ExtGranularityRange nextGranularityRange = rangeIterator.next();
    -            if (range.getMax().compareTo(nextGranularityRange.getMax()) > 0) {
    -                throw new ValidationException(
    -                        "Price granularity error: range list must be ordered with increasing \"max\"");
    -            }
    -            validateGranularityRangeIncrement(nextGranularityRange);
    -            range = nextGranularityRange;
    +            validateGranularityRangeMax(rangeMax);
    +            validateGranularityRangeIncrement(range);
    +            validateGranularityRangeMaxOrdering(previousRangeMax, rangeMax);
    +
    +            previousRangeMax = rangeMax;
    +        }
    +    }
    +
    +    private static void validateGranularityRangeMax(BigDecimal rangeMax)
    +            throws ValidationException {
    +        if (rangeMax == null) {
    +            throw new ValidationException("Price granularity error: max value should not be missed");
    +        }
    +    }
    +
    +    private static void validateGranularityRangeMaxOrdering(BigDecimal previousRangeMax, BigDecimal rangeMax)
    +            throws ValidationException {
    +        if (previousRangeMax != null && previousRangeMax.compareTo(rangeMax) > 0) {
    +            throw new ValidationException(
    +                    "Price granularity error: range list must be ordered with increasing \"max\"");
             }
         }
     
    -    /**
    -     * Validates {@link ExtGranularityRange}s increment.
    -     */
         private static void validateGranularityRangeIncrement(ExtGranularityRange range)
                 throws ValidationException {
    -        if (range.getIncrement().compareTo(BigDecimal.ZERO) <= 0) {
    +        final BigDecimal increment = range.getIncrement();
    +        if (increment == null || increment.compareTo(BigDecimal.ZERO) <= 0) {
                 throw new ValidationException("Price granularity error: increment must be a nonzero positive number");
             }
         }
    @@ -323,6 +473,10 @@ private void validateAliases(Map aliases) throws ValidationExcep
                     throw new ValidationException(String.format(
                             "request.ext.prebid.aliases.%s refers to unknown bidder: %s", alias, coreBidder));
                 }
    +            if (!bidderCatalog.isActive(coreBidder)) {
    +                throw new ValidationException(String.format(
    +                        "request.ext.prebid.aliases.%s refers to disabled bidder: %s", alias, coreBidder));
    +            }
                 if (alias.equals(coreBidder)) {
                     throw new ValidationException(String.format("request.ext.prebid.aliases.%s defines a no-op alias. "
                             + "Choose a different alias, or remove this entry", alias));
    @@ -392,18 +546,12 @@ private void validateUser(User user, Map aliases) throws Validat
                     }
                 }
     
    -            final ExtUserDigiTrust digitrust = extUser.getDigitrust();
    -            if (digitrust != null && digitrust.getPref() != null && digitrust.getPref() != 0) {
    -                throw new ValidationException("request.user contains a digitrust object that is not valid");
    -            }
    -
                 final List eids = extUser.getEids();
                 if (eids != null) {
                     if (eids.isEmpty()) {
                         throw new ValidationException(
                                 "request.user.ext.eids must contain at least one element or be undefined");
                     }
    -                final Set uniqueSources = new HashSet<>(eids.size());
                     for (int index = 0; index < eids.size(); index++) {
                         final ExtUserEid eid = eids.get(index);
                         if (StringUtils.isBlank(eid.getSource())) {
    @@ -431,9 +579,10 @@ private void validateUser(User user, Map aliases) throws Validat
                                 }
                             }
                         }
    -                    uniqueSources.add(eid.getSource());
                     }
    -
    +                final Set uniqueSources = eids.stream()
    +                        .map(ExtUserEid::getSource)
    +                        .collect(Collectors.toSet());
                     if (eids.size() != uniqueSources.size()) {
                         throw new ValidationException("request.user.ext.eids must contain unique sources");
                     }
    @@ -492,12 +641,12 @@ private void fillAndValidateNative(Native xNative, int impIndex) throws Validati
     
         private Request parseNativeRequest(String rawStringNativeRequest, int impIndex) throws ValidationException {
             if (StringUtils.isBlank(rawStringNativeRequest)) {
    -            throw new ValidationException("request.imp.[%d].ext.native contains empty request value", impIndex);
    +            throw new ValidationException("request.imp[%d].native contains empty request value", impIndex);
             }
             try {
                 return mapper.mapper().readValue(rawStringNativeRequest, Request.class);
             } catch (IOException e) {
    -            throw new ValidationException("Error while parsing request.imp.[%d].ext.native.request", impIndex);
    +            throw new ValidationException("Error while parsing request.imp[%d].native.request", impIndex);
             }
         }
     
    @@ -509,7 +658,8 @@ private void validateNativeContextTypes(Integer context, Integer contextSubType,
                 return;
             }
     
    -        if (type < ContextType.CONTENT.getValue() || type > ContextType.PRODUCT.getValue()) {
    +        if (type < ContextType.CONTENT.getValue()
    +                || (type > ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) {
                 throw new ValidationException(
                         "request.imp[%d].native.request.context is invalid. See " + documentationOnPage(39), index);
             }
    @@ -524,30 +674,36 @@ private void validateNativeContextTypes(Integer context, Integer contextSubType,
                 return;
             }
     
    -        if (subType >= 100) {
    -            throw new ValidationException(
    -                    "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index);
    +        if (subType >= ContextSubType.GENERAL.getValue() && subType <= ContextSubType.USER_GENERATED.getValue()) {
    +            if (type != ContextType.CONTENT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) {
    +                throw new ValidationException(
    +                        "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid "
    +                                + "combination. See " + documentationOnPage(39), index, context, contextSubType);
    +            }
    +            return;
             }
     
    -        if (subType >= ContextSubType.GENERAL.getValue() && subType <= ContextSubType.USER_GENERATED.getValue()
    -                && type != ContextType.CONTENT.getValue()) {
    -            throw new ValidationException(
    -                    "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid "
    -                            + "combination. See " + documentationOnPage(39), index, context, contextSubType);
    +        if (subType >= ContextSubType.SOCIAL.getValue() && subType <= ContextSubType.CHAT.getValue()) {
    +            if (type != ContextType.SOCIAL.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) {
    +                throw new ValidationException(
    +                        "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid "
    +                                + "combination. See " + documentationOnPage(39), index, context, contextSubType);
    +            }
    +            return;
             }
     
    -        if (subType >= ContextSubType.SOCIAL.getValue() && subType <= ContextSubType.CHAT.getValue()
    -                && type != ContextType.SOCIAL.getValue()) {
    -            throw new ValidationException(
    -                    "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid "
    -                            + "combination. See " + documentationOnPage(39), index, context, contextSubType);
    +        if (subType >= ContextSubType.SELLING.getValue() && subType <= ContextSubType.PRODUCT_REVIEW.getValue()) {
    +            if (type != ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) {
    +                throw new ValidationException(
    +                        "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid "
    +                                + "combination. See " + documentationOnPage(39), index, context, contextSubType);
    +            }
    +            return;
             }
     
    -        if (subType >= ContextSubType.SELLING.getValue() && subType <= ContextSubType.PRODUCT_REVIEW.getValue()
    -                && type != ContextType.PRODUCT.getValue()) {
    +        if (subType < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) {
                 throw new ValidationException(
    -                    "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid "
    -                            + "combination. See " + documentationOnPage(39), index, context, contextSubType);
    +                    "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index);
             }
         }
     
    @@ -557,7 +713,8 @@ private void validateNativePlacementType(Integer placementType, int index) throw
                 return;
             }
     
    -        if (type < PlacementType.FEED.getValue() || type > PlacementType.RECOMMENDATION_WIDGET.getValue()) {
    +        if (type < PlacementType.FEED.getValue() || (type > PlacementType.RECOMMENDATION_WIDGET.getValue()
    +                && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) {
                 throw new ValidationException(
                         "request.imp[%d].native.request.plcmttype is invalid. See " + documentationOnPage(40), index, type);
             }
    @@ -624,10 +781,11 @@ private void validateNativeAssetData(DataObject data, int impIndex, int assetInd
             }
     
             final Integer type = data.getType();
    -        if (type < DataAssetType.SPONSORED.getValue() || type > DataAssetType.CTA_TEXT.getValue()) {
    +        if (type < DataAssetType.SPONSORED.getValue()
    +                || (type > DataAssetType.CTA_TEXT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) {
                 throw new ValidationException(
    -                    "request.imp[%d].native.request.assets[%d].data.type must in the range [1, 12]. Got %d",
    -                    impIndex, assetIndex, type);
    +                    "request.imp[%d].native.request.assets[%d].data.type is invalid. See section 7.4: "
    +                            + documentationOnPage(40), impIndex, assetIndex);
             }
         }
     
    @@ -693,8 +851,8 @@ private void validateNativeEventTracker(EventTracker eventTracker, int impIndex,
             if (eventTracker != null) {
                 final int event = eventTracker.getEvent() != null ? eventTracker.getEvent() : 0;
     
    -            if (event != 0 && (event < EventType.IMPRESSION.getValue()
    -                    || event > EventType.VIEWABLE_VIDEO50.getValue())) {
    +            if (event != 0 && (event < EventType.IMPRESSION.getValue() || (event > EventType.VIEWABLE_VIDEO50.getValue()
    +                    && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND))) {
                     throw new ValidationException(
                             "request.imp[%d].native.request.eventtrackers[%d].event is invalid. See section 7.6: "
                                     + documentationOnPage(43), impIndex, eventIndex
    @@ -712,8 +870,8 @@ private void validateNativeEventTracker(EventTracker eventTracker, int impIndex,
     
                 for (int methodIndex = 0; methodIndex < methods.size(); methodIndex++) {
                     int method = methods.get(methodIndex) != null ? methods.get(methodIndex) : 0;
    -                if (method < EventTrackingMethod.IMAGE.getValue()
    -                        || method > EventTrackingMethod.JS.getValue()) {
    +                if (method < EventTrackingMethod.IMAGE.getValue() || (method > EventTrackingMethod.JS.getValue()
    +                        && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) {
                         throw new ValidationException(
                                 "request.imp[%d].native.request.eventtrackers[%d].methods[%d] is invalid. See section 7.7: "
                                         + documentationOnPage(43), impIndex, eventIndex, methodIndex
    @@ -736,31 +894,129 @@ private String toEncodedRequest(Request nativeRequest, List updatedAssets
         }
     
         private void validateImpExt(ObjectNode ext, Map aliases, int impIndex) throws ValidationException {
    -        if (ext == null || ext.size() < 1) {
    -            throw new ValidationException("request.imp[%d].ext must contain at least one bidder", impIndex);
    +        validateImpExtPrebid(ext != null ? ext.get(PREBID_EXT) : null, aliases, impIndex);
    +    }
    +
    +    private void validateImpExtPrebid(JsonNode extPrebidNode, Map aliases, int impIndex)
    +            throws ValidationException {
    +
    +        if (extPrebidNode == null) {
    +            throw new ValidationException(
    +                    "request.imp[%d].ext.prebid must be defined", impIndex);
             }
     
    -        final Iterator> bidderExtensions = ext.fields();
    +        if (!extPrebidNode.isObject()) {
    +            throw new ValidationException(
    +                    "request.imp[%d].ext.prebid must an object type", impIndex);
    +        }
    +
    +        final JsonNode extPrebidBidderNode = extPrebidNode.get(BIDDER_EXT);
    +
    +        if (extPrebidBidderNode != null && !extPrebidBidderNode.isObject()) {
    +            throw new ValidationException(
    +                    "request.imp[%d].ext.prebid.bidder must be an object type", impIndex);
    +        }
    +
    +        final ExtImpPrebid extPrebid = parseExtImpPrebid((ObjectNode) extPrebidNode, impIndex);
    +
    +        validateImpExtPrebidBidder(extPrebid, aliases, impIndex);
    +        validateImpExtPrebidStoredResponses(extPrebid, aliases, impIndex);
    +    }
    +
    +    private void validateImpExtPrebidBidder(ExtImpPrebid extPrebid, Map aliases, int impIndex)
    +            throws ValidationException {
    +
    +        final ObjectNode extPrebidBidder = extPrebid.getBidder();
    +
    +        if (extPrebidBidder == null) {
    +            if (extPrebid.getStoredAuctionResponse() != null) {
    +                return;
    +            } else {
    +                throw new ValidationException("request.imp[%d].ext.prebid.bidder must be defined", impIndex);
    +            }
    +        }
    +
    +        final Iterator> bidderExtensions = extPrebidBidder.fields();
             while (bidderExtensions.hasNext()) {
                 final Map.Entry bidderExtension = bidderExtensions.next();
                 final String bidder = bidderExtension.getKey();
    -            if (!Objects.equals(bidder, PREBID_EXT) && !Objects.equals(bidder, CONTEXT_EXT)) {
    -                validateImpBidderExtName(impIndex, bidderExtension, aliases.getOrDefault(bidder, bidder));
    +            validateImpBidderExtName(impIndex, bidderExtension, aliases.getOrDefault(bidder, bidder));
    +        }
    +    }
    +
    +    private void validateImpExtPrebidStoredResponses(ExtImpPrebid extPrebid,
    +                                                     Map aliases,
    +                                                     int impIndex) throws ValidationException {
    +        final ExtStoredAuctionResponse extStoredAuctionResponse = extPrebid.getStoredAuctionResponse();
    +        if (extStoredAuctionResponse != null && extStoredAuctionResponse.getId() == null) {
    +            throw new ValidationException("request.imp[%d].ext.prebid.storedauctionresponse.id should be defined",
    +                    impIndex);
    +        }
    +
    +        final List storedBidResponses = extPrebid.getStoredBidResponse();
    +        if (CollectionUtils.isNotEmpty(storedBidResponses)) {
    +            final ObjectNode bidderNode = extPrebid.getBidder();
    +            if (bidderNode == null || bidderNode.isEmpty()) {
    +                throw new ValidationException(String.format(
    +                        "request.imp[%d].ext.prebid.bidder should be defined for storedbidresponse", impIndex));
    +            }
    +
    +            for (ExtStoredBidResponse storedBidResponse : storedBidResponses) {
    +                validateStoredBidResponse(storedBidResponse, bidderNode, aliases, impIndex);
                 }
             }
         }
     
    +    private void validateStoredBidResponse(ExtStoredBidResponse extStoredBidResponse, ObjectNode bidderNode,
    +                                           Map aliases, int impIndex) throws ValidationException {
    +        final String bidder = extStoredBidResponse.getBidder();
    +        final String id = extStoredBidResponse.getId();
    +        if (StringUtils.isEmpty(bidder)) {
    +            throw new ValidationException(String.format(
    +                    "request.imp[%d].ext.prebid.storedbidresponse.bidder was not defined", impIndex));
    +        }
    +
    +        if (StringUtils.isEmpty(id)) {
    +            throw new ValidationException(String.format(
    +                    "Id was not defined for request.imp[%d].ext.prebid.storedbidresponse.id", impIndex));
    +        }
    +
    +        final String resolvedBidder = aliases.getOrDefault(bidder, bidder);
    +
    +        if (!bidderCatalog.isValidName(resolvedBidder)) {
    +            throw new ValidationException(String.format(
    +                    "request.imp[%d].ext.prebid.storedbidresponse.bidder is not valid bidder", impIndex));
    +        }
    +
    +        final boolean noCorrespondentBidderParameters = StreamUtil.asStream(bidderNode.fieldNames())
    +                .noneMatch(impBidder -> impBidder.equals(resolvedBidder) || impBidder.equals(bidder));
    +        if (noCorrespondentBidderParameters) {
    +            throw new ValidationException(String.format(
    +                    "request.imp[%d].ext.prebid.storedbidresponse.bidder does not have correspondent bidder parameters",
    +                    impIndex));
    +        }
    +    }
    +
    +    private ExtImpPrebid parseExtImpPrebid(ObjectNode extImpPrebid, int impIndex) throws ValidationException {
    +        try {
    +            return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class);
    +        } catch (JsonProcessingException e) {
    +            throw new ValidationException(String.format(
    +                    " bidRequest.imp[%d].ext.prebid: %s has invalid format", impIndex, e.getMessage()));
    +        }
    +    }
    +
         private void validateImpBidderExtName(int impIndex, Map.Entry bidderExtension, String bidderName)
                 throws ValidationException {
             if (bidderCatalog.isValidName(bidderName)) {
                 final Set messages = bidderParamValidator.validate(bidderName, bidderExtension.getValue());
                 if (!messages.isEmpty()) {
    -                throw new ValidationException("request.imp[%d].ext.%s failed validation.\n%s", impIndex,
    +                throw new ValidationException("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%s", impIndex,
                             bidderName, String.join("\n", messages));
                 }
    -        } else if (!bidderCatalog.isDeprecatedName(bidderName) && !bidderCatalog.isAlias(bidderName)) {
    +        } else if (!bidderCatalog.isDeprecatedName(bidderName)) {
                 throw new ValidationException(
    -                    "request.imp[%d].ext contains unknown bidder: %s", impIndex, bidderName);
    +                    "request.imp[%d].ext.prebid.bidder contains unknown bidder: %s", impIndex, bidderName);
             }
         }
     
    @@ -867,6 +1123,10 @@ private void validateMetrics(List metrics, int impIndex) throws Validati
             }
         }
     
    +    private static boolean isObjectNode(JsonNode node) {
    +        return node != null && node.isObject();
    +    }
    +
         private static boolean hasPositiveValue(Integer value) {
             return value != null && value > 0;
         }
    diff --git a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java
    index a35ab15982c..6f500ca22fa 100644
    --- a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java
    +++ b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java
    @@ -1,28 +1,126 @@
     package org.prebid.server.validation;
     
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.JsonNode;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Deal;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Pmp;
    +import com.iab.openrtb.request.Site;
     import com.iab.openrtb.response.Bid;
    +import io.vertx.core.logging.Logger;
    +import io.vertx.core.logging.LoggerFactory;
    +import org.apache.commons.collections4.CollectionUtils;
    +import org.apache.commons.collections4.ListUtils;
    +import org.apache.commons.lang3.ObjectUtils;
     import org.apache.commons.lang3.StringUtils;
    +import org.prebid.server.auction.BidderAliases;
    +import org.prebid.server.auction.model.AuctionContext;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.json.JacksonMapper;
    +import org.prebid.server.log.ConditionalLogger;
    +import org.prebid.server.metric.MetricName;
    +import org.prebid.server.metric.Metrics;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDeal;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine;
    +import org.prebid.server.proto.openrtb.ext.response.BidType;
    +import org.prebid.server.settings.model.Account;
    +import org.prebid.server.settings.model.AccountAuctionConfig;
    +import org.prebid.server.settings.model.AccountBidValidationConfig;
    +import org.prebid.server.settings.model.BidValidationEnforcement;
    +import org.prebid.server.util.DealUtil;
     import org.prebid.server.validation.model.ValidationResult;
     
     import java.math.BigDecimal;
    +import java.util.ArrayList;
    +import java.util.Collection;
    +import java.util.Collections;
    +import java.util.Currency;
    +import java.util.List;
    +import java.util.Objects;
    +import java.util.Set;
    +import java.util.function.Consumer;
    +import java.util.stream.Collectors;
    +import java.util.stream.Stream;
     
     /**
      * Validator for response {@link Bid} object.
      */
     public class ResponseBidValidator {
     
    -    public ValidationResult validate(Bid bid) {
    +    private static final Logger logger = LoggerFactory.getLogger(ResponseBidValidator.class);
    +    private static final ConditionalLogger UNRELATED_BID_LOGGER = new ConditionalLogger("not_matched_bid", logger);
    +    private static final ConditionalLogger SECURE_CREATIVE_LOGGER = new ConditionalLogger("secure_creatives_validation",
    +            logger);
    +    private static final ConditionalLogger CREATIVE_SIZE_LOGGER = new ConditionalLogger("creative_size_validation",
    +            logger);
    +    private static final double LOG_SAMPLING_RATE = 0.01;
    +
    +    private static final String[] INSECURE_MARKUP_MARKERS = {"http:", "http%3A"};
    +    private static final String[] SECURE_MARKUP_MARKERS = {"https:", "https%3A"};
    +
    +    private static final String PREBID_EXT = "prebid";
    +    private static final String BIDDER_EXT = "bidder";
    +
    +    private static final String DEALS_ONLY = "dealsonly";
    +
    +    private final BidValidationEnforcement bannerMaxSizeEnforcement;
    +    private final BidValidationEnforcement secureMarkupEnforcement;
    +    private final Metrics metrics;
    +
    +    private final JacksonMapper mapper;
    +    private final boolean dealsEnabled;
    +
    +    public ResponseBidValidator(BidValidationEnforcement bannerMaxSizeEnforcement,
    +                                BidValidationEnforcement secureMarkupEnforcement,
    +                                Metrics metrics,
    +                                JacksonMapper mapper,
    +                                boolean dealsEnabled) {
    +
    +        this.bannerMaxSizeEnforcement = Objects.requireNonNull(bannerMaxSizeEnforcement);
    +        this.secureMarkupEnforcement = Objects.requireNonNull(secureMarkupEnforcement);
    +        this.metrics = Objects.requireNonNull(metrics);
    +
    +        this.mapper = Objects.requireNonNull(mapper);
    +        this.dealsEnabled = dealsEnabled;
    +    }
    +
    +    public ValidationResult validate(BidderBid bidderBid,
    +                                     String bidder,
    +                                     AuctionContext auctionContext,
    +                                     BidderAliases aliases) {
    +
    +        final Bid bid = bidderBid.getBid();
    +        final BidRequest bidRequest = auctionContext.getBidRequest();
    +        final Account account = auctionContext.getAccount();
    +        final List warnings = new ArrayList<>();
    +
             try {
    -            validateFieldsFor(bid);
    +            validateCommonFields(bid);
    +            validateTypeSpecific(bidderBid, bidder);
    +            validateCurrency(bidderBid.getBidCurrency());
    +
    +            final Imp correspondingImp = findCorrespondingImp(bid, bidRequest);
    +            if (bidderBid.getType() == BidType.banner) {
    +                warnings.addAll(validateBannerFields(bid, bidder, bidRequest, account, correspondingImp, aliases));
    +            }
    +
    +            if (dealsEnabled) {
    +                validateDealsFor(bidderBid, auctionContext.getBidRequest(), bidder, aliases, warnings);
    +            }
    +
    +            warnings.addAll(validateSecureMarkup(bid, bidder, bidRequest, account, correspondingImp, aliases));
             } catch (ValidationException e) {
    -            return ValidationResult.error(e.getMessage());
    +            return ValidationResult.error(warnings, e.getMessage());
             }
    -        return ValidationResult.success();
    +        return ValidationResult.success(warnings);
         }
     
    -    private static void validateFieldsFor(Bid bid) throws ValidationException {
    +    private static void validateCommonFields(Bid bid) throws ValidationException {
             if (bid == null) {
    -            throw new ValidationException("Empty bid object submitted.");
    +            throw new ValidationException("Empty bid object submitted");
             }
     
             final String bidId = bid.getId();
    @@ -51,4 +149,296 @@ private static void validateFieldsFor(Bid bid) throws ValidationException {
                 throw new ValidationException("Bid \"%s\" missing creative ID", bidId);
             }
         }
    +
    +    private void validateTypeSpecific(BidderBid bidderBid, String bidder) throws ValidationException {
    +        final Bid bid = bidderBid.getBid();
    +        final boolean isVastSpecificAbsent = bid.getAdm() == null && bid.getNurl() == null;
    +
    +        if (Objects.equals(bidderBid.getType(), BidType.video) && isVastSpecificAbsent) {
    +            metrics.updateAdapterRequestErrorMetric(bidder, MetricName.badserverresponse);
    +            throw new ValidationException("Bid \"%s\" with video type missing adm and nurl", bid.getId());
    +        }
    +    }
    +
    +    private static void validateCurrency(String currency) throws ValidationException {
    +        try {
    +            if (StringUtils.isNotBlank(currency)) {
    +                Currency.getInstance(currency);
    +            }
    +        } catch (IllegalArgumentException e) {
    +            throw new ValidationException("BidResponse currency \"%s\" is not valid", currency);
    +        }
    +    }
    +
    +    private static Imp findCorrespondingImp(Bid bid, BidRequest bidRequest) throws ValidationException {
    +        return bidRequest.getImp().stream()
    +                .filter(imp -> Objects.equals(imp.getId(), bid.getImpid()))
    +                .findFirst()
    +                .orElseThrow(() -> exceptionAndLogOnePercent(
    +                        String.format("Bid \"%s\" has no corresponding imp in request", bid.getId())));
    +    }
    +
    +    private static ValidationException exceptionAndLogOnePercent(String message) {
    +        UNRELATED_BID_LOGGER.warn(message, 0.01);
    +        return new ValidationException(message);
    +    }
    +
    +    private List validateBannerFields(Bid bid,
    +                                              String bidder,
    +                                              BidRequest bidRequest,
    +                                              Account account,
    +                                              Imp correspondingImp,
    +                                              BidderAliases aliases) throws ValidationException {
    +
    +        final BidValidationEnforcement bannerMaxSizeEnforcement = effectiveBannerMaxSizeEnforcement(account);
    +        if (bannerMaxSizeEnforcement != BidValidationEnforcement.skip) {
    +            final Format maxSize = maxSizeForBanner(correspondingImp);
    +
    +            if (bannerSizeIsNotValid(bid, maxSize)) {
    +                final String accountId = account.getId();
    +                final String message = String.format(
    +                        "BidResponse validation `%s`: bidder `%s` response triggers creative size validation for bid "
    +                                + "%s, account=%s, referrer=%s, max imp size='%dx%d', bid response size='%dx%d'",
    +                        bannerMaxSizeEnforcement, bidder, bid.getId(), accountId, getReferer(bidRequest),
    +                        maxSize.getW(), maxSize.getH(), bid.getW(), bid.getH());
    +
    +                return singleWarningOrValidationException(
    +                        bannerMaxSizeEnforcement,
    +                        metricName -> metrics.updateSizeValidationMetrics(
    +                                aliases.resolveBidder(bidder), accountId, metricName),
    +                        CREATIVE_SIZE_LOGGER, message);
    +            }
    +        }
    +        return Collections.emptyList();
    +    }
    +
    +    private BidValidationEnforcement effectiveBannerMaxSizeEnforcement(Account account) {
    +        final AccountAuctionConfig accountAuctionConfig = account.getAuction();
    +        final AccountBidValidationConfig validationConfig =
    +                accountAuctionConfig != null ? accountAuctionConfig.getBidValidations() : null;
    +        final BidValidationEnforcement accountBannerMaxSizeEnforcement =
    +                validationConfig != null ? validationConfig.getBannerMaxSizeEnforcement() : null;
    +
    +        return ObjectUtils.defaultIfNull(accountBannerMaxSizeEnforcement, bannerMaxSizeEnforcement);
    +    }
    +
    +    private static Format maxSizeForBanner(Imp imp) {
    +        int maxW = 0;
    +        int maxH = 0;
    +        for (final Format size : bannerFormats(imp)) {
    +            maxW = Math.max(maxW, size.getW());
    +            maxH = Math.max(maxH, size.getH());
    +        }
    +        return Format.builder().w(maxW).h(maxH).build();
    +    }
    +
    +    private static List bannerFormats(Imp imp) {
    +        final Banner banner = imp.getBanner();
    +        final List formats = banner != null ? banner.getFormat() : null;
    +        return ListUtils.emptyIfNull(formats);
    +    }
    +
    +    private static boolean bannerSizeIsNotValid(Bid bid, Format maxSize) {
    +        final Integer bidW = bid.getW();
    +        final Integer bidH = bid.getH();
    +        return bidW == null || bidW > maxSize.getW()
    +                || bidH == null || bidH > maxSize.getH();
    +    }
    +
    +    private List validateSecureMarkup(Bid bid,
    +                                              String bidder,
    +                                              BidRequest bidRequest,
    +                                              Account account,
    +                                              Imp correspondingImp,
    +                                              BidderAliases aliases) throws ValidationException {
    +
    +        if (secureMarkupEnforcement == BidValidationEnforcement.skip) {
    +            return Collections.emptyList();
    +        }
    +
    +        final String accountId = account.getId();
    +        final String referer = getReferer(bidRequest);
    +        final String adm = bid.getAdm();
    +
    +        if (isImpSecure(correspondingImp) && markupIsNotSecure(adm)) {
    +            final String message = String.format("BidResponse validation `%s`: bidder `%s` response triggers secure"
    +                            + " creative validation for bid %s, account=%s, referrer=%s, adm=%s",
    +                    secureMarkupEnforcement, bidder, bid.getId(), accountId, referer, adm);
    +            return singleWarningOrValidationException(
    +                    secureMarkupEnforcement,
    +                    metricName -> metrics.updateSecureValidationMetrics(
    +                            aliases.resolveBidder(bidder), accountId, metricName),
    +                    SECURE_CREATIVE_LOGGER, message);
    +        }
    +
    +        return Collections.emptyList();
    +    }
    +
    +    private static boolean isImpSecure(Imp imp) {
    +        return Objects.equals(imp.getSecure(), 1);
    +    }
    +
    +    private static boolean markupIsNotSecure(String adm) {
    +        return StringUtils.containsAny(adm, INSECURE_MARKUP_MARKERS)
    +                || !StringUtils.containsAny(adm, SECURE_MARKUP_MARKERS);
    +    }
    +
    +    private static List singleWarningOrValidationException(BidValidationEnforcement enforcement,
    +                                                                   Consumer metricsRecorder,
    +                                                                   ConditionalLogger conditionalLogger,
    +                                                                   String message) throws ValidationException {
    +        switch (enforcement) {
    +            case enforce:
    +                metricsRecorder.accept(MetricName.err);
    +                conditionalLogger.warn(message, LOG_SAMPLING_RATE);
    +                throw new ValidationException(message);
    +            case warn:
    +                metricsRecorder.accept(MetricName.warn);
    +                conditionalLogger.warn(message, LOG_SAMPLING_RATE);
    +                return Collections.singletonList(message);
    +            default:
    +                throw new IllegalStateException(String.format("Unexpected enforcement: %s", enforcement));
    +        }
    +    }
    +
    +    private static String getReferer(BidRequest bidRequest) {
    +        final Site site = bidRequest.getSite();
    +        return site != null ? site.getPage() : "unknown";
    +    }
    +
    +    private void validateDealsFor(BidderBid bidderBid,
    +                                  BidRequest bidRequest,
    +                                  String bidder,
    +                                  BidderAliases aliases,
    +                                  List warnings) throws ValidationException {
    +
    +        final Bid bid = bidderBid.getBid();
    +        final String bidId = bid.getId();
    +
    +        final Imp imp = bidRequest.getImp().stream()
    +                .filter(curImp -> Objects.equals(curImp.getId(), bid.getImpid()))
    +                .findFirst()
    +                .orElseThrow(() -> new ValidationException("Bid \"%s\" has no corresponding imp in request", bidId));
    +
    +        final String dealId = bid.getDealid();
    +
    +        if (isDealsOnlyImp(imp, bidder) && dealId == null) {
    +            throw new ValidationException("Bid \"%s\" missing required field 'dealid'", bidId);
    +        }
    +
    +        if (dealId != null) {
    +            final Set dealIdsFromImp = getDealIdsFromImp(imp, bidder, aliases);
    +            if (CollectionUtils.isNotEmpty(dealIdsFromImp) && !dealIdsFromImp.contains(dealId)) {
    +                warnings.add(String.format("WARNING: Bid \"%s\" has 'dealid' not present in corresponding imp in"
    +                                + " request. 'dealid' in bid: '%s', deal Ids in imp: '%s'",
    +                        bidId, dealId, String.join(",", dealIdsFromImp)));
    +            }
    +            if (bidderBid.getType() == BidType.banner) {
    +                if (imp.getBanner() == null) {
    +                    throw new ValidationException("Bid \"%s\" has banner media type but corresponding imp in request "
    +                            + "is missing 'banner' object", bidId);
    +                }
    +
    +                final List bannerFormats = getBannerFormats(imp);
    +                if (bidSizeNotInFormats(bid, bannerFormats)) {
    +                    throw new ValidationException("Bid \"%s\" has 'w' and 'h' not supported by corresponding imp in "
    +                            + "request. Bid dimensions: '%dx%d', formats in imp: '%s'", bidId, bid.getW(), bid.getH(),
    +                            formatSizes(bannerFormats));
    +                }
    +
    +                if (isPgDeal(imp, dealId)) {
    +                    validateIsInLineItemSizes(bid, bidId, imp);
    +                }
    +            }
    +        }
    +    }
    +
    +    private void validateIsInLineItemSizes(Bid bid, String bidId, Imp imp) throws ValidationException {
    +        final List lineItemSizes = getLineItemSizes(imp);
    +        if (bidSizeNotInFormats(bid, lineItemSizes)) {
    +            throw new ValidationException("Bid \"%s\" has 'w' and 'h' not matched to Line Item. Bid "
    +                    + "dimensions: '%dx%d', Line Item sizes: '%s'", bidId, bid.getW(), bid.getH(),
    +                    formatSizes(lineItemSizes));
    +        }
    +    }
    +
    +    private static boolean isDealsOnlyImp(Imp imp, String bidder) {
    +        final JsonNode dealsOnlyNode = bidderParamsFromImp(imp).get(bidder).get(DEALS_ONLY);
    +        return dealsOnlyNode != null && dealsOnlyNode.isBoolean() && dealsOnlyNode.asBoolean();
    +    }
    +
    +    private static JsonNode bidderParamsFromImp(Imp imp) {
    +        return imp.getExt().get(PREBID_EXT).get(BIDDER_EXT);
    +    }
    +
    +    private Set getDealIdsFromImp(Imp imp, String bidder, BidderAliases aliases) {
    +        return getDeals(imp)
    +                .filter(Objects::nonNull)
    +                .filter(deal -> DealUtil.isBidderHasDeal(bidder, dealExt(deal.getExt()), aliases))
    +                .map(Deal::getId)
    +                .filter(Objects::nonNull)
    +                .collect(Collectors.toSet());
    +    }
    +
    +    private static Stream getDeals(Imp imp) {
    +        final Pmp pmp = imp.getPmp();
    +        return pmp != null ? pmp.getDeals().stream() : Stream.empty();
    +    }
    +
    +    private static boolean bidSizeNotInFormats(Bid bid, List formats) {
    +        return formats.stream()
    +                .noneMatch(format -> sizesEqual(bid, format));
    +    }
    +
    +    private static boolean sizesEqual(Bid bid, Format format) {
    +        return Objects.equals(format.getH(), bid.getH()) && Objects.equals(format.getW(), bid.getW());
    +    }
    +
    +    private static List getBannerFormats(Imp imp) {
    +        return ListUtils.emptyIfNull(imp.getBanner().getFormat());
    +    }
    +
    +    private List getLineItemSizes(Imp imp) {
    +        return getDeals(imp)
    +                .map(Deal::getExt)
    +                .filter(Objects::nonNull)
    +                .map(this::dealExt)
    +                .filter(Objects::nonNull)
    +                .map(ExtDeal::getLine)
    +                .filter(Objects::nonNull)
    +                .map(ExtDealLine::getSizes)
    +                .filter(Objects::nonNull)
    +                .flatMap(Collection::stream)
    +                .filter(Objects::nonNull)
    +                .collect(Collectors.toList());
    +    }
    +
    +    private boolean isPgDeal(Imp imp, String dealId) {
    +        return getDeals(imp)
    +                .filter(Objects::nonNull)
    +                .filter(deal -> Objects.equals(deal.getId(), dealId))
    +                .map(Deal::getExt)
    +                .filter(Objects::nonNull)
    +                .map(this::dealExt)
    +                .filter(Objects::nonNull)
    +                .map(ExtDeal::getLine)
    +                .filter(Objects::nonNull)
    +                .map(ExtDealLine::getLineItemId)
    +                .anyMatch(Objects::nonNull);
    +    }
    +
    +    private ExtDeal dealExt(JsonNode ext) {
    +        try {
    +            return mapper.mapper().treeToValue(ext, ExtDeal.class);
    +        } catch (JsonProcessingException e) {
    +            logger.warn("Error decoding deal.ext: {0}", e, e.getMessage());
    +            return null;
    +        }
    +    }
    +
    +    private static String formatSizes(List lineItemSizes) {
    +        return lineItemSizes.stream()
    +                .map(format -> String.format("%dx%d", format.getW(), format.getH()))
    +                .collect(Collectors.joining(","));
    +    }
     }
    diff --git a/src/main/java/org/prebid/server/validation/ValidationException.java b/src/main/java/org/prebid/server/validation/ValidationException.java
    index 6ecbbd7871f..628102a0e6a 100644
    --- a/src/main/java/org/prebid/server/validation/ValidationException.java
    +++ b/src/main/java/org/prebid/server/validation/ValidationException.java
    @@ -3,6 +3,10 @@
     @SuppressWarnings("serial")
     class ValidationException extends Exception {
     
    +    ValidationException(String errorMessageFormat) {
    +        super(errorMessageFormat);
    +    }
    +
         ValidationException(String errorMessageFormat, Object... args) {
             super(String.format(errorMessageFormat, args));
         }
    diff --git a/src/main/java/org/prebid/server/validation/VideoRequestValidator.java b/src/main/java/org/prebid/server/validation/VideoRequestValidator.java
    index 8bca4bbf02a..02e1e353bb6 100644
    --- a/src/main/java/org/prebid/server/validation/VideoRequestValidator.java
    +++ b/src/main/java/org/prebid/server/validation/VideoRequestValidator.java
    @@ -166,5 +166,4 @@ private static List validateEachPod(List pods) {
             }
             return podErrorsResult;
         }
    -
     }
    diff --git a/src/main/java/org/prebid/server/validation/model/ValidationResult.java b/src/main/java/org/prebid/server/validation/model/ValidationResult.java
    index af0f9c13f0d..19a2e45e158 100644
    --- a/src/main/java/org/prebid/server/validation/model/ValidationResult.java
    +++ b/src/main/java/org/prebid/server/validation/model/ValidationResult.java
    @@ -1,26 +1,42 @@
     package org.prebid.server.validation.model;
     
    -import lombok.AllArgsConstructor;
     import lombok.Value;
     
     import java.util.Collections;
     import java.util.List;
     
    -@AllArgsConstructor
     @Value
     public class ValidationResult {
     
    +    List warnings;
    +
         List errors;
     
    +    public boolean hasWarnings() {
    +        return !warnings.isEmpty();
    +    }
    +
         public boolean hasErrors() {
             return !errors.isEmpty();
         }
     
    -    public static ValidationResult error(String errorMessageFormat, Object... args) {
    -        return new ValidationResult(Collections.singletonList(String.format(errorMessageFormat, args)));
    +    public static ValidationResult error(String errorMessageFormat) {
    +        return error(Collections.emptyList(), errorMessageFormat);
    +    }
    +
    +    public static ValidationResult error(List warnings, String errorMessageFormat) {
    +        return new ValidationResult(warnings, Collections.singletonList(errorMessageFormat));
         }
     
         public static ValidationResult success() {
    -        return new ValidationResult(Collections.emptyList());
    +        return success(Collections.emptyList());
    +    }
    +
    +    public static ValidationResult success(List warnings) {
    +        return new ValidationResult(warnings, Collections.emptyList());
    +    }
    +
    +    public static ValidationResult warning(List warnings) {
    +        return new ValidationResult(warnings, Collections.emptyList());
         }
     }
    diff --git a/src/main/java/org/prebid/server/vast/VastModifier.java b/src/main/java/org/prebid/server/vast/VastModifier.java
    new file mode 100644
    index 00000000000..beb7e2358e8
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/vast/VastModifier.java
    @@ -0,0 +1,148 @@
    +package org.prebid.server.vast;
    +
    +import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.node.TextNode;
    +import org.apache.commons.lang3.BooleanUtils;
    +import org.apache.commons.lang3.StringUtils;
    +import org.prebid.server.bidder.BidderCatalog;
    +import org.prebid.server.cache.proto.request.PutObject;
    +import org.prebid.server.events.EventsContext;
    +import org.prebid.server.events.EventsService;
    +import org.prebid.server.exception.PreBidException;
    +import org.prebid.server.metric.MetricName;
    +import org.prebid.server.metric.Metrics;
    +
    +import java.util.List;
    +import java.util.Objects;
    +import java.util.Set;
    +
    +public class VastModifier {
    +
    +    private static final String IN_LINE_TAG = "";
    +    private static final String IN_LINE_CLOSE_TAG = "";
    +    private static final String WRAPPER_TAG = "";
    +    private static final String WRAPPER_CLOSE_TAG = "";
    +    private static final String IMPRESSION_CLOSE_TAG = "";
    +    private final BidderCatalog bidderCatalog;
    +    private final EventsService eventsService;
    +    private final Metrics metrics;
    +
    +    public VastModifier(BidderCatalog bidderCatalog, EventsService eventsService, Metrics metrics) {
    +        this.bidderCatalog = Objects.requireNonNull(bidderCatalog);
    +        this.eventsService = Objects.requireNonNull(eventsService);
    +        this.metrics = Objects.requireNonNull(metrics);
    +    }
    +
    +    public JsonNode modifyVastXml(Boolean isEventsEnabled,
    +                                  Set allowedBidders,
    +                                  PutObject putObject,
    +                                  String accountId,
    +                                  String integration) {
    +        final JsonNode value = putObject.getValue();
    +        final String bidder = putObject.getBidder();
    +        final boolean isValueValid = value != null && !value.isNull();
    +        if (BooleanUtils.isTrue(isEventsEnabled) && allowedBidders.contains(bidder) && isValueValid) {
    +            final EventsContext eventsContext = EventsContext.builder()
    +                    .auctionId(putObject.getAid())
    +                    .auctionTimestamp(putObject.getTimestamp())
    +                    .integration(integration)
    +                    .build();
    +            final String vastUrlTracking = eventsService.vastUrlTracking(
    +                    putObject.getBidid(),
    +                    bidder,
    +                    accountId,
    +                    null,
    +                    eventsContext);
    +            try {
    +                return new TextNode(appendTrackingUrlToVastXml(value.asText(), vastUrlTracking, bidder));
    +            } catch (PreBidException e) {
    +                metrics.updateAdapterRequestErrorMetric(bidder, MetricName.badserverresponse);
    +            }
    +        }
    +
    +        return value;
    +    }
    +
    +    public String createBidVastXml(String bidder,
    +                                   String bidAdm,
    +                                   String bidNurl,
    +                                   String eventBidId,
    +                                   String accountId,
    +                                   EventsContext eventsContext,
    +                                   List debugWarnings,
    +                                   String lineItemId) {
    +        if (!bidderCatalog.isModifyingVastXmlAllowed(bidder)) {
    +            return bidAdm;
    +        }
    +
    +        final String vastXml = resolveVastXmlFrom(bidAdm, bidNurl);
    +        if (!eventsContext.isEnabledForAccount()) {
    +            return vastXml;
    +        }
    +
    +        final String vastUrl = eventsService.vastUrlTracking(eventBidId, bidder,
    +                accountId, lineItemId, eventsContext);
    +        try {
    +            return appendTrackingUrlToVastXml(vastXml, vastUrl, bidder);
    +        } catch (PreBidException e) {
    +            debugWarnings.add(e.getMessage());
    +            metrics.updateAdapterRequestErrorMetric(bidder, MetricName.badserverresponse);
    +        }
    +        return vastXml;
    +    }
    +
    +    private static String resolveVastXmlFrom(String bidAdm, String bidNurl) {
    +        return StringUtils.isEmpty(bidAdm) && bidNurl != null
    +                ? ""
    +                + "prebid.org wrapper"
    +                + ""
    +                + ""
    +                + ""
    +                : bidAdm;
    +    }
    +
    +    private String appendTrackingUrlToVastXml(String vastXml, String vastUrlTracking, String bidder) {
    +        final int inLineTagIndex = StringUtils.indexOfIgnoreCase(vastXml, IN_LINE_TAG);
    +        final int wrapperTagIndex = StringUtils.indexOfIgnoreCase(vastXml, WRAPPER_TAG);
    +
    +        if (inLineTagIndex != -1) {
    +            return appendTrackingUrl(vastXml, vastUrlTracking, IN_LINE_CLOSE_TAG);
    +        } else if (wrapperTagIndex != -1) {
    +            return appendTrackingUrl(vastXml, vastUrlTracking, WRAPPER_CLOSE_TAG);
    +        }
    +        throw new PreBidException(
    +                String.format("VastXml does not contain neither InLine nor Wrapper for %s response", bidder));
    +    }
    +
    +    private static String appendTrackingUrl(String vastXml, String vastUrlTracking, String elementCloseTag) {
    +        if (vastXml.contains(IMPRESSION_CLOSE_TAG)) {
    +            return insertAfterExistingImpressionTag(vastXml, vastUrlTracking);
    +        }
    +        return insertBeforeElementCloseTag(vastXml, vastUrlTracking, elementCloseTag);
    +    }
    +
    +    private static String insertAfterExistingImpressionTag(String vastXml, String vastUrlTracking) {
    +        final String impressionTag = "";
    +        final int replacementStart = vastXml.lastIndexOf(IMPRESSION_CLOSE_TAG);
    +
    +        return new StringBuilder().append(vastXml, 0, replacementStart)
    +                .append(IMPRESSION_CLOSE_TAG)
    +                .append(impressionTag)
    +                .append(vastXml.substring(replacementStart + IMPRESSION_CLOSE_TAG.length()))
    +                .toString();
    +    }
    +
    +    private static String insertBeforeElementCloseTag(String vastXml, String vastUrlTracking, String elementCloseTag) {
    +        final int indexOfCloseTag = StringUtils.indexOfIgnoreCase(vastXml, elementCloseTag);
    +
    +        if (indexOfCloseTag == -1) {
    +            return vastXml;
    +        }
    +
    +        final String caseSpecificCloseTag =
    +                vastXml.substring(indexOfCloseTag, indexOfCloseTag + elementCloseTag.length());
    +        final String impressionTag = "";
    +
    +        return vastXml.replace(caseSpecificCloseTag, impressionTag + caseSpecificCloseTag);
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/vertx/CircuitBreaker.java b/src/main/java/org/prebid/server/vertx/CircuitBreaker.java
    index cd16be04946..62751197055 100644
    --- a/src/main/java/org/prebid/server/vertx/CircuitBreaker.java
    +++ b/src/main/java/org/prebid/server/vertx/CircuitBreaker.java
    @@ -38,6 +38,7 @@ public CircuitBreaker(String name,
                     Objects.requireNonNull(name),
                     Objects.requireNonNull(vertx),
                     new CircuitBreakerOptions()
    +                        .setNotificationPeriod(0)
                             .setMaxFailures(openingThreshold)
                             .setResetTimeout(closingIntervalMs));
     
    @@ -133,4 +134,16 @@ public CircuitBreaker closeHandler(Handler handler) {
             breaker.closeHandler(handler);
             return this;
         }
    +
    +    public boolean isOpen() {
    +        switch (breaker.state()) {
    +            case OPEN:
    +            case HALF_OPEN:
    +                return true;
    +            case CLOSED:
    +                return false;
    +            default:
    +                throw new IllegalStateException("Should never happen");
    +        }
    +    }
     }
    diff --git a/src/main/java/org/prebid/server/vertx/ContextRunner.java b/src/main/java/org/prebid/server/vertx/ContextRunner.java
    index ed5ea8fdc20..19229ec1b8c 100644
    --- a/src/main/java/org/prebid/server/vertx/ContextRunner.java
    +++ b/src/main/java/org/prebid/server/vertx/ContextRunner.java
    @@ -53,7 +53,6 @@ public  void runOnServiceContext(Handler> action) {
         private  void runOnContext(Supplier contextFactory, int times, Handler> action) {
             final CountDownLatch completionLatch = new CountDownLatch(times);
             final AtomicBoolean actionFailed = new AtomicBoolean(false);
    -
             for (int i = 0; i < times; i++) {
                 final Context context = contextFactory.get();
     
    diff --git a/src/main/java/org/prebid/server/vertx/LocalMessageCodec.java b/src/main/java/org/prebid/server/vertx/LocalMessageCodec.java
    new file mode 100644
    index 00000000000..0714cffe474
    --- /dev/null
    +++ b/src/main/java/org/prebid/server/vertx/LocalMessageCodec.java
    @@ -0,0 +1,46 @@
    +package org.prebid.server.vertx;
    +
    +import io.vertx.core.buffer.Buffer;
    +import io.vertx.core.eventbus.EventBus;
    +import io.vertx.core.eventbus.MessageCodec;
    +
    +/**
    + * Message codec intended for use with objects passed around via {@link EventBus} only locally, i.e. within one JVM.
    + */
    +public class LocalMessageCodec implements MessageCodec {
    +
    +    private static final String CODEC_NAME = "LocalMessageCodec";
    +
    +    public static MessageCodec create() {
    +        return new LocalMessageCodec();
    +    }
    +
    +    @Override
    +    public void encodeToWire(Buffer buffer, Object source) {
    +        throw new UnsupportedOperationException("Serialization is not supported by this message codec");
    +    }
    +
    +    @Override
    +    public Object decodeFromWire(int pos, Buffer buffer) {
    +        throw new UnsupportedOperationException("Deserialization is not supported by this message codec");
    +    }
    +
    +    @Override
    +    public Object transform(Object source) {
    +        return source;
    +    }
    +
    +    @Override
    +    public String name() {
    +        return codecName();
    +    }
    +
    +    @Override
    +    public byte systemCodecID() {
    +        return -1;
    +    }
    +
    +    public static String codecName() {
    +        return CODEC_NAME;
    +    }
    +}
    diff --git a/src/main/java/org/prebid/server/vertx/http/BasicHttpClient.java b/src/main/java/org/prebid/server/vertx/http/BasicHttpClient.java
    index 45c54cb83e9..6c1d927a7a5 100644
    --- a/src/main/java/org/prebid/server/vertx/http/BasicHttpClient.java
    +++ b/src/main/java/org/prebid/server/vertx/http/BasicHttpClient.java
    @@ -4,12 +4,14 @@
     import io.vertx.core.MultiMap;
     import io.vertx.core.Promise;
     import io.vertx.core.Vertx;
    +import io.vertx.core.buffer.Buffer;
     import io.vertx.core.http.HttpClientRequest;
     import io.vertx.core.http.HttpMethod;
     import org.prebid.server.vertx.http.model.HttpClientResponse;
     
     import java.util.Objects;
     import java.util.concurrent.TimeoutException;
    +import java.util.function.Consumer;
     
     /**
      * Simple wrapper around {@link HttpClient} with general functionality.
    @@ -27,6 +29,19 @@ public BasicHttpClient(Vertx vertx, io.vertx.core.http.HttpClient httpClient) {
         @Override
         public Future request(HttpMethod method, String url, MultiMap headers, String body,
                                                   long timeoutMs) {
    +        return request(method, url, headers, timeoutMs, body,
    +                (HttpClientRequest httpClientRequest) -> httpClientRequest.end(body));
    +    }
    +
    +    @Override
    +    public Future request(HttpMethod method, String url, MultiMap headers, byte[] body,
    +                                              long timeoutMs) {
    +        return request(method, url, headers, timeoutMs, body,
    +                (HttpClientRequest httpClientRequest) -> httpClientRequest.end(Buffer.buffer(body)));
    +    }
    +
    +    private  Future request(HttpMethod method, String url, MultiMap headers, long timeoutMs,
    +                                                   T body, Consumer requestBodySetter) {
             final Promise promise = Promise.promise();
     
             if (timeoutMs <= 0) {
    @@ -48,12 +63,11 @@ public Future request(HttpMethod method, String url, MultiMa
                 }
     
                 if (body != null) {
    -                httpClientRequest.end(body);
    +                requestBodySetter.accept(httpClientRequest);
                 } else {
                     httpClientRequest.end();
                 }
             }
    -
             return promise.future();
         }
     
    diff --git a/src/main/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClient.java b/src/main/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClient.java
    index 1bf3516363d..265c09b785a 100644
    --- a/src/main/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClient.java
    +++ b/src/main/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClient.java
    @@ -1,5 +1,6 @@
     package org.prebid.server.vertx.http;
     
    +import com.github.benmanes.caffeine.cache.Caffeine;
     import io.vertx.core.Future;
     import io.vertx.core.MultiMap;
     import io.vertx.core.Vertx;
    @@ -17,7 +18,6 @@
     import java.time.Clock;
     import java.util.Map;
     import java.util.Objects;
    -import java.util.concurrent.ConcurrentHashMap;
     import java.util.concurrent.TimeUnit;
     import java.util.function.Function;
     
    @@ -29,12 +29,12 @@ public class CircuitBreakerSecuredHttpClient implements HttpClient {
         private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerSecuredHttpClient.class);
         private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger);
         private static final int LOG_PERIOD_SECONDS = 5;
    +    private static final long IDLE_EXPIRE_DAYS = 3;
     
         private final Function circuitBreakerCreator;
    -    private final Map circuitBreakerByName = new ConcurrentHashMap<>();
    +    private final Map circuitBreakerByName;
     
         private final HttpClient httpClient;
    -    private final Metrics metrics;
     
         public CircuitBreakerSecuredHttpClient(Vertx vertx,
                                                HttpClient httpClient,
    @@ -44,8 +44,50 @@ public CircuitBreakerSecuredHttpClient(Vertx vertx,
                                                long closingIntervalMs,
                                                Clock clock) {
     
    -        circuitBreakerCreator = name -> new CircuitBreaker(
    -                "http-client-circuit-breaker-" + name,
    +        this.httpClient = Objects.requireNonNull(httpClient);
    +
    +        circuitBreakerCreator = name -> createCircuitBreaker(
    +                name, vertx, openingThreshold, openingIntervalMs, closingIntervalMs, clock, metrics);
    +
    +        circuitBreakerByName = Caffeine.newBuilder()
    +                .expireAfterAccess(IDLE_EXPIRE_DAYS, TimeUnit.DAYS) // remove unused CBs
    +                .removalListener((name, cb, cause) -> removeCircuitBreakerGauge(name, metrics))
    +                .build()
    +                .asMap();
    +
    +        metrics.createHttpClientCircuitBreakerNumberGauge(circuitBreakerByName::size);
    +
    +        logger.info("Initialized HTTP client with Circuit Breaker");
    +    }
    +
    +    @Override
    +    public Future request(HttpMethod method,
    +                                              String url,
    +                                              MultiMap headers,
    +                                              String body,
    +                                              long timeoutMs) {
    +
    +        return circuitBreakerByName.computeIfAbsent(nameFrom(url), circuitBreakerCreator)
    +                .execute(promise -> httpClient.request(method, url, headers, body, timeoutMs).setHandler(promise));
    +    }
    +
    +    @Override
    +    public Future request(HttpMethod method, String url, MultiMap headers, byte[] body,
    +                                              long timeoutMs) {
    +        return circuitBreakerByName.computeIfAbsent(nameFrom(url), circuitBreakerCreator)
    +                .execute(promise -> httpClient.request(method, url, headers, body, timeoutMs).setHandler(promise));
    +    }
    +
    +    private CircuitBreaker createCircuitBreaker(String name,
    +                                                Vertx vertx,
    +                                                int openingThreshold,
    +                                                long openingIntervalMs,
    +                                                long closingIntervalMs,
    +                                                Clock clock,
    +                                                Metrics metrics) {
    +
    +        final CircuitBreaker circuitBreaker = new CircuitBreaker(
    +                "http_cb_" + name,
                     Objects.requireNonNull(vertx),
                     openingThreshold,
                     openingIntervalMs,
    @@ -55,16 +97,22 @@ public CircuitBreakerSecuredHttpClient(Vertx vertx,
                     .halfOpenHandler(ignored -> circuitHalfOpened(name))
                     .closeHandler(ignored -> circuitClosed(name));
     
    -        this.httpClient = Objects.requireNonNull(httpClient);
    -        this.metrics = Objects.requireNonNull(metrics);
    +        createCircuitBreakerGauge(name, circuitBreaker, metrics);
     
    -        logger.info("Initialized HTTP client with Circuit Breaker");
    +        return circuitBreaker;
    +    }
    +
    +    private void createCircuitBreakerGauge(String name, CircuitBreaker circuitBreaker, Metrics metrics) {
    +        metrics.createHttpClientCircuitBreakerGauge(idFrom(name), circuitBreaker::isOpen);
    +    }
    +
    +    private void removeCircuitBreakerGauge(String name, Metrics metrics) {
    +        metrics.removeHttpClientCircuitBreakerGauge(idFrom(name));
         }
     
         private void circuitOpened(String name) {
             conditionalLogger.warn(String.format("Http client request to %s is failed, circuit opened.", name),
                     LOG_PERIOD_SECONDS, TimeUnit.SECONDS);
    -        metrics.updateHttpClientCircuitBreakerMetric(idFrom(name), true);
         }
     
         private void circuitHalfOpened(String name) {
    @@ -73,25 +121,16 @@ private void circuitHalfOpened(String name) {
     
         private void circuitClosed(String name) {
             logger.warn("Http client request to {0} becomes succeeded, circuit closed.", name);
    -        metrics.updateHttpClientCircuitBreakerMetric(idFrom(name), false);
    -    }
    -
    -    @Override
    -    public Future request(HttpMethod method, String url, MultiMap headers, String body,
    -                                              long timeoutMs) {
    -        return circuitBreakerByName.computeIfAbsent(nameFrom(url), circuitBreakerCreator)
    -                .execute(promise -> httpClient.request(method, url, headers, body, timeoutMs).setHandler(promise));
         }
     
         private static String nameFrom(String urlAsString) {
             final URL url = parseUrl(urlAsString);
    -        return url.getProtocol() + "://" + url.getHost()
    -                + (url.getPort() != -1 ? ":" + url.getPort() : "") + url.getPath();
    +        return url.getProtocol() + "://" + url.getHost() + (url.getPort() != -1 ? ":" + url.getPort() : "");
         }
     
         private static String idFrom(String urlAsString) {
    -        final URL url = parseUrl(urlAsString);
    -        return url.getHost().replaceAll("[^\\w]", "_");
    +        return urlAsString
    +                .replaceAll("[^\\w]+", "_");
         }
     
         private static URL parseUrl(String url) {
    diff --git a/src/main/java/org/prebid/server/vertx/http/HttpClient.java b/src/main/java/org/prebid/server/vertx/http/HttpClient.java
    index 3b827fa71cb..50c045cdc8e 100644
    --- a/src/main/java/org/prebid/server/vertx/http/HttpClient.java
    +++ b/src/main/java/org/prebid/server/vertx/http/HttpClient.java
    @@ -8,17 +8,18 @@
     /**
      * Interface describes HTTP interactions.
      */
    -@FunctionalInterface
     public interface HttpClient {
     
         Future request(HttpMethod method, String url, MultiMap headers, String body, long timeoutMs);
     
    +    Future request(HttpMethod method, String url, MultiMap headers, byte[] body, long timeoutMs);
    +
         default Future get(String url, MultiMap headers, long timeoutMs) {
    -        return request(HttpMethod.GET, url, headers, null, timeoutMs);
    +        return request(HttpMethod.GET, url, headers, (String) null, timeoutMs);
         }
     
         default Future get(String url, long timeoutMs) {
    -        return request(HttpMethod.GET, url, null, null, timeoutMs);
    +        return request(HttpMethod.GET, url, null, (String) null, timeoutMs);
         }
     
         default Future post(String url, MultiMap headers, String body, long timeoutMs) {
    diff --git a/src/main/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClient.java b/src/main/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClient.java
    index bacf262d672..b50b58977cc 100644
    --- a/src/main/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClient.java
    +++ b/src/main/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClient.java
    @@ -25,30 +25,46 @@ public class CircuitBreakerSecuredJdbcClient implements JdbcClient {
         private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger);
         private static final int LOG_PERIOD_SECONDS = 5;
     
    -    private final CircuitBreaker breaker;
         private final JdbcClient jdbcClient;
    -    private final Metrics metrics;
    +    private final CircuitBreaker breaker;
     
    -    public CircuitBreakerSecuredJdbcClient(Vertx vertx, JdbcClient jdbcClient, Metrics metrics,
    -                                           int openingThreshold, long openingIntervalMs, long closingIntervalMs,
    +    public CircuitBreakerSecuredJdbcClient(Vertx vertx,
    +                                           JdbcClient jdbcClient,
    +                                           Metrics metrics,
    +                                           int openingThreshold,
    +                                           long openingIntervalMs,
    +                                           long closingIntervalMs,
                                                Clock clock) {
     
    -        breaker = new CircuitBreaker("jdbc-client-circuit-breaker", Objects.requireNonNull(vertx),
    -                openingThreshold, openingIntervalMs, closingIntervalMs, Objects.requireNonNull(clock))
    +        this.jdbcClient = Objects.requireNonNull(jdbcClient);
    +
    +        breaker = new CircuitBreaker(
    +                "db_cb",
    +                Objects.requireNonNull(vertx),
    +                openingThreshold,
    +                openingIntervalMs,
    +                closingIntervalMs,
    +                Objects.requireNonNull(clock))
                     .openHandler(ignored -> circuitOpened())
                     .halfOpenHandler(ignored -> circuitHalfOpened())
                     .closeHandler(ignored -> circuitClosed());
     
    -        this.jdbcClient = Objects.requireNonNull(jdbcClient);
    -        this.metrics = Objects.requireNonNull(metrics);
    +        metrics.createDatabaseCircuitBreakerGauge(breaker::isOpen);
     
             logger.info("Initialized JDBC client with Circuit Breaker");
         }
     
    +    @Override
    +    public  Future executeQuery(String query,
    +                                      List params,
    +                                      Function mapper,
    +                                      Timeout timeout) {
    +
    +        return breaker.execute(promise -> jdbcClient.executeQuery(query, params, mapper, timeout).setHandler(promise));
    +    }
    +
         private void circuitOpened() {
    -        conditionalLogger.warn("Database is unavailable, circuit opened.",
    -                LOG_PERIOD_SECONDS, TimeUnit.SECONDS);
    -        metrics.updateDatabaseCircuitBreakerMetric(true);
    +        conditionalLogger.warn("Database is unavailable, circuit opened.", LOG_PERIOD_SECONDS, TimeUnit.SECONDS);
         }
     
         private void circuitHalfOpened() {
    @@ -57,12 +73,5 @@ private void circuitHalfOpened() {
     
         private void circuitClosed() {
             logger.warn("Database becomes working, circuit closed.");
    -        metrics.updateDatabaseCircuitBreakerMetric(false);
    -    }
    -
    -    @Override
    -    public  Future executeQuery(String query, List params, Function mapper,
    -                                      Timeout timeout) {
    -        return breaker.execute(promise -> jdbcClient.executeQuery(query, params, mapper, timeout).setHandler(promise));
         }
     }
    diff --git a/src/main/java/org/prebid/server/vertx/jdbc/JdbcClient.java b/src/main/java/org/prebid/server/vertx/jdbc/JdbcClient.java
    index 159191ea3ae..b447ea98dc9 100644
    --- a/src/main/java/org/prebid/server/vertx/jdbc/JdbcClient.java
    +++ b/src/main/java/org/prebid/server/vertx/jdbc/JdbcClient.java
    @@ -8,14 +8,14 @@
     import java.util.function.Function;
     
     /**
    - * Interface for asynchronous interaction with database over JDBC API
    + * Interface for asynchronous interaction with database over JDBC API.
      */
     @FunctionalInterface
     public interface JdbcClient {
     
         /**
          * Executes query with parameters and returns {@link Future} eventually holding result mapped to a model
    -     * object by provided mapper
    +     * object by provided mapper.
          */
          Future executeQuery(String query, List params, Function mapper, Timeout timeout);
     }
    diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
    index 16d371548fc..679c0a282e8 100644
    --- a/src/main/resources/application.yaml
    +++ b/src/main/resources/application.yaml
    @@ -6,6 +6,7 @@ vertx:
       uploads-dir: file-uploads
       init-timeout-ms: 5000
       http-server-instances: 1
    +  enable-per-client-endpoint-metrics: false
     http:
       port: 8080
       max-headers-size: 16384
    @@ -50,6 +51,26 @@ admin-endpoints:
         path: /logging/changelevel
         on-application-port: false
         protected: true
    +  tracelog:
    +    enabled: false
    +    path: /pbs-admin/tracelog
    +    on-application-port: false
    +    protected: true
    +  deals-status:
    +    enabled: false
    +    path: /pbs-admin/deals-status
    +    on-application-port: false
    +    protected: true
    +  lineitem-status:
    +    enabled: false
    +    path: /pbs-admin/lineitem-status
    +    on-application-port: false
    +    protected: true
    +  e2eadmin:
    +    enabled: false
    +    path: /pbs-admin/e2eAdmin/*
    +    on-application-port: false
    +    protected: true
     http-client:
       max-pool-size: 4000
       idle-timeout-ms: 0
    @@ -67,6 +88,11 @@ vendor: local
     default-timeout-ms: 900
     max-timeout-ms: 5000
     timeout-adjustment-ms: 30
    +system: system
    +sub-system: subSystem
    +infra: infra
    +data-center: dataCenter
    +profile: profile
     auction:
       ad-server-currency: USD
       blacklisted-accounts:
    @@ -75,13 +101,23 @@ auction:
       max-timeout-ms: 5000
       timeout-adjustment-ms: 30
       stored-requests-timeout-ms: 50
    +  timeout-notification:
    +    timeout-ms: 200
    +    log-result: false
    +    log-failure-only: false
    +    log-sampling-rate: 0.0
       max-request-size: 262144
    -  id-generator-type: uuid
    +  generate-source-tid: true
       generate-bid-id: false
       cache:
         expected-request-time-ms: 10
         only-winning-bids: false
    +  validations:
    +    banner-creative-max-size: skip
    +    secure-markup: skip
    +  host-schain-node:
     video:
    +  stored-request-required: false
       stored-requests-timeout-ms: 90
     amp:
       default-timeout-ms: 900
    @@ -91,7 +127,8 @@ setuid:
       default-timeout-ms: 2000
     vtrack:
       default-timeout-ms: 2000
    -  allow-unkonwn-bidder: true
    +  allow-unknown-bidder: true
    +  modify-vast-for-unknown-bidder: true
     cookie-sync:
       coop-sync:
         default: true
    @@ -107,11 +144,13 @@ currency-converter:
         url: https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json
         default-timeout-ms: 4000
         refresh-period-ms: 900000
    +    stale-after-ms: 259200000
     metrics:
       metricType: flushingCounter
       accounts:
         default-verbosity: none
     settings:
    +  generate-storedrequest-bidrequest-id: false
       enforce-valid-account: false
       database:
         pool-size: 20
    @@ -122,6 +161,7 @@ settings:
         account-invalidation-enabled: true
       targeting:
         truncate-attr-chars: 20
    +  default-account-config:
     recaptcha-url: https://www.google.com/recaptcha/api/siteverify
     recaptcha-secret: secret_value
     host-cookie:
    @@ -131,16 +171,17 @@ host-cookie:
       max-cookie-size-bytes: 0
     gdpr:
       enabled: true
    -  default-value: 1
       eea-countries: at,bg,be,cy,cz,dk,ee,fi,fr,de,gr,hu,ie,it,lv,lt,lu,mt,nl,pl,pt,ro,sk,si,es,se,gb,is,no,li,ai,aw,pt,bm,aq,io,vg,ic,ky,fk,re,mw,gp,gf,yt,pf,tf,gl,pt,ms,an,bq,cw,sx,nc,pn,sh,pm,gs,tc,uk,wf
       vendorlist:
         default-timeout-ms: 2000
         v1:
    -      http-endpoint-template: https://vendorlist.consensu.org/v-{VERSION}/vendorlist.json
    +      http-endpoint-template: https://vendor-list.consensu.org/v-{VERSION}/vendorlist.json
           refresh-missing-list-period-ms: 3600000
    +      deprecated: false
         v2:
    -      http-endpoint-template: https://vendorlist.consensu.org/v2/archives/vendor-list-v{VERSION}.json
    +      http-endpoint-template: https://vendor-list.consensu.org/v2/archives/vendor-list-v{VERSION}.json
           refresh-missing-list-period-ms: 3600000
    +      deprecated: false
       purposes:
         p1:
           enforce-purpose: full
    @@ -180,6 +221,8 @@ gdpr:
       purpose-one-treatment-interpretation: ignore
     ccpa:
       enforce: true
    +lmt:
    +  enforce: true
     geolocation:
       enabled: false
       type: maxmind
    @@ -204,3 +247,51 @@ ipv6:
       always-mask-right: 64
       anon-left-mask-bits: 56
       private-networks: ::1/128, 2001:db8::/32, fc00::/7, fe80::/10, ff00::/8
    +analytics:
    +  pubstack:
    +    enabled: false
    +    endpoint: http://localhost:8090
    +    scopeid: change-me
    +    configuration-refresh-delay-ms: 7200000
    +    timeout-ms: 5000
    +    buffers:
    +      size-bytes: 2097152
    +      count: 100
    +      report-ttl-ms: 900000
    +
    +device-info:
    +  enabled: false
    +deals:
    +  enabled: false
    +  simulation:
    +    enabled: false
    +    ready-at-adjustment-ms: 0
    +  planner:
    +    plan-advance-period: "0 */1 * * * *"
    +    update-period: "0 */1 * * * *"
    +    timeout-ms: 4000
    +    register-period-sec: 60
    +  delivery-stats:
    +    delivery-period: "0 */1 * * * *"
    +    cached-reports-number: 20
    +    line-item-status-ttl-sec: 3600
    +    line-items-per-report: 25
    +    reports-interval-ms: 0
    +    batches-interval-ms: 1000
    +    request-compression-enabled: true
    +  delivery-progress:
    +    line-item-status-ttl-sec: 3600
    +    cached-plans-number: 20
    +    report-reset-period: "0 */1 * * * *"
    +  delivery-progress-report:
    +    competitors-number: 10
    +  max-deals-per-bidder: 3
    +  alert-proxy:
    +    enabled: false
    +    url: http://localhost
    +    timeout-sec: 5
    +    alert-types:
    +      pbs-planner-client-error: 15
    +      pbs-planner-empty-response-error: 15
    +      pbs-register-client-error: 15
    +      pbs-delivery-stats-client-error: 15
    diff --git a/src/main/resources/bidder-config/acuityads.yaml b/src/main/resources/bidder-config/acuityads.yaml
    new file mode 100644
    index 00000000000..01113d5a930
    --- /dev/null
    +++ b/src/main/resources/bidder-config/acuityads.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  acuityads:
    +    enabled: false
    +    endpoint: http://{{Host}}.admanmedia.com/bid?token={{AccountID}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: integrations@acuityads.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 231
    +    usersync:
    +      url: https://cs.admanmedia.com/sync/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
    +      redirect-url: /setuid?bidder=acuityads&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[UID]
    +      cookie-family-name: acuityads
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/adf.yaml b/src/main/resources/bidder-config/adf.yaml
    new file mode 100644
    index 00000000000..1888a9cd21e
    --- /dev/null
    +++ b/src/main/resources/bidder-config/adf.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  adf:
    +    enabled: false
    +    endpoint: https://adx.adform.net/adx/openrtb
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: scope.sspp@adform.com
    +      app-media-types:
    +        - banner
    +        - native
    +        - video
    +      site-media-types:
    +        - banner
    +        - native
    +        - video
    +      supported-vendors:
    +      vendor-id: 50
    +    usersync:
    +      url: https://cm.adform.net/cookie?redirect_url=
    +      redirect-url: /setuid?bidder=adf&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID
    +      cookie-family-name: adf
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/adform.yaml b/src/main/resources/bidder-config/adform.yaml
    index 2924ef3707a..b9d10ca014d 100644
    --- a/src/main/resources/bidder-config/adform.yaml
    +++ b/src/main/resources/bidder-config/adform.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: scope.sspp@adform.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/adgeneration.yaml b/src/main/resources/bidder-config/adgeneration.yaml
    index 136809a7e3a..37e26b93fcd 100644
    --- a/src/main/resources/bidder-config/adgeneration.yaml
    +++ b/src/main/resources/bidder-config/adgeneration.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: ssp-ope@supership.jp
           app-media-types:
    diff --git a/src/main/resources/bidder-config/adhese.yaml b/src/main/resources/bidder-config/adhese.yaml
    index 02bb0098f50..2f2f8cdd15b 100644
    --- a/src/main/resources/bidder-config/adhese.yaml
    +++ b/src/main/resources/bidder-config/adhese.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: info@adhese.com
           app-media-types:
    @@ -16,7 +16,7 @@ adapters:
             - banner
             - video
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 553
         usersync:
           url:
           redirect-url:
    diff --git a/src/main/resources/bidder-config/adkernel.yaml b/src/main/resources/bidder-config/adkernel.yaml
    index 9f00b37cbd3..7579337265c 100644
    --- a/src/main/resources/bidder-config/adkernel.yaml
    +++ b/src/main/resources/bidder-config/adkernel.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid-dev@adkernel.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/adkerneladn.yaml b/src/main/resources/bidder-config/adkerneladn.yaml
    index 72a9b04857d..fc9f7ae41ee 100644
    --- a/src/main/resources/bidder-config/adkerneladn.yaml
    +++ b/src/main/resources/bidder-config/adkerneladn.yaml
    @@ -1,12 +1,12 @@
     adapters:
       adkerneladn:
         enabled: false
    -    endpoint: http://tag.adkernel.com/rtbpub?account=
    +    endpoint: http://{{Host}}/rtbpub?account={{PublisherID}}
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid-dev@adkernel.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/adman.yaml b/src/main/resources/bidder-config/adman.yaml
    index 2eea6558af4..031694850f4 100644
    --- a/src/main/resources/bidder-config/adman.yaml
    +++ b/src/main/resources/bidder-config/adman.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@admanmedia.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/admixer.yaml b/src/main/resources/bidder-config/admixer.yaml
    index 82cf3b92552..291427b4940 100644
    --- a/src/main/resources/bidder-config/admixer.yaml
    +++ b/src/main/resources/bidder-config/admixer.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: "prebid@admixer.net"
           app-media-types:
    @@ -20,7 +20,7 @@ adapters:
             - native
             - audio
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 511
         usersync:
           url: https://inv-nets.admixer.net/adxcm.aspx?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=1&rurl=
           redirect-url: /setuid?bidder=admixer&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$$visitor_cookie$$
    diff --git a/src/main/resources/bidder-config/adocean.yaml b/src/main/resources/bidder-config/adocean.yaml
    index 146b12edacb..80435c0530b 100644
    --- a/src/main/resources/bidder-config/adocean.yaml
    +++ b/src/main/resources/bidder-config/adocean.yaml
    @@ -6,10 +6,11 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: aoteam@gemius.com
           app-media-types:
    +        - banner
           site-media-types:
             - banner
           supported-vendors:
    diff --git a/src/main/resources/bidder-config/adoppler.yaml b/src/main/resources/bidder-config/adoppler.yaml
    index ad9492bf19d..2da613a35a7 100644
    --- a/src/main/resources/bidder-config/adoppler.yaml
    +++ b/src/main/resources/bidder-config/adoppler.yaml
    @@ -1,12 +1,12 @@
     adapters:
       adoppler:
         enabled: false
    -    endpoint: http://app.trustedmarketplace.io/ads
    +    endpoint: http://{{AccountID}}.trustedmarketplace.io/ads/processHeaderBid/{{AdUnit}}
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: "info@adoppler.com"
           app-media-types:
    @@ -22,4 +22,4 @@ adapters:
           redirect-url:
           cookie-family-name: adoppler
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/adot.yaml b/src/main/resources/bidder-config/adot.yaml
    new file mode 100644
    index 00000000000..b8591168da6
    --- /dev/null
    +++ b/src/main/resources/bidder-config/adot.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  adot:
    +    enabled: false
    +    endpoint: https://dsp.adotmob.com/headerbidding/bidrequest
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: false
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: admin@we-are-adot.com"
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 272
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: adot
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/adpone.yaml b/src/main/resources/bidder-config/adpone.yaml
    index ab182145225..29008b957d3 100644
    --- a/src/main/resources/bidder-config/adpone.yaml
    +++ b/src/main/resources/bidder-config/adpone.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: tech@adpone.com
           app-media-types:
    @@ -14,7 +14,7 @@ adapters:
           site-media-types:
             - banner
           supported-vendors:
    -      vendor-id: 16
    +      vendor-id: 799
         usersync:
           url: https://usersync.adpone.com/csync?redir=
           redirect-url: /setuid?bidder=adpone&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={uid}
    diff --git a/src/main/resources/bidder-config/adprime.yaml b/src/main/resources/bidder-config/adprime.yaml
    new file mode 100644
    index 00000000000..005768b385c
    --- /dev/null
    +++ b/src/main/resources/bidder-config/adprime.yaml
    @@ -0,0 +1,25 @@
    +adapters:
    +  adprime:
    +    enabled: false
    +    endpoint: http://delta.adprime.com/?c=o&m=ortb
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: prebid@adprime.com
    +      app-media-types:
    +        - banner
    +        - video
    +      site-media-types:
    +        - banner
    +        - video
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: adprime
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/adtarget.yaml b/src/main/resources/bidder-config/adtarget.yaml
    index 03cf31f8010..5858c3aa2ab 100644
    --- a/src/main/resources/bidder-config/adtarget.yaml
    +++ b/src/main/resources/bidder-config/adtarget.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: kamil@adtarget.com.tr
           app-media-types:
    @@ -16,10 +16,10 @@ adapters:
             - banner
             - video
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 779
         usersync:
    -      url: https://sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
    -      redirect-url: /setuid?bidder=adtarget&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={uid}
    +      url:
    +      redirect-url:
           cookie-family-name: adtarget
    -      type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      type: iframe
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/adtelligent.yaml b/src/main/resources/bidder-config/adtelligent.yaml
    index 5eff25f26ac..d6bd680bdf6 100644
    --- a/src/main/resources/bidder-config/adtelligent.yaml
    +++ b/src/main/resources/bidder-config/adtelligent.yaml
    @@ -7,6 +7,26 @@ adapters:
         modifying-vast-xml-allowed: true
         deprecated-names:
         aliases:
    +      mediafuse:
    +        enabled: false
    +        endpoint: http://ghb.hbmp.mediafuse.com/pbs/ortb
    +        meta-info:
    +          maintainer-email: support@mediafuse.com
    +          vendor-id: 411
    +        usersync:
    +          url:
    +          redirect-url:
    +          cookie-family-name: mediafuse
    +      viewdoes:
    +        enabled: false
    +        endpoint: http://ghb.sync.viewdeos.com/pbs/ortb
    +        meta-info:
    +          maintainer-email: contact@viewdeos.com
    +          vendor-id: 924
    +        usersync:
    +          url:
    +          redirect-url:
    +          cookie-family-name: viewdoes
         meta-info:
           maintainer-email: hb@adtelligent.com
           app-media-types:
    @@ -16,10 +36,10 @@ adapters:
             - banner
             - video
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 410
         usersync:
    -      url: https://sync.adtelligent.com/csync?t=p&ep=0&gdpr={{gdpr}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
    -      redirect-url: /setuid?bidder=adtelligent&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={uid}
    +      url:
    +      redirect-url:
           cookie-family-name: adtelligent
    -      type: redirect
    +      type: iframe
           support-cors: false
    diff --git a/src/main/resources/bidder-config/advangelists.yaml b/src/main/resources/bidder-config/advangelists.yaml
    index ad8ece79843..317f677cf8a 100644
    --- a/src/main/resources/bidder-config/advangelists.yaml
    +++ b/src/main/resources/bidder-config/advangelists.yaml
    @@ -1,12 +1,12 @@
     adapters:
       advangelists:
         enabled: false
    -    endpoint: http://nep.advangelists.com/xp/get?pubid=
    +    endpoint: http://nep.advangelists.com/xp/get?pubid={{PublisherID}}
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@advangelists.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/adxcg.yaml b/src/main/resources/bidder-config/adxcg.yaml
    new file mode 100644
    index 00000000000..1370a929c99
    --- /dev/null
    +++ b/src/main/resources/bidder-config/adxcg.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  adxcg:
    +    enabled: false
    +    endpoint: http://
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: info@adxcg.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: adxcg
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/adyoulike.yaml b/src/main/resources/bidder-config/adyoulike.yaml
    new file mode 100644
    index 00000000000..c04390734e1
    --- /dev/null
    +++ b/src/main/resources/bidder-config/adyoulike.yaml
    @@ -0,0 +1,24 @@
    +adapters:
    +  adyoulike:
    +    enabled: false
    +    endpoint: https://broker.omnitagjs.com/broker/bid?partnerId=19340f4f097d16f41f34fc0274981ca4
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: core@adyoulike.com
    +      app-media-types:
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 259
    +    usersync:
    +      url: http://visitor.omnitagjs.com/visitor/bsync?uid=19340f4f097d16f41f34fc0274981ca4&name=PrebidServer&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&url=
    +      redirect-url: /setuid?bidder=adyoulike&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[BUYER_USERID]
    +      cookie-family-name: adyoulike
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/aja.yaml b/src/main/resources/bidder-config/aja.yaml
    index 54e4e40b2f8..6a30e64d555 100644
    --- a/src/main/resources/bidder-config/aja.yaml
    +++ b/src/main/resources/bidder-config/aja.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: dev@aja-kk.co.jp
           app-media-types:
    @@ -18,8 +18,8 @@ adapters:
           supported-vendors:
           vendor-id: 0
         usersync:
    -      url: https://ad.as.amanad.adtdp.com/v1/sync/ssp?ssp=4&gdpr={{gdpr}}&us_privacy={{us_privacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Daja%26gdpr%3D{{gdpr}}%26gdpr_consent%3D{{gdpr_consent}}%26uid%3D%25s
    -      redirect-url:
    +      url: https://ad.as.amanad.adtdp.com/v1/sync/ssp?ssp=4&gdpr={{gdpr}}&us_privacy={{us_privacy}}&redir=
    +      redirect-url: /setuid?bidder=aja&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=%s
           cookie-family-name: aja
           type: redirect
           support-cors: false
    diff --git a/src/main/resources/bidder-config/algorix.yaml b/src/main/resources/bidder-config/algorix.yaml
    new file mode 100644
    index 00000000000..99ba2313ed0
    --- /dev/null
    +++ b/src/main/resources/bidder-config/algorix.yaml
    @@ -0,0 +1,24 @@
    +adapters:
    +  algorix:
    +    enabled: false
    +    endpoint: https://xyz.svr-algorix.com/rtb/sa?sid={SID}&token={TOKEN}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: xunyunbo@algorix.co
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: algorix
    +      type: redirect
    +      support-cors: false
    \ No newline at end of file
    diff --git a/src/main/resources/bidder-config/amx.yaml b/src/main/resources/bidder-config/amx.yaml
    new file mode 100644
    index 00000000000..86d479e7f88
    --- /dev/null
    +++ b/src/main/resources/bidder-config/amx.yaml
    @@ -0,0 +1,25 @@
    +adapters:
    +  amx:
    +    enabled: false
    +    endpoint: http://pbs.amxrtb.com/auction/openrtb
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: prebid@amxrtb.com
    +      app-media-types:
    +        - banner
    +        - video
    +      site-media-types:
    +        - banner
    +        - video
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url: https://prebid.a-mo.net/cchain/0?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb=
    +      redirect-url: /setuid?bidder=amx&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=
    +      cookie-family-name: amx
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/applogy.yaml b/src/main/resources/bidder-config/applogy.yaml
    index a8f0ec4119a..5ab676d8eb5 100644
    --- a/src/main/resources/bidder-config/applogy.yaml
    +++ b/src/main/resources/bidder-config/applogy.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: work@applogy.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/appnexus.yaml b/src/main/resources/bidder-config/appnexus.yaml
    index f78f4cd26a8..a701a054cc9 100644
    --- a/src/main/resources/bidder-config/appnexus.yaml
    +++ b/src/main/resources/bidder-config/appnexus.yaml
    @@ -6,7 +6,9 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases: districtm
    +    aliases:
    +      districtm:
    +        enabled: false
         meta-info:
           maintainer-email: prebid-server@xandr.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/avocet.yaml b/src/main/resources/bidder-config/avocet.yaml
    index e69decbfcc6..f85bb70f0dc 100644
    --- a/src/main/resources/bidder-config/avocet.yaml
    +++ b/src/main/resources/bidder-config/avocet.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: developers@avocet.io
           app-media-types:
    diff --git a/src/main/resources/bidder-config/axonix.yaml b/src/main/resources/bidder-config/axonix.yaml
    new file mode 100644
    index 00000000000..8aae123d675
    --- /dev/null
    +++ b/src/main/resources/bidder-config/axonix.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  axonix:
    +    enabled: false
    +    endpoint: https://openrtb-us-east-1.axonix.com/supply/prebid-server/{{SupplyId}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support.axonix@emodoinc.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 678
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: axonix
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/beachfront.yaml b/src/main/resources/bidder-config/beachfront.yaml
    index 61bbdbcbfd5..36f929269c8 100644
    --- a/src/main/resources/bidder-config/beachfront.yaml
    +++ b/src/main/resources/bidder-config/beachfront.yaml
    @@ -7,7 +7,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@beachfront.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/beintoo.yaml b/src/main/resources/bidder-config/beintoo.yaml
    index 2b893499f07..fcf94a6285c 100644
    --- a/src/main/resources/bidder-config/beintoo.yaml
    +++ b/src/main/resources/bidder-config/beintoo.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: adops@beintoo.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/between.yaml b/src/main/resources/bidder-config/between.yaml
    new file mode 100644
    index 00000000000..1ba64253844
    --- /dev/null
    +++ b/src/main/resources/bidder-config/between.yaml
    @@ -0,0 +1,23 @@
    +adapters:
    +  between:
    +    enabled: false
    +    endpoint: http://{{Host}}.betweendigital.com/openrtb_bid?sspId={{PublisherId}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: buying@betweenx.com
    +      app-media-types:
    +        - banner
    +      site-media-types:
    +        - banner
    +      supported-vendors:
    +      vendor-id: 724
    +    usersync:
    +      url: https://ads.betweendigital.com/match?bidder_id=pbs&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&callback_url=
    +      redirect-url: /setuid?bidder=between&gdpr=0&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}&uid=${USER_ID}
    +      cookie-family-name: between
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/bidmachine.yaml b/src/main/resources/bidder-config/bidmachine.yaml
    new file mode 100644
    index 00000000000..fe1f3441c87
    --- /dev/null
    +++ b/src/main/resources/bidder-config/bidmachine.yaml
    @@ -0,0 +1,23 @@
    +adapters:
    +  bidmachine:
    +    enabled: false
    +    endpoint: https://{{Host}}.bidmachine.io/{{PATH}}/{{SELLER_ID}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: hi@bidmachine.io
    +      app-media-types:
    +        - banner
    +        - video
    +      site-media-types:
    +      supported-vendors:
    +      vendor-id: 736
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: bidmachine
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/bidmyadz.yaml b/src/main/resources/bidder-config/bidmyadz.yaml
    new file mode 100644
    index 00000000000..a20af8849f6
    --- /dev/null
    +++ b/src/main/resources/bidder-config/bidmyadz.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  bidmyadz:
    +    enabled: false
    +    endpoint: http://endpoint.bidmyadz.com/c0f68227d14ed938c6c49f3967cbe9bc
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: contact@bidmyadz.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url: https://cookie-sync.bidmyadz.com/c0f68227d14ed938c6c49f3967cbe9bc?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&red="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dbidmyadz%26uid%3D%5BUID%5D%26us_privacy%3D{{.USPrivacy}}%26gdpr_consent%3D{{.GDPRConsent}}%26gdpr%3D{{.GDPR}}
    +      redirect-url:
    +      cookie-family-name: bidmyadz
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/bidscube.yaml b/src/main/resources/bidder-config/bidscube.yaml
    new file mode 100644
    index 00000000000..687124550e9
    --- /dev/null
    +++ b/src/main/resources/bidder-config/bidscube.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  bidscube:
    +    enabled: false
    +    endpoint: http://supply.bidscube.com/?c=o&m=rtb
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support@bidscube.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: bidscube
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/bmtm.yaml b/src/main/resources/bidder-config/bmtm.yaml
    new file mode 100644
    index 00000000000..b3806087075
    --- /dev/null
    +++ b/src/main/resources/bidder-config/bmtm.yaml
    @@ -0,0 +1,23 @@
    +adapters:
    +  bmtm:
    +    enabled: false
    +    endpoint: https://one.elitebidder.com/api/pbs
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: false
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: dev@brightmountainmedia.com
    +      app-media-types:
    +      site-media-types:
    +        - banner
    +        - video
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: bmtm
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/brightroll.yaml b/src/main/resources/bidder-config/brightroll.yaml
    index 049c67cb535..62b83d3cfd3 100644
    --- a/src/main/resources/bidder-config/brightroll.yaml
    +++ b/src/main/resources/bidder-config/brightroll.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: dsp-supply-prebid@verizonmedia.com
           app-media-types:
    @@ -134,7 +134,7 @@ adapters:
               - IAB26-3
               - IAB26-4
             imp_battr: [1, 3, 8, 9, 10, 13, 14, 17]
    -        bid_floor: 0.3
    +        bid_floor: 0.65
           - id: adthrive
             badv:
               - 100sofrecipes.com
    diff --git a/src/main/resources/bidder-config/colossus.yaml b/src/main/resources/bidder-config/colossus.yaml
    new file mode 100644
    index 00000000000..532c9d7abf6
    --- /dev/null
    +++ b/src/main/resources/bidder-config/colossus.yaml
    @@ -0,0 +1,25 @@
    +adapters:
    +  colossus:
    +    enabled: false
    +    endpoint: http://colossusssp.com/?c=o&m=rtb
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support@huddledmasses.com
    +      app-media-types:
    +        - banner
    +        - video
    +      site-media-types:
    +        - banner
    +        - video
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url: https://sync.colossusssp.com/pbs.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
    +      redirect-url: /setuid?bidder=colossus&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}&us_privacy={{us_privacy}}}&uid=[UID]
    +      cookie-family-name: colossus
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/connectad.yaml b/src/main/resources/bidder-config/connectad.yaml
    new file mode 100644
    index 00000000000..679682927c3
    --- /dev/null
    +++ b/src/main/resources/bidder-config/connectad.yaml
    @@ -0,0 +1,23 @@
    +adapters:
    +  connectad:
    +    enabled: false
    +    endpoint: http://bidder.connectad.io/API?src=pbs
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support@connectad.io
    +      app-media-types:
    +        - banner
    +      site-media-types:
    +        - banner
    +      supported-vendors:
    +      vendor-id: 138
    +    usersync:
    +      url: https://cdn.connectad.io/connectmyusers.php?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb=
    +      redirect-url: /setuid?bidder=connectad&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&&us_privacy={{us_privacy}}&uid=
    +      cookie-family-name: connectad
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/consumable.yaml b/src/main/resources/bidder-config/consumable.yaml
    index 173a813205c..df35aa02551 100644
    --- a/src/main/resources/bidder-config/consumable.yaml
    +++ b/src/main/resources/bidder-config/consumable.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: naffis@consumable.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/conversant.yaml b/src/main/resources/bidder-config/conversant.yaml
    index dd6ed57d493..bbb2cf28bb7 100644
    --- a/src/main/resources/bidder-config/conversant.yaml
    +++ b/src/main/resources/bidder-config/conversant.yaml
    @@ -5,8 +5,9 @@ adapters:
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
    +    generate-bid-id: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: CNVR_PublisherIntegration@conversantmedia.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/cpmstar.yaml b/src/main/resources/bidder-config/cpmstar.yaml
    index d85db422ef1..6577b5d39b4 100644
    --- a/src/main/resources/bidder-config/cpmstar.yaml
    +++ b/src/main/resources/bidder-config/cpmstar.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@cpmstar.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/criteo.yaml b/src/main/resources/bidder-config/criteo.yaml
    new file mode 100644
    index 00000000000..629b067af83
    --- /dev/null
    +++ b/src/main/resources/bidder-config/criteo.yaml
    @@ -0,0 +1,24 @@
    +adapters:
    +  criteo:
    +    enabled: false
    +    endpoint: https://bidder.criteo.com/cdb?profileId=230
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    generate-slot-id: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: prebid@criteo.com
    +      app-media-types:
    +        - banner
    +      site-media-types:
    +        - banner
    +      supported-vendors:
    +      vendor-id: 91
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: criteo
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/datablocks.yaml b/src/main/resources/bidder-config/datablocks.yaml
    index 944b61dd63b..bd697165e1f 100644
    --- a/src/main/resources/bidder-config/datablocks.yaml
    +++ b/src/main/resources/bidder-config/datablocks.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@datablocks.net
           app-media-types:
    @@ -18,10 +18,10 @@ adapters:
             - video
             - native
           supported-vendors:
    -      vendor-id: 14
    +      vendor-id: 0
         usersync:
           url: https://sync.v5prebid.datablocks.net/s2ssync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r=
           redirect-url: /setuid?bidder=datablocks&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=${uid}
           cookie-family-name: datablocks
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/decenterads.yaml b/src/main/resources/bidder-config/decenterads.yaml
    new file mode 100644
    index 00000000000..ea72423d6da
    --- /dev/null
    +++ b/src/main/resources/bidder-config/decenterads.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  decenterads:
    +    enabled: false
    +    endpoint: http://supply.decenterads.com/?c=o&m=rtb
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support@decenterads.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: decenterads
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/deepintent.yaml b/src/main/resources/bidder-config/deepintent.yaml
    new file mode 100644
    index 00000000000..5f59148f1a2
    --- /dev/null
    +++ b/src/main/resources/bidder-config/deepintent.yaml
    @@ -0,0 +1,23 @@
    +adapters:
    +  deepintent:
    +    enabled: false
    +    endpoint: https://prebid.deepintent.com/prebid
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: sourabh@deepintent.com
    +      app-media-types:
    +        - banner
    +      site-media-types:
    +        - banner
    +      supported-vendors:
    +      vendor-id: 541
    +    usersync:
    +      url: https://match.deepintent.com/usersync/136?id=unk&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
    +      redirect-url: /setuid?bidder=deepintent&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=${DI_USER_ID}
    +      cookie-family-name: deepintent
    +      type: iframe
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/dmx.yaml b/src/main/resources/bidder-config/dmx.yaml
    index f7e58b68b80..4bab8e05e2c 100644
    --- a/src/main/resources/bidder-config/dmx.yaml
    +++ b/src/main/resources/bidder-config/dmx.yaml
    @@ -6,9 +6,9 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
    -      maintainer-email: steve@districtm.net
    +      maintainer-email: prebid@districtm.net
           app-media-types:
             - banner
             - video
    @@ -18,8 +18,8 @@ adapters:
           supported-vendors:
           vendor-id: 144
         usersync:
    -      url: https://dmx.districtm.io/s/v1/img/s/10007?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&redirect=
    -      redirect-url: /setuid?bidder=datablocks&gdpr=${gdpr}&gdpr_consent=${gdpr_consent}&uid=${uid}
    +      url:
    +      redirect-url:
           cookie-family-name: dmx
           type: redirect
           support-cors: false
    diff --git a/src/main/resources/bidder-config/emxdigital.yaml b/src/main/resources/bidder-config/emxdigital.yaml
    index 3a2b11f7291..a734fedc8e3 100644
    --- a/src/main/resources/bidder-config/emxdigital.yaml
    +++ b/src/main/resources/bidder-config/emxdigital.yaml
    @@ -6,12 +6,15 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: adops@emxdigital.com
           app-media-types:
    +        - banner
    +        - video
           site-media-types:
             - banner
    +        - video
           supported-vendors:
           vendor-id: 183
         usersync:
    diff --git a/src/main/resources/bidder-config/engagebdr.yaml b/src/main/resources/bidder-config/engagebdr.yaml
    index d6fdf49f6fd..e3a4237e80c 100644
    --- a/src/main/resources/bidder-config/engagebdr.yaml
    +++ b/src/main/resources/bidder-config/engagebdr.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: tech@engagebdr.com
           app-media-types:
    @@ -17,8 +17,8 @@ adapters:
           supported-vendors:
           vendor-id: 62
         usersync:
    -      url: https://match.bnmla.com/usersync/s2s_sync?gdpr={{gdpr}}&gdpr_consent={gdpr_consent}}&us_privacy={{us_privacy}}&r=
    -      redirect-url: /setuid?bidder=visx&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={UUID}
    +      url: https://match.bnmla.com/usersync?sspid=1000363&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
    +      redirect-url: /setuid?bidder=engagebdr&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={UUID}
           cookie-family-name: engagebdr
           type: iframe
           support-cors: false
    diff --git a/src/main/resources/bidder-config/eplanning.yaml b/src/main/resources/bidder-config/eplanning.yaml
    index 4a0e76766fd..6e316e04cfb 100644
    --- a/src/main/resources/bidder-config/eplanning.yaml
    +++ b/src/main/resources/bidder-config/eplanning.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: producto@e-planning.net
           app-media-types:
    diff --git a/src/main/resources/bidder-config/epom.yaml b/src/main/resources/bidder-config/epom.yaml
    new file mode 100644
    index 00000000000..b633dec1b31
    --- /dev/null
    +++ b/src/main/resources/bidder-config/epom.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  epom:
    +    enabled: false
    +    endpoint: https://an.epom.com/ortb
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support@epom.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 849
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: epom
    +      type: iframe
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/evolution.yaml b/src/main/resources/bidder-config/evolution.yaml
    new file mode 100644
    index 00000000000..8691d3be0ba
    --- /dev/null
    +++ b/src/main/resources/bidder-config/evolution.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  e_volution:
    +    enabled: false
    +    endpoint: http://service.e-volution.ai/pbserver
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: { }
    +    meta-info:
    +      maintainer-email: admin@e-volution.ai
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 957
    +    usersync:
    +      url: https://sync.e-volution.ai/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&redirect=
    +      redirect-url: /setuid?bidder=e_volution&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[UID]
    +      cookie-family-name: e_volution
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/facebook.yaml b/src/main/resources/bidder-config/facebook.yaml
    index b27bb386cb7..5d6c22a9928 100644
    --- a/src/main/resources/bidder-config/facebook.yaml
    +++ b/src/main/resources/bidder-config/facebook.yaml
    @@ -4,11 +4,12 @@ adapters:
         endpoint: https://an.facebook.com/placementbid.ortb
         platform-id:
         app-secret:
    +    timeout-notification-url-template: https://www.facebook.com/audiencenetwork/nurl/?partner=%s&app=%s&auction=%s&ortb_loss_code=2
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: none
           app-media-types:
    diff --git a/src/main/resources/bidder-config/gamma.yaml b/src/main/resources/bidder-config/gamma.yaml
    index 26646687c60..47ff4c491f9 100644
    --- a/src/main/resources/bidder-config/gamma.yaml
    +++ b/src/main/resources/bidder-config/gamma.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: support@gammassp.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/gamoshi.yaml b/src/main/resources/bidder-config/gamoshi.yaml
    index 397d77f6682..e0ab8775717 100644
    --- a/src/main/resources/bidder-config/gamoshi.yaml
    +++ b/src/main/resources/bidder-config/gamoshi.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: dev@gamoshi.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/grid.yaml b/src/main/resources/bidder-config/grid.yaml
    index 126bd47b72f..f8cfb77b6c5 100644
    --- a/src/main/resources/bidder-config/grid.yaml
    +++ b/src/main/resources/bidder-config/grid.yaml
    @@ -1,12 +1,12 @@
     adapters:
       grid:
         enabled: false
    -    endpoint: http://grid.bidswitch.net/sp_bid?sp=prebid
    +    endpoint: https://grid.bidswitch.net/sp_bid?sp=prebid
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: grid-tech@themediagrid.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/gumgum.yaml b/src/main/resources/bidder-config/gumgum.yaml
    index 275c725872c..eb8bf5fb9e5 100644
    --- a/src/main/resources/bidder-config/gumgum.yaml
    +++ b/src/main/resources/bidder-config/gumgum.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@gumgum.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/improvedigital.yaml b/src/main/resources/bidder-config/improvedigital.yaml
    index 776c1565851..b4ee74bd8c2 100644
    --- a/src/main/resources/bidder-config/improvedigital.yaml
    +++ b/src/main/resources/bidder-config/improvedigital.yaml
    @@ -6,15 +6,17 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: hb@azerion.com
           app-media-types:
             - banner
             - video
    +        - native
           site-media-types:
             - banner
             - video
    +        - native
           supported-vendors:
           vendor-id: 253
         usersync:
    diff --git a/src/main/resources/bidder-config/inmobi.yaml b/src/main/resources/bidder-config/inmobi.yaml
    index 275968871a7..583a1478b6f 100644
    --- a/src/main/resources/bidder-config/inmobi.yaml
    +++ b/src/main/resources/bidder-config/inmobi.yaml
    @@ -6,18 +6,20 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
    -      maintainer-email: prebid-support@inmobi.com
    +      maintainer-email: technology-irv@inmobi.com
           app-media-types:
             - banner
             - video
           site-media-types:
    +        - banner
    +        - video
           supported-vendors:
           vendor-id: 333
         usersync:
    -      url:
    -      redirect-url:
    +      url: https://id5-sync.com/i/495/0.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&callback=
    +      redirect-url: /setuid?bidder=inmobi&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={ID5UID}
           cookie-family-name: inmobi
           type: redirect
           support-cors: false
    diff --git a/src/main/resources/bidder-config/interactiveoffers.yaml b/src/main/resources/bidder-config/interactiveoffers.yaml
    new file mode 100644
    index 00000000000..048eb617660
    --- /dev/null
    +++ b/src/main/resources/bidder-config/interactiveoffers.yaml
    @@ -0,0 +1,23 @@
    +adapters:
    +  interactiveoffers:
    +    enabled: false
    +    endpoint: https://prebid-server.ioadx.com/bidRequest/?partnerId={{PartnerId}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: dev@interactiveoffers.com
    +      app-media-types:
    +        - banner
    +      site-media-types:
    +        - banner
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: interactiveoffers
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/invibes.yaml b/src/main/resources/bidder-config/invibes.yaml
    new file mode 100644
    index 00000000000..f133b423930
    --- /dev/null
    +++ b/src/main/resources/bidder-config/invibes.yaml
    @@ -0,0 +1,21 @@
    +adapters:
    +  invibes:
    +    enabled: false
    +    endpoint: https://{{Host}}/bid/ServerBidAdContent
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: system_operations@invibes.com
    +      site-media-types:
    +        - banner
    +      supported-vendors:
    +      vendor-id: 436
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: invibes
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/ix.yaml b/src/main/resources/bidder-config/ix.yaml
    index 6bf899e5284..1120c6119c4 100644
    --- a/src/main/resources/bidder-config/ix.yaml
    +++ b/src/main/resources/bidder-config/ix.yaml
    @@ -1,21 +1,28 @@
     adapters:
       ix:
         enabled: false
    -    endpoint: http://rubicon-us-east.lb.indexww.com/transbidder?p=189517
    +    endpoint: https://
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names: indexExchange
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: pdu-supply-prebid@indexexchange.com
           app-media-types:
    +        - banner
    +        - video
    +        - native
    +        - audio
           site-media-types:
             - banner
    +        - video
    +        - native
    +        - audio
           supported-vendors:
           vendor-id: 10
         usersync:
    -      url: https://ssum.casalemedia.com/usermatchredir?s=189517&cb=
    +      url: https://ssum.casalemedia.com/usermatchredir?s=189517&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb=
           redirect-url: /setuid?bidder=ix&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=
           cookie-family-name: ix
           type: redirect
    diff --git a/src/main/resources/bidder-config/jixie.yaml b/src/main/resources/bidder-config/jixie.yaml
    new file mode 100644
    index 00000000000..b88c2357550
    --- /dev/null
    +++ b/src/main/resources/bidder-config/jixie.yaml
    @@ -0,0 +1,23 @@
    +adapters:
    +  jixie:
    +    enabled: false
    +    endpoint: https://hb.jixie.io/v2/hbsvrpost
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: contact@jixie.io
    +      app-media-types:
    +      site-media-types:
    +        - banner
    +        - video
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url: https://id.jixie.io/api/sync?pid=&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect=
    +      redirect-url: /setuid?bidder=jixie&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=%%JXUID%%
    +      cookie-family-name: jixie
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/kayzen.yaml b/src/main/resources/bidder-config/kayzen.yaml
    new file mode 100644
    index 00000000000..025280db1f8
    --- /dev/null
    +++ b/src/main/resources/bidder-config/kayzen.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  kayzen:
    +    enabled: false
    +    endpoint: https://bids-{{ZoneID}}.bidder.kayzen.io/?exchange={{AccountID}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: platform-dev@kayzen.io
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 528
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: kayzen
    +      type: redirect
    +      support-cors: false
    \ No newline at end of file
    diff --git a/src/main/resources/bidder-config/kidoz.yaml b/src/main/resources/bidder-config/kidoz.yaml
    index b014675efd0..5474b27c725 100644
    --- a/src/main/resources/bidder-config/kidoz.yaml
    +++ b/src/main/resources/bidder-config/kidoz.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid-support@kidoz.net
           app-media-types:
    diff --git a/src/main/resources/bidder-config/krushmedia.yaml b/src/main/resources/bidder-config/krushmedia.yaml
    new file mode 100644
    index 00000000000..cc1f8ddd784
    --- /dev/null
    +++ b/src/main/resources/bidder-config/krushmedia.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  krushmedia:
    +    enabled: false
    +    endpoint: http://ads4.krushmedia.com/?c=rtb&m=req&key={{AccountID}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: adapter@krushmedia.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url: https://cs.krushmedia.com/4e4abdd5ecc661643458a730b1aa927d.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
    +      redirect-url: /setuid?bidder=krushmedia&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[uid]
    +      cookie-family-name: krushmedia
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/kubient.yaml b/src/main/resources/bidder-config/kubient.yaml
    index dd602011e69..2dc8e27374d 100644
    --- a/src/main/resources/bidder-config/kubient.yaml
    +++ b/src/main/resources/bidder-config/kubient.yaml
    @@ -6,9 +6,9 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
    -      maintainer-email: support@kubient.com
    +      maintainer-email: prebid@kubient.com
           app-media-types:
             - banner
             - video
    @@ -16,7 +16,7 @@ adapters:
             - banner
             - video
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 794
         usersync:
           url:
           redirect-url:
    diff --git a/src/main/resources/bidder-config/lifestreet.yaml b/src/main/resources/bidder-config/lifestreet.yaml
    deleted file mode 100644
    index 446a6f79b03..00000000000
    --- a/src/main/resources/bidder-config/lifestreet.yaml
    +++ /dev/null
    @@ -1,24 +0,0 @@
    -adapters:
    -  lifestreet:
    -    enabled: false
    -    endpoint: https://prebid.s2s.lfstmedia.com/adrequest
    -    pbs-enforces-gdpr: true
    -    pbs-enforces-ccpa: true
    -    modifying-vast-xml-allowed: true
    -    deprecated-names:
    -    aliases:
    -    meta-info:
    -      maintainer-email: mobile.tech@lifestreet.com
    -      app-media-types:
    -        - banner
    -      site-media-types:
    -        - banner
    -        - video
    -      supported-vendors:
    -      vendor-id: 67
    -    usersync:
    -      url: https://ads.lfstmedia.com/idsync/137062?synced=1&ttl=1s&rurl=
    -      redirect-url: /setuid?bidder=lifestreet&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$$visitor_cookie$$
    -      cookie-family-name: lifestreet
    -      type: redirect
    -      support-cors: false
    diff --git a/src/main/resources/bidder-config/lockerdome.yaml b/src/main/resources/bidder-config/lockerdome.yaml
    index b693b2c8489..a3636da97fd 100644
    --- a/src/main/resources/bidder-config/lockerdome.yaml
    +++ b/src/main/resources/bidder-config/lockerdome.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: bidding@lockerdome.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/logicad.yaml b/src/main/resources/bidder-config/logicad.yaml
    index 9761a40a805..27f6e6388eb 100644
    --- a/src/main/resources/bidder-config/logicad.yaml
    +++ b/src/main/resources/bidder-config/logicad.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@so-netmedia.jp
           app-media-types:
    @@ -14,7 +14,7 @@ adapters:
           site-media-types:
             - banner
           supported-vendors:
    -      vendor-id: 14
    +      vendor-id: 0
         usersync:
           url: https://cr-p31.ladsp.jp/cookiesender/31?r=true&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ru=
           redirect-url: /setuid?bidder=logicad&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID
    diff --git a/src/main/resources/bidder-config/loopme.yaml b/src/main/resources/bidder-config/loopme.yaml
    new file mode 100644
    index 00000000000..312e042579e
    --- /dev/null
    +++ b/src/main/resources/bidder-config/loopme.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  loopme:
    +    enabled: false
    +    endpoint: http://prebid-eu.loopmertb.com
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support@loopme.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 109
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: loopme
    +      type: redirect
    +      support-cors: false
    \ No newline at end of file
    diff --git a/src/main/resources/bidder-config/lunamedia.yaml b/src/main/resources/bidder-config/lunamedia.yaml
    index 04a58ecc482..27be0aa060a 100644
    --- a/src/main/resources/bidder-config/lunamedia.yaml
    +++ b/src/main/resources/bidder-config/lunamedia.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: josh@lunamedia.io
           app-media-types:
    diff --git a/src/main/resources/bidder-config/madvertise.yaml b/src/main/resources/bidder-config/madvertise.yaml
    new file mode 100644
    index 00000000000..b12c0084457
    --- /dev/null
    +++ b/src/main/resources/bidder-config/madvertise.yaml
    @@ -0,0 +1,24 @@
    +adapters:
    +  madvertise:
    +    enabled: false
    +    endpoint: https://mobile.mng-ads.com/bidrequest{{ZoneID}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support@madvertise.com
    +      app-media-types:
    +        - banner
    +        - video
    +      site-media-types:
    +        - banner
    +      supported-vendors:
    +      vendor-id: 153
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: madvertise
    +      type: redirect
    +      support-cors: false
    \ No newline at end of file
    diff --git a/src/main/resources/bidder-config/marsmedia.yaml b/src/main/resources/bidder-config/marsmedia.yaml
    index d6098c1cff8..2bbe9706c5b 100644
    --- a/src/main/resources/bidder-config/marsmedia.yaml
    +++ b/src/main/resources/bidder-config/marsmedia.yaml
    @@ -1,12 +1,12 @@
     adapters:
       marsmedia:
         enabled: false
    -    endpoint: https://bid306.rtbsrv.com/bidder/?bid=f3xtet
    +    endpoint: https://bidd.rtbsrv.com/bidder/?bid=f3xtet
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@mars.media
           app-media-types:
    @@ -16,10 +16,10 @@ adapters:
             - banner
             - video
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 776
         usersync:
           url: https://dmp.rtbsrv.com/dmp/profiles/cm?p_id=179&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect=
           redirect-url: /setuid?bidder=marsmedia&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=${UUID}
           cookie-family-name: marsmedia
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/medianet.yaml b/src/main/resources/bidder-config/medianet.yaml
    new file mode 100644
    index 00000000000..af4b1310752
    --- /dev/null
    +++ b/src/main/resources/bidder-config/medianet.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  medianet:
    +    enabled: false
    +    endpoint: https://prebid-adapter.media.net/rtb/pb/prebids2s?src={{PREBID_SERVER_ENDPOINT}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: prebid@media.net
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 142
    +    usersync:
    +      url: https://hbx.media.net/cksync.php?cs=1&type=pbs&ovsid=setstatuscode&redirect=
    +      redirect-url: /setuid?bidder=medianet&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=
    +      cookie-family-name: medianet
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/mgid.yaml b/src/main/resources/bidder-config/mgid.yaml
    index 091085b4b56..54f0e4b83a7 100644
    --- a/src/main/resources/bidder-config/mgid.yaml
    +++ b/src/main/resources/bidder-config/mgid.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@mgid.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/mobfoxpb.yaml b/src/main/resources/bidder-config/mobfoxpb.yaml
    new file mode 100644
    index 00000000000..138de40585f
    --- /dev/null
    +++ b/src/main/resources/bidder-config/mobfoxpb.yaml
    @@ -0,0 +1,25 @@
    +adapters:
    +  mobfoxpb:
    +    enabled: false
    +    endpoint: http://bes.mobfox.com/?c=__route__&m=__method__&key=__key__
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: platform@mobfox.com
    +      app-media-types:
    +      - banner
    +      - video
    +      site-media-types:
    +      - banner
    +      - video
    +      supported-vendors:
    +      vendor-id: 311
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: mobfoxpb
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/mobilefuse.yaml b/src/main/resources/bidder-config/mobilefuse.yaml
    index f612bda31e6..c894ec28e5d 100644
    --- a/src/main/resources/bidder-config/mobilefuse.yaml
    +++ b/src/main/resources/bidder-config/mobilefuse.yaml
    @@ -1,12 +1,12 @@
     adapters:
       mobilefuse:
         enabled: false
    -    endpoint: http://mfx-us-east.mobilefuse.com/openrtb?pub_id=
    +    endpoint: http://mfx.mobilefuse.com/openrtb?pub_id=
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@mobilefuse.com
           app-media-types:
    @@ -14,10 +14,10 @@ adapters:
             - video
           site-media-types:
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 909
         usersync:
           url:
           redirect-url:
           cookie-family-name: mobilefuse
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/nanointeractive.yaml b/src/main/resources/bidder-config/nanointeractive.yaml
    index 04543d81ac9..5ed47efd535 100644
    --- a/src/main/resources/bidder-config/nanointeractive.yaml
    +++ b/src/main/resources/bidder-config/nanointeractive.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: development@nanointeractive.com
           app-media-types:
    @@ -14,10 +14,10 @@ adapters:
           site-media-types:
             - banner
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 72
         usersync:
           url: https://ad.audiencemanager.de/hbs/cookie_sync?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirectUri=
           redirect-url: /setuid?bidder=nanointeractive&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID
           cookie-family-name: nanointeractive
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/ninthdecimal.yaml b/src/main/resources/bidder-config/ninthdecimal.yaml
    index ac27d2b59c7..2b6f6790fd3 100644
    --- a/src/main/resources/bidder-config/ninthdecimal.yaml
    +++ b/src/main/resources/bidder-config/ninthdecimal.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: abudig@ninthdecimal.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/nobid.yaml b/src/main/resources/bidder-config/nobid.yaml
    new file mode 100644
    index 00000000000..b464cf5dfa7
    --- /dev/null
    +++ b/src/main/resources/bidder-config/nobid.yaml
    @@ -0,0 +1,25 @@
    +adapters:
    +  nobid:
    +    enabled: false
    +    endpoint: https://ads.servenobid.com/ortb_adreq?tek=pbs&ver=1
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: developers@nobid.io
    +      app-media-types:
    +        - banner
    +        - video
    +      site-media-types:
    +        - banner
    +        - video
    +      supported-vendors:
    +      vendor-id: 816
    +    usersync:
    +      url: https://ads.servenobid.com/getsync?tek=pbs&ver=1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect=
    +      redirect-url: /setuid?bidder=nobid&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID
    +      cookie-family-name: nobid
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/onetag.yaml b/src/main/resources/bidder-config/onetag.yaml
    new file mode 100644
    index 00000000000..ad6a5be202b
    --- /dev/null
    +++ b/src/main/resources/bidder-config/onetag.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  onetag:
    +    enabled: false
    +    endpoint: https://prebid-server.onetag-sys.com/prebid-server/{{publisherId}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: devops@onetag.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 241
    +    usersync:
    +      url: https://onetag-sys.com/usync/?redir=
    +      redirect-url: /setuid?bidder=onetag&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=${USER_TOKEN}
    +      cookie-family-name: onetag
    +      type: iframe
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/openx.yaml b/src/main/resources/bidder-config/openx.yaml
    index fd1064c5142..9f52c68146c 100644
    --- a/src/main/resources/bidder-config/openx.yaml
    +++ b/src/main/resources/bidder-config/openx.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@openx.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/orbidder.yaml b/src/main/resources/bidder-config/orbidder.yaml
    index 846ecd38f6e..612d24275bf 100644
    --- a/src/main/resources/bidder-config/orbidder.yaml
    +++ b/src/main/resources/bidder-config/orbidder.yaml
    @@ -6,15 +6,14 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: realtime-siggi@otto.de
           app-media-types:
             - banner
           site-media-types:
    -        - banner
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 559
         usersync:
           url:
           redirect-url:
    diff --git a/src/main/resources/bidder-config/outbrain.yaml b/src/main/resources/bidder-config/outbrain.yaml
    new file mode 100644
    index 00000000000..40454e1d9aa
    --- /dev/null
    +++ b/src/main/resources/bidder-config/outbrain.yaml
    @@ -0,0 +1,25 @@
    +adapters:
    +  outbrain:
    +    enabled: false
    +    endpoint: https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: prog-ops-team@outbrain.com
    +      app-media-types:
    +        - banner
    +        - native
    +      site-media-types:
    +        - banner
    +        - native
    +      supported-vendors:
    +      vendor-id: 164
    +    usersync:
    +      url: https://prebidtest.zemanta.com/usersync/prebidtest?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb=
    +      redirect-url: /setuid?bidder=outbrain&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=__ZUID__
    +      cookie-family-name: outbrain
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/pangle.yaml b/src/main/resources/bidder-config/pangle.yaml
    new file mode 100644
    index 00000000000..2447d2edbb7
    --- /dev/null
    +++ b/src/main/resources/bidder-config/pangle.yaml
    @@ -0,0 +1,24 @@
    +adapters:
    +  pangle:
    +    enabled: false
    +    endpoint: https://
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: pangle_dsp@bytedance.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: pangle
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/pubmatic.yaml b/src/main/resources/bidder-config/pubmatic.yaml
    index a9542884382..cf8c21b9206 100644
    --- a/src/main/resources/bidder-config/pubmatic.yaml
    +++ b/src/main/resources/bidder-config/pubmatic.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: header-bidding@pubmatic.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/pubnative.yaml b/src/main/resources/bidder-config/pubnative.yaml
    index 3d5fab4bad2..1774325bb29 100644
    --- a/src/main/resources/bidder-config/pubnative.yaml
    +++ b/src/main/resources/bidder-config/pubnative.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: product@pubnative.net
           app-media-types:
    @@ -18,10 +18,10 @@ adapters:
             - video
             - native
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 512
         usersync:
           url:
           redirect-url:
           cookie-family-name: pubnative
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/pulsepoint.yaml b/src/main/resources/bidder-config/pulsepoint.yaml
    index d6389d1323b..3aae50737f2 100644
    --- a/src/main/resources/bidder-config/pulsepoint.yaml
    +++ b/src/main/resources/bidder-config/pulsepoint.yaml
    @@ -6,13 +6,19 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: ExchangeTeam@pulsepoint.com
           app-media-types:
             - banner
    +        - video
    +        - audio
    +        - native
           site-media-types:
             - banner
    +        - video
    +        - audio
    +        - native
           supported-vendors:
           vendor-id: 81
         usersync:
    diff --git a/src/main/resources/bidder-config/revcontent.yaml b/src/main/resources/bidder-config/revcontent.yaml
    new file mode 100644
    index 00000000000..7783b2ba26d
    --- /dev/null
    +++ b/src/main/resources/bidder-config/revcontent.yaml
    @@ -0,0 +1,25 @@
    +adapters:
    +  revcontent:
    +    enabled: false
    +    endpoint: https://trends.revcontent.com/rtb
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: developers@revcontent.com
    +      app-media-types:
    +        - banner
    +        - native
    +      site-media-types:
    +        - banner
    +        - native
    +      supported-vendors:
    +      vendor-id: 203
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: revcontent
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/rhythmone.yaml b/src/main/resources/bidder-config/rhythmone.yaml
    index dea2ebc3a3f..5b65221b008 100644
    --- a/src/main/resources/bidder-config/rhythmone.yaml
    +++ b/src/main/resources/bidder-config/rhythmone.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: support@rhythmone.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/rtbhouse.yaml b/src/main/resources/bidder-config/rtbhouse.yaml
    index b1ea36d685a..5e59957c9c3 100644
    --- a/src/main/resources/bidder-config/rtbhouse.yaml
    +++ b/src/main/resources/bidder-config/rtbhouse.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@rtbhouse.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/rubicon.yaml b/src/main/resources/bidder-config/rubicon.yaml
    index 81d1e0b82b2..95b53acc19b 100644
    --- a/src/main/resources/bidder-config/rubicon.yaml
    +++ b/src/main/resources/bidder-config/rubicon.yaml
    @@ -5,7 +5,7 @@ adapters:
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         modifying-vast-xml-allowed: true
         generate-bid-id: false
         XAPI:
    diff --git a/src/main/resources/bidder-config/salunamedia.yaml b/src/main/resources/bidder-config/salunamedia.yaml
    new file mode 100644
    index 00000000000..55518cc6c2d
    --- /dev/null
    +++ b/src/main/resources/bidder-config/salunamedia.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  sa_lunamedia:
    +    enabled: false
    +    endpoint: http://balancer.lmgssp.com/pserver
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support@lunamedia.io
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 998
    +    usersync:
    +      url: https://cookie.lmgssp.com/pserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&redirect=
    +      redirect-url: /setuid?bidder=sa_lunamedia&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[UID]
    +      cookie-family-name: sa_lunamedia
    +      type: redirect
    +      support-cors: false
    \ No newline at end of file
    diff --git a/src/main/resources/bidder-config/sharethrough.yaml b/src/main/resources/bidder-config/sharethrough.yaml
    index f0433dc101e..13ac86dc7d0 100644
    --- a/src/main/resources/bidder-config/sharethrough.yaml
    +++ b/src/main/resources/bidder-config/sharethrough.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: pubgrowth.engineering@sharethrough.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/silvermob.yaml b/src/main/resources/bidder-config/silvermob.yaml
    new file mode 100644
    index 00000000000..e5a7a392467
    --- /dev/null
    +++ b/src/main/resources/bidder-config/silvermob.yaml
    @@ -0,0 +1,24 @@
    +adapters:
    +  silvermob:
    +    enabled: false
    +    endpoint: http://{{Host}}.silvermob.com/marketplace/api/dsp/bid/{{ZoneID}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support@silvermob.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: silvermob
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/smaato.yaml b/src/main/resources/bidder-config/smaato.yaml
    index bfacf24c294..d5dbe5fc20a 100644
    --- a/src/main/resources/bidder-config/smaato.yaml
    +++ b/src/main/resources/bidder-config/smaato.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@smaato.com
           app-media-types:
    @@ -16,10 +16,10 @@ adapters:
             - banner
             - video
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 82
         usersync:
    -      url:
    -      redirect-url:
    +      url: https://s.ad.smaato.net/c/?adExInit=n&redir=
    +      redirect-url: /setuid?bidder=smaato&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID
           cookie-family-name: smaato
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/smartadserver.yaml b/src/main/resources/bidder-config/smartadserver.yaml
    index edb90522bc1..8ba06f8c65b 100644
    --- a/src/main/resources/bidder-config/smartadserver.yaml
    +++ b/src/main/resources/bidder-config/smartadserver.yaml
    @@ -1,12 +1,12 @@
     adapters:
       smartadserver:
         enabled: false
    -    endpoint: https://ssb.smartadserver.com
    +    endpoint: https://ssb-global.smartadserver.com
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: support@smartadserver.com
           app-media-types:
    @@ -16,9 +16,9 @@ adapters:
             - banner
             - video
           supported-vendors:
    -      vendor-id: 328
    +      vendor-id: 45
         usersync:
    -      url: https://ssbsync.smartadserver.com/api/sync?callerId=5&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirectUri=
    +      url: https://ssbsync-global.smartadserver.com/api/sync?callerId=5&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirectUri=
           redirect-url: /setuid?bidder=smartadserver&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=[ssb_sync_pid]
           cookie-family-name: smartadserver
           type: redirect
    diff --git a/src/main/resources/bidder-config/smartrtb.yaml b/src/main/resources/bidder-config/smartrtb.yaml
    index 938ab996713..c5d9e0a1a68 100644
    --- a/src/main/resources/bidder-config/smartrtb.yaml
    +++ b/src/main/resources/bidder-config/smartrtb.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: "engineering@smrtb.com"
           app-media-types:
    diff --git a/src/main/resources/bidder-config/smartyads.yaml b/src/main/resources/bidder-config/smartyads.yaml
    new file mode 100644
    index 00000000000..f09be44799e
    --- /dev/null
    +++ b/src/main/resources/bidder-config/smartyads.yaml
    @@ -0,0 +1,27 @@
    +adapters:
    +  smartyads:
    +    enabled: false
    +    endpoint: http://{{Host}}.smartyads.com/bid?rtb_seat_id={{SourceId}}&secret_key={{AccountID}}
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: support@smartyads.com
    +      app-media-types:
    +        - banner
    +        - video
    +        - native
    +      site-media-types:
    +        - banner
    +        - video
    +        - native
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url: https://us.ck-ie.com/yhsfle286.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
    +      redirect-url: /setuid?bidder=smartyads&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={$PARTNER_UID}
    +      cookie-family-name: smartyads
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/smilewanted.yaml b/src/main/resources/bidder-config/smilewanted.yaml
    new file mode 100644
    index 00000000000..04d57e66cbe
    --- /dev/null
    +++ b/src/main/resources/bidder-config/smilewanted.yaml
    @@ -0,0 +1,25 @@
    +adapters:
    +  smilewanted:
    +    enabled: false
    +    endpoint: http://prebid-server.smilewanted.com
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: { }
    +    meta-info:
    +      maintainer-email: tech@smilewanted.com
    +      app-media-types:
    +        - banner
    +        - video
    +      site-media-types:
    +        - banner
    +        - video
    +      supported-vendors:
    +      vendor-id: 639
    +    usersync:
    +      url: https://csync.smilewanted.com/getuid?source=prebid-server&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect=
    +      redirect-url: /setuid?bidder=smilewanted&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID
    +      cookie-family-name: smilewanted
    +      type: redirect
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/somoaudience.yaml b/src/main/resources/bidder-config/somoaudience.yaml
    index afde7ccb5b4..abe0c21c7f8 100644
    --- a/src/main/resources/bidder-config/somoaudience.yaml
    +++ b/src/main/resources/bidder-config/somoaudience.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: publishers@somoaudience.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/sonobi.yaml b/src/main/resources/bidder-config/sonobi.yaml
    index a1fc1702850..226db071560 100644
    --- a/src/main/resources/bidder-config/sonobi.yaml
    +++ b/src/main/resources/bidder-config/sonobi.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: apex.prebid@sonobi.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/sovrn.yaml b/src/main/resources/bidder-config/sovrn.yaml
    index a588d16ed8c..be30f724803 100644
    --- a/src/main/resources/bidder-config/sovrn.yaml
    +++ b/src/main/resources/bidder-config/sovrn.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: sovrnoss@sovrn.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/synacormedia.yaml b/src/main/resources/bidder-config/synacormedia.yaml
    index b82564e32b0..88c00ec13f8 100644
    --- a/src/main/resources/bidder-config/synacormedia.yaml
    +++ b/src/main/resources/bidder-config/synacormedia.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: eng-demand@synacor.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/tappx.yaml b/src/main/resources/bidder-config/tappx.yaml
    index f0e3541dd0a..1b87ce81278 100644
    --- a/src/main/resources/bidder-config/tappx.yaml
    +++ b/src/main/resources/bidder-config/tappx.yaml
    @@ -6,18 +6,20 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: tappx@tappx.com
           app-media-types:
    +        - banner
    +        - video
           site-media-types:
             - banner
             - video
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 628
         usersync:
    -      url:
    -      redirect-url:
    +      url: https://ssp.api.tappx.com/cs/usersync.php?gdpr_optin={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&type=iframe&ruid=
    +      redirect-url: /setuid?bidder=tappx&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={{TPPXUID}}
           cookie-family-name: tappx
    -      type: redirect
    +      type: iframe
           support-cors: false
    diff --git a/src/main/resources/bidder-config/telaria.yaml b/src/main/resources/bidder-config/telaria.yaml
    index 0de53e17589..6dc41133697 100644
    --- a/src/main/resources/bidder-config/telaria.yaml
    +++ b/src/main/resources/bidder-config/telaria.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: github@telaria.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/triplelift.yaml b/src/main/resources/bidder-config/triplelift.yaml
    index 4074ae75ff1..ac984af1295 100644
    --- a/src/main/resources/bidder-config/triplelift.yaml
    +++ b/src/main/resources/bidder-config/triplelift.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@triplelift.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/tripleliftnative.yaml b/src/main/resources/bidder-config/tripleliftnative.yaml
    index 2b7856cec04..5b0fac21483 100644
    --- a/src/main/resources/bidder-config/tripleliftnative.yaml
    +++ b/src/main/resources/bidder-config/tripleliftnative.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@triplelift.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/ttx.yaml b/src/main/resources/bidder-config/ttx.yaml
    index 53b5dc71c91..1a61d3122b8 100644
    --- a/src/main/resources/bidder-config/ttx.yaml
    +++ b/src/main/resources/bidder-config/ttx.yaml
    @@ -1,18 +1,21 @@
     adapters:
       ttx:
         enabled: false
    +    # Old http://ssc.33across.com/api/v1/hb
         endpoint: https://ssc.33across.com/api/v1/s2s
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases: 33across
    +    aliases:
    +      '33across':
    +        enabled: false
         meta-info:
           maintainer-email: headerbidding@33across.com
           app-media-types:
    -        - banner
           site-media-types:
             - banner
    +        - video
           supported-vendors:
           vendor-id: 58
         usersync:
    diff --git a/src/main/resources/bidder-config/ucfunnel.yaml b/src/main/resources/bidder-config/ucfunnel.yaml
    index 9f229be6aee..1cd80e39319 100644
    --- a/src/main/resources/bidder-config/ucfunnel.yaml
    +++ b/src/main/resources/bidder-config/ucfunnel.yaml
    @@ -1,12 +1,12 @@
     adapters:
       ucfunnel:
         enabled: false
    -    endpoint: http://apac-hk-adx.aralego.com/prebid
    +    endpoint: https://pbs.aralego.com/prebid
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: "support@ucfunnel.com"
           app-media-types:
    @@ -16,10 +16,10 @@ adapters:
             - banner
             - video
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 607
         usersync:
           url: https://sync.aralego.com/idsync?gdpr={{gdpr}}}&gdpr_consent={{gdpr_consent}}&usprivacy={{us_privacy}}&redirect=
           redirect-url: /setuid?bidder=ucfunnel&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=SspCookieUserId
           cookie-family-name: ucfunnel
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/unicorn.yaml b/src/main/resources/bidder-config/unicorn.yaml
    new file mode 100644
    index 00000000000..116ea7d0244
    --- /dev/null
    +++ b/src/main/resources/bidder-config/unicorn.yaml
    @@ -0,0 +1,22 @@
    +adapters:
    +  unicorn:
    +    enabled: false
    +    endpoint: https://ds.uncn.jp/pb/0/bid.json
    +    pbs-enforces-gdpr: true
    +    pbs-enforces-ccpa: true
    +    modifying-vast-xml-allowed: true
    +    deprecated-names:
    +    aliases: {}
    +    meta-info:
    +      maintainer-email: prebid@unicorn.inc
    +      app-media-types:
    +        - banner
    +      site-media-types:
    +      supported-vendors:
    +      vendor-id: 0
    +    usersync:
    +      url:
    +      redirect-url:
    +      cookie-family-name: unicorn
    +      type: iframe
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/unruly.yaml b/src/main/resources/bidder-config/unruly.yaml
    index 0304e40e7cc..87b01e96dcb 100644
    --- a/src/main/resources/bidder-config/unruly.yaml
    +++ b/src/main/resources/bidder-config/unruly.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: adspaces@unrulygroup.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/valueimpression.yaml b/src/main/resources/bidder-config/valueimpression.yaml
    index e14ee9294c5..fbe3a5e9a34 100644
    --- a/src/main/resources/bidder-config/valueimpression.yaml
    +++ b/src/main/resources/bidder-config/valueimpression.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: info@valueimpression.com
           app-media-types:
    diff --git a/src/main/resources/bidder-config/verizonmedia.yaml b/src/main/resources/bidder-config/verizonmedia.yaml
    index f8cdbef1ef0..6f5be2c81a1 100644
    --- a/src/main/resources/bidder-config/verizonmedia.yaml
    +++ b/src/main/resources/bidder-config/verizonmedia.yaml
    @@ -1,22 +1,23 @@
     adapters:
       verizonmedia:
         enabled: false
    -    endpoint: https://
    +    endpoint: https://s2shb.ssp.yahoo.com/admax/bid/partners/MAG
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: dsp-supply-prebid@verizonmedia.com
           app-media-types:
    +        - banner
           site-media-types:
             - banner
           supported-vendors:
           vendor-id: 25
         usersync:
    -      url:
    +      url: https://ups.analytics.yahoo.com/ups/58401/sync?redir=true&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}
           redirect-url:
           cookie-family-name: verizonmedia
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/visx.yaml b/src/main/resources/bidder-config/visx.yaml
    index d5b231d56d5..45bf55e7cd3 100644
    --- a/src/main/resources/bidder-config/visx.yaml
    +++ b/src/main/resources/bidder-config/visx.yaml
    @@ -1,12 +1,12 @@
     adapters:
       visx:
         enabled: false
    -    endpoint: https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_standard
    +    endpoint: https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_java
         pbs-enforces-gdpr: true
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: supply.partners@yoc.com
           app-media-types:
    @@ -17,7 +17,7 @@ adapters:
           vendor-id: 154
         usersync:
           url: https://t.visx.net/s2s_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir=
    -      redirect-url: /setuid?bidder=visx&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={UUID}
    +      redirect-url: /setuid?bidder=visx&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=${UUID}
           cookie-family-name: visx
           type: redirect
           support-cors: false
    diff --git a/src/main/resources/bidder-config/vrtcal.yaml b/src/main/resources/bidder-config/vrtcal.yaml
    index 1ad90ba09e6..c9701c97652 100644
    --- a/src/main/resources/bidder-config/vrtcal.yaml
    +++ b/src/main/resources/bidder-config/vrtcal.yaml
    @@ -6,17 +6,17 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: support@vrtcal.com
           app-media-types:
             - banner
           site-media-types:
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 706
         usersync:
           url:
           redirect-url:
           cookie-family-name: vrtcal
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/yeahmobi.yaml b/src/main/resources/bidder-config/yeahmobi.yaml
    index 4e1a9eb6023..03d92c968fb 100644
    --- a/src/main/resources/bidder-config/yeahmobi.yaml
    +++ b/src/main/resources/bidder-config/yeahmobi.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: junping.zhao@yeahmobi.com
           app-media-types:
    @@ -15,7 +15,7 @@ adapters:
             - native
           site-media-types:
           supported-vendors:
    -      vendor-id: 0
    +      vendor-id: 803
         usersync:
           url:
           redirect-url:
    diff --git a/src/main/resources/bidder-config/yieldlab.yaml b/src/main/resources/bidder-config/yieldlab.yaml
    index a353396af6d..ca930938364 100644
    --- a/src/main/resources/bidder-config/yieldlab.yaml
    +++ b/src/main/resources/bidder-config/yieldlab.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: solutions@yieldlab.de
           app-media-types:
    diff --git a/src/main/resources/bidder-config/yieldmo.yaml b/src/main/resources/bidder-config/yieldmo.yaml
    index bb688f07526..6b4f8596a9f 100644
    --- a/src/main/resources/bidder-config/yieldmo.yaml
    +++ b/src/main/resources/bidder-config/yieldmo.yaml
    @@ -6,12 +6,15 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: prebid@yieldmo.com
           app-media-types:
    +        - banner
    +        - video
           site-media-types:
             - banner
    +        - video
           supported-vendors:
           vendor-id: 173
         usersync:
    @@ -19,4 +22,4 @@ adapters:
           redirect-url: /setuid?bidder=yieldmo&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid=$UID
           cookie-family-name: yieldmo
           type: redirect
    -      support-cors: false
    \ No newline at end of file
    +      support-cors: false
    diff --git a/src/main/resources/bidder-config/yieldone.yaml b/src/main/resources/bidder-config/yieldone.yaml
    index 43f615ed011..53b684e8359 100644
    --- a/src/main/resources/bidder-config/yieldone.yaml
    +++ b/src/main/resources/bidder-config/yieldone.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: y1dev@platform-one.co.jp
           app-media-types:
    diff --git a/src/main/resources/bidder-config/zeroclickfraud.yaml b/src/main/resources/bidder-config/zeroclickfraud.yaml
    index 0795fa800c1..93fb1b35644 100644
    --- a/src/main/resources/bidder-config/zeroclickfraud.yaml
    +++ b/src/main/resources/bidder-config/zeroclickfraud.yaml
    @@ -6,7 +6,7 @@ adapters:
         pbs-enforces-ccpa: true
         modifying-vast-xml-allowed: true
         deprecated-names:
    -    aliases:
    +    aliases: {}
         meta-info:
           maintainer-email: support@datablocks.net
           app-media-types:
    diff --git a/src/main/resources/static/bidder-params/acuityads.json b/src/main/resources/static/bidder-params/acuityads.json
    new file mode 100644
    index 00000000000..314a6cb5f8d
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/acuityads.json
    @@ -0,0 +1,19 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "AcuityAds Adapter Params",
    +  "description": "A schema which validates params accepted by the AcuityAds adapter",
    +  "type": "object",
    +  "properties": {
    +    "host": {
    +      "type": "string",
    +      "description": "Network host to send request",
    +      "minLength": 1
    +    },
    +    "accountid": {
    +      "type": "string",
    +      "description": "Account id",
    +      "minLength": 1
    +    }
    +  },
    +  "required": ["host", "accountid"]
    +}
    diff --git a/src/main/resources/static/bidder-params/adf.json b/src/main/resources/static/bidder-params/adf.json
    new file mode 100644
    index 00000000000..ae7d47ae629
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/adf.json
    @@ -0,0 +1,19 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Adf Adapter Params",
    +  "description": "A schema which validates params accepted by the adf adapter",
    +  "type": "object",
    +  "properties": {
    +    "mid": {
    +      "type": [
    +        "integer",
    +        "string"
    +      ],
    +      "pattern": "^\\d+$",
    +      "description": "An ID which identifies the placement selling the impression"
    +    }
    +  },
    +  "required": [
    +    "mid"
    +  ]
    +}
    diff --git a/src/main/resources/static/bidder-params/adform.json b/src/main/resources/static/bidder-params/adform.json
    index 8ed37752f61..0f4ad83bfdf 100644
    --- a/src/main/resources/static/bidder-params/adform.json
    +++ b/src/main/resources/static/bidder-params/adform.json
    @@ -31,7 +31,7 @@
         },
         "cdims": {
           "type": "string",
    -      "description": "Comma-separated creative dimentions.",
    +      "description": "Comma-separated creative dimensions.",
           "pattern": "(^\\d+x\\d+)(,\\d+x\\d+)*$"
         },
         "minp": {
    @@ -47,4 +47,4 @@
       "required": [
         "mid"
       ]
    -}
    \ No newline at end of file
    +}
    diff --git a/src/main/resources/static/bidder-params/adoppler.json b/src/main/resources/static/bidder-params/adoppler.json
    index 7bf55ec1250..eaa4e6df80e 100644
    --- a/src/main/resources/static/bidder-params/adoppler.json
    +++ b/src/main/resources/static/bidder-params/adoppler.json
    @@ -7,6 +7,10 @@
         "adunit": {
           "type": "string",
           "description": "AdUnit to bid against to."
    +    },
    +    "client": {
    +      "type": "string",
    +      "description": "Client name."
         }
       },
       "required": ["adunit"]
    diff --git a/src/main/resources/static/bidder-params/adot.json b/src/main/resources/static/bidder-params/adot.json
    new file mode 100644
    index 00000000000..e4c9922a52d
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/adot.json
    @@ -0,0 +1,17 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "The Adot Adapter Params",
    +  "description": "A schema which validates params accepted by Adot adapter",
    +  "type": "object",
    +  "properties": {
    +    "placementId": {
    +      "type": "string",
    +      "description": "An ID which identifies this placement of the impression"
    +    },
    +    "parallax": {
    +      "type": "boolean",
    +      "description": "It determines if the wanted advertising format is a parallax."
    +    }
    +  },
    +  "required": []
    +}
    diff --git a/src/main/resources/static/bidder-params/adprime.json b/src/main/resources/static/bidder-params/adprime.json
    new file mode 100644
    index 00000000000..31a83842e9a
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/adprime.json
    @@ -0,0 +1,14 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Adprime Adapter Params",
    +  "description": "A schema which validates params accepted by the Adprime adapter",
    +
    +  "type": "object",
    +  "properties": {
    +    "TagID": {
    +      "type": "string",
    +      "description": "An ID which identifies the adprime ad tag"
    +    }
    +  },
    +  "required" : [ "TagID" ]
    +}
    diff --git a/src/main/resources/static/bidder-params/adxcg.json b/src/main/resources/static/bidder-params/adxcg.json
    new file mode 100644
    index 00000000000..ee438c961a0
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/adxcg.json
    @@ -0,0 +1,16 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Adxcg Adapter Params",
    +  "description": "A schema which validates params accepted by the Adxcg adapter",
    +  "type": "object",
    +  "properties": {
    +    "adzoneid": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "An ID which identifies the placement selling the impression"
    +    }
    +  },
    +  "required": [
    +    "adzoneid"
    +  ]
    +}
    \ No newline at end of file
    diff --git a/src/main/resources/static/bidder-params/adyoulike.json b/src/main/resources/static/bidder-params/adyoulike.json
    new file mode 100644
    index 00000000000..448de344739
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/adyoulike.json
    @@ -0,0 +1,33 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "AdYouLike Adapter Params",
    +  "description": "A schema which validates params accepted by the AdYouLike adapter",
    +  "type": "object",
    +  "properties": {
    +    "placement": {
    +      "type": "string",
    +      "description": "Placement Id"
    +    },
    +    "campaign": {
    +      "type": "string",
    +      "description": "Id of a forced campaign"
    +    },
    +    "track": {
    +      "type": "string",
    +      "description": "Id of a forced Track"
    +    },
    +    "creative": {
    +      "type": "string",
    +      "description": "Id of a forced creative"
    +    },
    +    "source": {
    +      "type": "string",
    +      "description": "context of the campaign"
    +    },
    +    "debug": {
    +      "type": "string",
    +      "description": "Arbitrary id used for debug purpose"
    +    }
    +  },
    +  "required": ["placement"]
    +}
    diff --git a/src/main/resources/static/bidder-params/algorix.json b/src/main/resources/static/bidder-params/algorix.json
    new file mode 100644
    index 00000000000..a29a8a1febc
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/algorix.json
    @@ -0,0 +1,17 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "AlgoriX Adapter Params",
    +  "description": "A schema which validates params accepted by the AlgoriX adapter",
    +  "type": "object",
    +  "properties": {
    +    "sid": {
    +      "type": "string",
    +      "description": "Your Sid"
    +    },
    +    "token": {
    +      "type": "string",
    +      "description": "Your Token"
    +    }
    +  },
    +  "required": ["sid", "token"]
    +}
    \ No newline at end of file
    diff --git a/src/main/resources/static/bidder-params/amx.json b/src/main/resources/static/bidder-params/amx.json
    new file mode 100644
    index 00000000000..f9b1b26b3db
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/amx.json
    @@ -0,0 +1,16 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "AMX RTB Adapter Params",
    +  "description": "A schema to validate params accepted by the AMX adapter",
    +  "type": "object",
    +  "properties": {
    +    "tagId" : {
    +      "type": "string",
    +      "description": "Set a tagId (overrides site.publisher.id, or app.publisher.id)"
    +    },
    +    "adUnitId": {
    +      "type": "string",
    +      "description": "Override imp.tagid value to provide a custom value in AMX ad unit ID reporting"
    +    }
    +  }
    +}
    diff --git a/src/main/resources/static/bidder-params/appnexus.json b/src/main/resources/static/bidder-params/appnexus.json
    index 78701b95b48..bfcb275b644 100644
    --- a/src/main/resources/static/bidder-params/appnexus.json
    +++ b/src/main/resources/static/bidder-params/appnexus.json
    @@ -71,6 +71,10 @@
           "type": "boolean",
           "description": "Boolean to signal AppNexus to apply the relevant payment rule"
         },
    +    "generate_ad_pod_id": {
    +      "type": "boolean",
    +      "description": "Boolean to signal AppNexus to add ad pod id to each request"
    +    },
         "private_sizes": {
           "type": "array",
           "items": {
    diff --git a/src/main/resources/static/bidder-params/axonix.json b/src/main/resources/static/bidder-params/axonix.json
    new file mode 100644
    index 00000000000..7a3762ce5e2
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/axonix.json
    @@ -0,0 +1,14 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Axonix Adapter Params",
    +  "description": "A schema which validates params accepted by the Axonix adapter",
    +  "type": "object",
    +  "properties": {
    +    "supplyId": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "Unique supply identifier"
    +    }
    +  },
    +  "required": ["supplyId"]
    +}
    diff --git a/src/main/resources/static/bidder-params/beachfront.json b/src/main/resources/static/bidder-params/beachfront.json
    index c2ff3b9f46c..624045291ac 100644
    --- a/src/main/resources/static/bidder-params/beachfront.json
    +++ b/src/main/resources/static/bidder-params/beachfront.json
    @@ -14,26 +14,30 @@
           "properties": {
             "video": {
               "type": "string",
    +          "title": "Video appId",
               "description": "An appId string that will be applied to video requests in this imp."
             },
             "banner": {
               "type": "string",
    +          "title": "Banner appId",
               "description": "An appId string that will be applied to banner requests in this imp."
             }
    -      }
    +      },
    +      "anyOf":[
    +        {"required":["video"]},
    +        {"required":["banner"]}
    +      ]
         },
         "bidfloor": {
           "type": "number",
    -      "description": "The price floor for the bid."
    -    },
    +      "description": "The price floor for the bid. Will override the bidfloor set for the impression."    },
         "videoResponseType": {
           "type": "string",
    -      "description": "By default the video response will be a nurl URL, but if you want AdM/VAST, set this to 'adm'. If you want both set it to 'both'. Setting it to any other string will have no effect and the default format will be returned."
    +      "description": "By default the video response will be an AdM element containing VAST 3.0 markup including tracking, click through, and mediafile elements pointing to mp4, webm or other playable media files or Vpaid media as configured for the exchange at beachfront.io. Optionally, set this to 'nurl' to receive a URI pointing to VAST 3.0 markup which will contain a mediafile pointing to a beachfront neptune javascript video player which will load your video and take care of tracking, etc. Regardless of which format is selected, the id of the returned impression will be the provided impression id (imp[{'id'...},...] in the request) with the format appended. The impression id will be returned unchanged as 'impid'. So if you indicate 'nurl', and an impression id 'someImp', the returned impression will have an 'impid' value of 'someImp', and the 'id' value 'someImpNurlVideo'. This is to differentiate the object in the case that a request includes both video and banner elements. Setting videoResponseType to any other string will have no effect and the default format (AdM) will be returned."
         }
       },
    -  "required": ["bidfloor"],
    -  "oneOf": [{
    -    "required": ["appId"] }, {
    -    "required": ["appIds"]
    -  }]
    +  "oneOf": [
    +    {"required": ["appId"] },
    +    {"required": ["appIds"]}
    +  ]
     }
    diff --git a/src/main/resources/static/bidder-params/between.json b/src/main/resources/static/bidder-params/between.json
    new file mode 100644
    index 00000000000..64863462697
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/between.json
    @@ -0,0 +1,18 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "BetweenDigital Adapter Params",
    +  "description": "A schema which validates params accepted by the BetweenDigital adapter",
    +  "type": "object",
    +  "properties": {
    +    "host": {
    +      "type": "string",
    +      "description": "Network Host to request from",
    +      "enum": ["lbs-eu1.ads", "lbs-ru1.ads", "lbs-us-east1.ads", "lbs-asia1.ads"]
    +    },
    +    "publisher_id": {
    +      "type": "string",
    +      "description": "Publisher ID from Between Exchange control panel"
    +    }
    +  },
    +  "required": ["host", "publisher_id"]
    +}
    diff --git a/src/main/resources/static/bidder-params/bidmachine.json b/src/main/resources/static/bidder-params/bidmachine.json
    new file mode 100644
    index 00000000000..f6faf888a2c
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/bidmachine.json
    @@ -0,0 +1,28 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Bidmachine Adapter Params",
    +  "description": "A schema which validates params accepted by the Kidoz adapter",
    +  "type": "object",
    +  "properties": {
    +    "host": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "Host"
    +    },
    +    "path": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "URL path"
    +    },
    +    "seller_id": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "Seller Identifier"
    +    }
    +  },
    +  "required": [
    +    "host",
    +    "path",
    +    "seller_id"
    +  ]
    +}
    \ No newline at end of file
    diff --git a/src/main/resources/static/bidder-params/bidmyadz.json b/src/main/resources/static/bidder-params/bidmyadz.json
    new file mode 100644
    index 00000000000..b45b48a7c30
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/bidmyadz.json
    @@ -0,0 +1,12 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "BidMyAdz Adapter Params",
    +  "description": "A schema which validates params accepted by the BidMyAdz adapter",
    +  "type": "object",
    +  "properties": {
    +    "placementId": {
    +      "type": "string"
    +    }
    +  },
    +  "required": ["placementId"]
    +}
    \ No newline at end of file
    diff --git a/src/main/resources/static/bidder-params/bidscube.json b/src/main/resources/static/bidder-params/bidscube.json
    new file mode 100644
    index 00000000000..88dc91ab391
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/bidscube.json
    @@ -0,0 +1,18 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "BidsCube Adapter Params",
    +  "description": "A schema which validates params accepted by the BidsCube adapter",
    +  "type": "object",
    +  "properties": {
    +    "placementId": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "An ID which identifies the BidsCube placement"
    +    },
    +    "customParams": {
    +      "type": "object",
    +      "description": "User-defined targeting key-value pairs."
    +    }
    +  },
    +  "required" : [ "placementId" ]
    +}
    diff --git a/src/main/resources/static/bidder-params/bmtm.json b/src/main/resources/static/bidder-params/bmtm.json
    new file mode 100644
    index 00000000000..f1fbbddc7c6
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/bmtm.json
    @@ -0,0 +1,16 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Bright Mountain Media Adapter Params",
    +  "description": "A schema which validates params accepted by the Bright Mountain Media adapter",
    +  "type": "object",
    +  "properties": {
    +    "placement_id": {
    +      "type": "number",
    +      "minimum": 1,
    +      "description": "Placement ID from Bright Mountain Media"
    +    }
    +  },
    +  "required": [
    +    "placement_id"
    +  ]
    +}
    \ No newline at end of file
    diff --git a/src/main/resources/static/bidder-params/colossus.json b/src/main/resources/static/bidder-params/colossus.json
    new file mode 100644
    index 00000000000..8d8f3dc0c4f
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/colossus.json
    @@ -0,0 +1,14 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Colossus Adapter Params",
    +  "description": "A schema which validates params accepted by the Colossus adapter",
    +
    +  "type": "object",
    +  "properties": {
    +    "TagID": {
    +      "type": "string",
    +      "description": "An ID which identifies the colossus ad tag"
    +    }
    +  },
    +  "required" : [ "TagID" ]
    +}
    diff --git a/src/main/resources/static/bidder-params/connectad.json b/src/main/resources/static/bidder-params/connectad.json
    new file mode 100644
    index 00000000000..faed542913f
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/connectad.json
    @@ -0,0 +1,22 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "ConnectAd S2S dapter Params",
    +  "description": "A schema which validates params accepted by the ConnectAd Adapter",
    +
    +  "type": "object",
    +  "properties": {
    +    "networkId": {
    +      "type": "integer",
    +      "description": "NetworkId"
    +    },
    +    "siteId": {
    +      "type": "integer",
    +      "description": "SiteId"
    +    },
    +    "bidfloor": {
    +      "type": "number",
    +      "description": "Requests Floorprice"
    +    }
    +  },
    +  "required": ["networkId", "siteId"]
    +}
    diff --git a/src/main/resources/static/bidder-params/conversant.json b/src/main/resources/static/bidder-params/conversant.json
    index 1154a4a998f..947a38b5be6 100644
    --- a/src/main/resources/static/bidder-params/conversant.json
    +++ b/src/main/resources/static/bidder-params/conversant.json
    @@ -24,10 +24,6 @@
           "type": "integer",
           "description": "Ad position on screen."
         },
    -    "mobile": {
    -      "type": "integer",
    -      "description": "Indicate if the site is mobile optimized."
    -    },
         "mimes": {
           "type": "array",
           "description": "Array of content MIME types.  For videos only.",
    @@ -56,4 +52,4 @@
       },
       "required": [
       ]
    -}
    \ No newline at end of file
    +}
    diff --git a/src/main/resources/static/bidder-params/criteo.json b/src/main/resources/static/bidder-params/criteo.json
    new file mode 100644
    index 00000000000..a44c2ae1cf1
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/criteo.json
    @@ -0,0 +1,50 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Criteo adapter params",
    +  "description": "The schema to validate Criteo specific params accepted by Criteo adapter",
    +  "type": "object",
    +  "properties": {
    +    "zoneid": {
    +      "type": "integer",
    +      "description": "Impression's zone ID.",
    +      "minimum": 0
    +    },
    +    "zoneId": {
    +      "type": "integer",
    +      "description": "Impression's zone ID, preferred.",
    +      "minimum": 0
    +    },
    +    "networkid": {
    +      "type": "integer",
    +      "description": "Impression's network ID.",
    +      "minimum": 0
    +    },
    +    "networkId": {
    +      "type": "integer",
    +      "description": "Impression's network ID, preferred.",
    +      "minimum": 0
    +    }
    +  },
    +  "anyOf": [
    +    {
    +      "required": [
    +        "zoneid"
    +      ]
    +    },
    +    {
    +      "required": [
    +        "zoneId"
    +      ]
    +    },
    +    {
    +      "required": [
    +        "networkid"
    +      ]
    +    },
    +    {
    +      "required": [
    +        "networkId"
    +      ]
    +    }
    +  ]
    +}
    diff --git a/src/main/resources/static/bidder-params/decenterads.json b/src/main/resources/static/bidder-params/decenterads.json
    new file mode 100644
    index 00000000000..970e99038a0
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/decenterads.json
    @@ -0,0 +1,18 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "DecenterAds Adapter Params",
    +  "description": "A schema which validates params accepted by the DecenterAds adapter",
    +  "type": "object",
    +  "properties": {
    +    "placementId": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "An ID which identifies the DecenterAds placement"
    +    },
    +    "customParams": {
    +      "type": "object",
    +      "description": "User-defined targeting key-value pairs."
    +    }
    +  },
    +  "required" : [ "placementId" ]
    +}
    diff --git a/src/main/resources/static/bidder-params/deepintent.json b/src/main/resources/static/bidder-params/deepintent.json
    new file mode 100644
    index 00000000000..772938805f1
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/deepintent.json
    @@ -0,0 +1,14 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Deepintent Adapter Params",
    +  "description": "A schema which validates params accepted by the Deepintent adapter",
    +
    +  "type": "object",
    +  "properties": {
    +    "tagId": {
    +      "type": "string",
    +      "description": "An ID which identifies the deepintent ad tag"
    +    }
    +  },
    +  "required" : [ "tagId" ]
    +}
    diff --git a/src/main/resources/static/bidder-params/dmx.json b/src/main/resources/static/bidder-params/dmx.json
    index 1c6333e90dd..fa2d447b5d2 100644
    --- a/src/main/resources/static/bidder-params/dmx.json
    +++ b/src/main/resources/static/bidder-params/dmx.json
    @@ -8,13 +8,25 @@
           "type": "string",
           "description": "Represent boost MemberId from districtm UI"
         },
    +    "placement_id" : {
    +      "type": "string",
    +      "description": "memberid replacement / alternative value or equivalent"
    +    },
    +    "seller_id" : {
    +      "type": "string",
    +      "description": "Represent DMX Partner when you get onboarded, this is for specific setup BURL vs NURL"
    +    },
    +    "dmxid": {
    +      "type": "string",
    +      "description": "Represent the placement ID dmxid equivalent to 'tagid', this value is optional"
    +    },
         "tagid": {
           "type": "string",
    -      "description": "Represent the placement ID, this value is optional"
    +      "description": "Represent the placement ID tagid equivalent to 'dmxid', this value is optional"
         },
         "bidfloor": {
    -      "type": "string",
    -      "description": "The minimum price acceptable for a bid"
    +      "type": "number",
    +      "description": "The minimum price acceptable for a bid, this is optional since we do get the one from the original openrtb request"
         }
       },
     
    diff --git a/src/main/resources/static/bidder-params/e_volution.json b/src/main/resources/static/bidder-params/e_volution.json
    new file mode 100644
    index 00000000000..c4a55373212
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/e_volution.json
    @@ -0,0 +1,13 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "E-volution Adapter Params",
    +  "description": "A schema which validates params accepted by the E-volution adapter",
    +  "type": "object",
    +  "properties": {
    +    "key": {
    +      "type": "string",
    +      "description": "network or placement key"
    +    }
    +  },
    +  "required": ["key"]
    +}
    \ No newline at end of file
    diff --git a/src/main/resources/static/bidder-params/epom.json b/src/main/resources/static/bidder-params/epom.json
    new file mode 100644
    index 00000000000..ee8c14e4f7e
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/epom.json
    @@ -0,0 +1,8 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Epom Adapter Params",
    +  "description": "A schema which validates params accepted by the Epom adapter",
    +  "type": "object",
    +
    +  "properties": {}
    +}
    diff --git a/src/main/resources/static/bidder-params/gumgum.json b/src/main/resources/static/bidder-params/gumgum.json
    index 7ee07474cc1..3a8f7a7c6dc 100644
    --- a/src/main/resources/static/bidder-params/gumgum.json
    +++ b/src/main/resources/static/bidder-params/gumgum.json
    @@ -8,9 +8,27 @@
           "type": "string",
           "description": "A tracking id used to identify GumGum zone.",
           "minLength": 8
    +    },
    +    "pubId": {
    +      "type": "integer",
    +      "description": "A tracking id used to identify GumGum publisher"
    +    },
    +    "irisid": {
    +      "type": "string",
    +      "description": "A hashed IRIS.TV Content ID"
         }
       },
    -  "required": [
    -    "zone"
    +  "anyOf": [
    +    {
    +      "required": [
    +        "zone"
    +      ]
    +    },
    +    {
    +      "required": [
    +        "pubId"
    +      ]
    +    }
       ]
     }
    +
    diff --git a/src/main/resources/static/bidder-params/interactiveoffers.json b/src/main/resources/static/bidder-params/interactiveoffers.json
    new file mode 100644
    index 00000000000..40957734d29
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/interactiveoffers.json
    @@ -0,0 +1,15 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Interactive Offers Adapter Params",
    +  "description": "A schema which validates params accepted by Interactive Offers adapter",
    +  "type": "object",
    +  "properties": {
    +    "partnerId": {
    +      "type": "string",
    +      "description": "The partners id"
    +    }
    +  },
    +  "required": [
    +    "partnerId"
    +  ]
    +}
    diff --git a/src/main/resources/static/bidder-params/invibes.json b/src/main/resources/static/bidder-params/invibes.json
    new file mode 100644
    index 00000000000..5545b17409d
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/invibes.json
    @@ -0,0 +1,30 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Invibes Adapter Params",
    +  "description": "A schema which validates params accepted by the Invibes adapter",
    +  "type": "object",
    +  "properties": {
    +    "placementId": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "An ID which identifies the site selling the impression"
    +    },
    +    "domainId": {
    +      "type": "integer",
    +      "description": "Ad domain id"
    +    },
    +    "debug": {
    +      "type": "object",
    +      "properties": {
    +        "testBvid": {
    +          "type": "string"
    +        },
    +        "testLog": {
    +          "type": "boolean"
    +        }
    +      },
    +      "description": "Parameters used for debugging purposes"
    +    }
    +  },
    +  "required": ["placementId"]
    +}
    diff --git a/src/main/resources/static/bidder-params/ix.json b/src/main/resources/static/bidder-params/ix.json
    index 7ed56deef46..f5a357c16f4 100644
    --- a/src/main/resources/static/bidder-params/ix.json
    +++ b/src/main/resources/static/bidder-params/ix.json
    @@ -7,9 +7,20 @@
         "siteId": {
           "type": "string",
           "description": "An ID which identifies the site selling the impression"
    +    },
    +    "size": {
    +      "type": "array",
    +      "items": {
    +        "type": "integer"
    +      },
    +      "minItems": 2,
    +      "maxItems": 2,
    +      "description": "An array of two integer containing the dimension"
         }
       },
    -  "required": [
    -    "siteId"
    +  "oneOf": [
    +    {"required": ["siteid"]},
    +    {"required": ["siteId"]},
    +    {"required": ["siteID"]}
       ]
     }
    diff --git a/src/main/resources/static/bidder-params/jixie.json b/src/main/resources/static/bidder-params/jixie.json
    new file mode 100644
    index 00000000000..732b05f8ca7
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/jixie.json
    @@ -0,0 +1,26 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Jixie Adapter Params",
    +  "description": "A schema which validates params accepted by the Jixie adapter",
    +  "type": "object",
    +  "properties": {
    +    "unit" : {
    +      "type": "string",
    +      "description": "The unit code of an inventory target",
    +      "minLength": 18
    +    },
    +    "accountid" : {
    +      "type": "string",
    +      "description": "The accountid of an inventory target"
    +    },
    +    "jxprop1" : {
    +      "type": "string",
    +      "description": "jxprop1 of an inventory target"
    +    },
    +    "jxprop2" : {
    +      "type": "string",
    +      "description": "jxprop2 of an inventory target"
    +    }
    +  },
    +  "required": ["unit"]
    +}
    diff --git a/src/main/resources/static/bidder-params/kayzen.json b/src/main/resources/static/bidder-params/kayzen.json
    new file mode 100644
    index 00000000000..f2256c6b029
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/kayzen.json
    @@ -0,0 +1,21 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Kayzen Adapter Params",
    +  "description": "A schema which validates params accepted by the Kayzen adapter",
    +  "type": "object",
    +
    +  "properties": {
    +    "zone": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "Zone ID"
    +    },
    +    "exchange": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "Exchange/Publisher Name"
    +    }
    +  },
    +  "required": ["zone", "exchange"]
    +}
    +
    diff --git a/src/main/resources/static/bidder-params/kidoz.json b/src/main/resources/static/bidder-params/kidoz.json
    index 25a5c94e704..dcd31e28bda 100644
    --- a/src/main/resources/static/bidder-params/kidoz.json
    +++ b/src/main/resources/static/bidder-params/kidoz.json
    @@ -5,18 +5,14 @@
       "type": "object",
       "properties": {
         "access_token": {
    -      "$ref": "#/definitions/non-empty-string",
    +      "type": "string",
    +      "minLength": 1,
           "description": "Kidoz access_token"
         },
         "publisher_id": {
    -      "$ref": "#/definitions/non-empty-string",
    -      "description": "Kidoz publisher_id"
    -    }
    -  },
    -  "definitions": {
    -    "non-empty-string": {
           "type": "string",
    -      "minLength": 1
    +      "minLength": 1,
    +      "description": "Kidoz publisher_id"
         }
       },
       "required": [
    diff --git a/src/main/resources/static/bidder-params/krushmedia.json b/src/main/resources/static/bidder-params/krushmedia.json
    new file mode 100644
    index 00000000000..fcd9e4f419d
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/krushmedia.json
    @@ -0,0 +1,13 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Krushmedia Adapter Params",
    +  "description": "A schema which validates params accepted by the Krushmedia adapter",
    +  "type": "object",
    +  "properties": {
    +    "key": {
    +      "type": "string",
    +      "description": "ssp key"
    +    }
    +  },
    +  "required": ["key"]
    +}
    diff --git a/src/main/resources/static/bidder-params/lifestreet.json b/src/main/resources/static/bidder-params/lifestreet.json
    deleted file mode 100644
    index d8acdf7ac04..00000000000
    --- a/src/main/resources/static/bidder-params/lifestreet.json
    +++ /dev/null
    @@ -1,15 +0,0 @@
    -{
    -  "$schema": "http://json-schema.org/draft-04/schema#",
    -  "title": "Lifestreet Adapter Params",
    -  "description": "A schema which validates params accepted by the Lifestreet adapter",
    -  "type": "object",
    -  "properties": {
    -    "slot_tag": {
    -      "type": "string",
    -      "description": "A tag which identifies the ad slot"
    -    }
    -  },
    -  "required": [
    -    "slot_tag"
    -  ]
    -}
    diff --git a/src/main/resources/static/bidder-params/loopme.json b/src/main/resources/static/bidder-params/loopme.json
    new file mode 100644
    index 00000000000..f6b4a0a8b2e
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/loopme.json
    @@ -0,0 +1,15 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Loopme Adapter Params",
    +  "description": "A schema which validates params accepted by the Loopme adapter",
    +  "type": "object",
    +
    +  "properties": {
    +    "accountId": {
    +      "type": "string",
    +      "description": "Account ID"
    +    }
    +  },
    +
    +  "required": ["accountId"]
    +}
    \ No newline at end of file
    diff --git a/src/main/resources/static/bidder-params/madvertise.json b/src/main/resources/static/bidder-params/madvertise.json
    new file mode 100644
    index 00000000000..d062df37a56
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/madvertise.json
    @@ -0,0 +1,16 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Madvertise Adapter Params",
    +  "description": "A schema which validates params accepted by the Madvertise adapter",
    +  "type": "object",
    +  "properties": {
    +    "zoneId": {
    +      "type": "string",
    +      "minLength": 7,
    +      "description": "The zone ID provided by Madvertise"
    +    }
    +  },
    +  "required": [
    +    "zoneId"
    +  ]
    +}
    \ No newline at end of file
    diff --git a/src/main/resources/static/bidder-params/medianet.json b/src/main/resources/static/bidder-params/medianet.json
    new file mode 100644
    index 00000000000..d50ee049fd1
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/medianet.json
    @@ -0,0 +1,20 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Medianet Adapter Params",
    +  "description": "A schema which validates params accepted by the Medianet adapter",
    +  "type": "object",
    +  "properties": {
    +    "cid": {
    +      "type": "string",
    +      "description": "The customer id provided by Media.net."
    +    },
    +    "crid": {
    +      "type": "string",
    +      "description": "The placement id provided by Media.net."
    +    }
    +  },
    +  "required": [
    +    "cid",
    +    "crid"
    +  ]
    +}
    diff --git a/src/main/resources/static/bidder-params/mobfoxpb.json b/src/main/resources/static/bidder-params/mobfoxpb.json
    new file mode 100644
    index 00000000000..0542b89f4dd
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/mobfoxpb.json
    @@ -0,0 +1,30 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Mobfox Adapter Params",
    +  "description": "A schema which validates params accepted by the Mobfox adapter",
    +  "type": "object",
    +  "properties": {
    +    "TagID": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "An ID which identifies the mobfox ad tag"
    +    },
    +    "key": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "An ID which identifies the mobfox adexchange partner"
    +    }
    +  },
    +  "oneOf": [
    +    {
    +      "required": [
    +        "TagID"
    +      ]
    +    },
    +    {
    +      "required": [
    +        "key"
    +      ]
    +    }
    +  ]
    +}
    diff --git a/src/main/resources/static/bidder-params/nanointeractive.json b/src/main/resources/static/bidder-params/nanointeractive.json
    index be51f480751..2023c787ce6 100644
    --- a/src/main/resources/static/bidder-params/nanointeractive.json
    +++ b/src/main/resources/static/bidder-params/nanointeractive.json
    @@ -6,7 +6,7 @@
       "properties": {
         "pid": {
           "type": "string",
    -      "description": "Placement idd"
    +      "description": "Placement id"
         },
         "nq": {
           "type": "array",
    @@ -23,10 +23,12 @@
           "type": "string",
           "description": "any segment value provided by publisher"
         },
    -    "ref" : {
    +    "ref": {
           "type": "string",
           "description": "referer"
         }
       },
    -  "required": ["pid"]
    +  "required": [
    +    "pid"
    +  ]
     }
    diff --git a/src/main/resources/static/bidder-params/nobid.json b/src/main/resources/static/bidder-params/nobid.json
    new file mode 100644
    index 00000000000..058df39ecb1
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/nobid.json
    @@ -0,0 +1,17 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "NoBid Adapter Params",
    +  "description": "A schema which validates params accepted by the NoBid adapter",
    +
    +  "type": "object",
    +  "properties": {
    +    "siteId": {
    +      "type": "integer",
    +      "description": "A Required ID which identifies the NoBid site. The siteId parameter is provided by your NoBid account manager."
    +    }, "placementId": {
    +      "type": "integer",
    +      "description": "An optional ID which identifies an adunit in a site. The placementId parameter is provided by your NoBid account manager."
    +    }
    +  },
    +  "required": ["siteId"]
    +}
    diff --git a/src/main/resources/static/bidder-params/onetag.json b/src/main/resources/static/bidder-params/onetag.json
    new file mode 100644
    index 00000000000..a1c22869a7b
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/onetag.json
    @@ -0,0 +1,20 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Onetag Adapter Params",
    +  "description": "A schema which validates params accepted by the Onetag adapter",
    +  "type": "object",
    +
    +  "properties": {
    +    "pubId": {
    +      "type": "string",
    +      "minLength": 1,
    +      "description": "The publisher's ID provided by OneTag"
    +    },
    +    "ext": {
    +      "type": "object",
    +      "description": "A set of custom key-value pairs"
    +    }
    +  },
    +
    +  "required": ["pubId"]
    +}
    diff --git a/src/main/resources/static/bidder-params/outbrain.json b/src/main/resources/static/bidder-params/outbrain.json
    new file mode 100644
    index 00000000000..8cfb8bbdf28
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/outbrain.json
    @@ -0,0 +1,40 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Outbrain Adapter Params",
    +  "description": "A schema which validates params accepted by the Outbrain adapter",
    +
    +  "type": "object",
    +  "properties": {
    +    "publisher": {
    +      "type": "object",
    +      "properties": {
    +        "id": {
    +          "type": "string"
    +        },
    +        "name": {
    +          "type": "string"
    +        },
    +        "domain": {
    +          "type": "string"
    +        }
    +      },
    +      "required": ["id"]
    +    },
    +    "tagid": {
    +      "type": "string"
    +    },
    +    "bcat": {
    +      "type": "array",
    +      "items": {
    +        "type": "string"
    +      }
    +    },
    +    "badv": {
    +      "type": "array",
    +      "items": {
    +        "type": "string"
    +      }
    +    }
    +  },
    +  "required": ["publisher"]
    +}
    diff --git a/src/main/resources/static/bidder-params/pangle.json b/src/main/resources/static/bidder-params/pangle.json
    new file mode 100644
    index 00000000000..a175a196390
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/pangle.json
    @@ -0,0 +1,34 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Pangle Adapter Params",
    +  "description": "A schema which validates params accepted by the Pangle adapter",
    +  "type": "object",
    +  "properties": {
    +    "token": {
    +      "type": "string",
    +      "description": "Access Token",
    +      "pattern": ".+"
    +    }
    +  },
    +  "appid": {
    +    "type": "string",
    +    "description": "App ID",
    +    "pattern": "[0-9]+"
    +  },
    +  "placementid": {
    +    "type": "string",
    +    "description": "Placement ID",
    +    "pattern": "[0-9]+"
    +  },
    +  "dependencies": {
    +    "placementid": [
    +      "appid"
    +    ],
    +    "appid": [
    +      "placementid"
    +    ]
    +  },
    +  "required": [
    +    "token"
    +  ]
    +}
    diff --git a/src/main/resources/static/bidder-params/pubmatic.json b/src/main/resources/static/bidder-params/pubmatic.json
    index f76322da121..4cd9e8b3f26 100644
    --- a/src/main/resources/static/bidder-params/pubmatic.json
    +++ b/src/main/resources/static/bidder-params/pubmatic.json
    @@ -12,6 +12,14 @@
           "type": "string",
           "description": "An ID which identifies the ad slot"
         },
    +    "pmzoneid": {
    +      "type": "string",
    +      "description": "Comma separated zone id. Used im deal targeting & site section targeting. e.g drama,sport"
    +    },
    +    "dctr": {
    +      "type": "string",
    +      "description": "Deals Custom Targeting, pipe separated key-value pairs e.g key1=V1,V2,V3|key2=v1|key3=v3,v5"
    +    },
         "wrapper": {
           "type": "object",
           "description": "Specifies pubmatic openwrap configuration for a publisher",
    diff --git a/src/main/resources/static/bidder-params/pulsepoint.json b/src/main/resources/static/bidder-params/pulsepoint.json
    index 78891760a92..7758a67084d 100644
    --- a/src/main/resources/static/bidder-params/pulsepoint.json
    +++ b/src/main/resources/static/bidder-params/pulsepoint.json
    @@ -11,16 +11,10 @@
         "ct": {
           "type": "integer",
           "description": "An ID which identifies the ad slot being sold"
    -    },
    -    "cf": {
    -      "type": "string",
    -      "pattern": "^[0-9]+[xX][0-9]+$",
    -      "description": "The size of the ad slot being sold. This should be a string like 300X250"
         }
       },
       "required": [
         "cp",
    -    "ct",
    -    "cf"
    +    "ct"
       ]
     }
    diff --git a/src/main/resources/static/bidder-params/revcontent.json b/src/main/resources/static/bidder-params/revcontent.json
    new file mode 100644
    index 00000000000..57e5f54e9a6
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/revcontent.json
    @@ -0,0 +1,8 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Revcontent Adapter Params",
    +  "description": "A schema which validates params accepted by the Revcontent adapter",
    +  "type": "object",
    +  "properties": {},
    +  "required": []
    +}
    \ No newline at end of file
    diff --git a/src/main/resources/static/bidder-params/rtbhouse.json b/src/main/resources/static/bidder-params/rtbhouse.json
    index 10b40b39394..431422ca756 100644
    --- a/src/main/resources/static/bidder-params/rtbhouse.json
    +++ b/src/main/resources/static/bidder-params/rtbhouse.json
    @@ -1,10 +1,13 @@
     {
       "$schema": "http://json-schema.org/draft-04/schema#",
    -  "title": "Rtbhouse Params",
    -  "description": "A schema which validates params accepted by the Rtbhouse",
    +  "title": "RTB House Adapter Params",
    +  "description": "A schema which validates params accepted by the RTB House adapter",
       "type": "object",
       "properties": {
    +    "publisherId": {
    +      "type": "string",
    +      "description": "The publisher's ID provided by RTB House"
    +    }
       },
    -  "required": [
    -  ]
    +  "required": ["publisherId"]
     }
    diff --git a/src/main/resources/static/bidder-params/sa_lunamedia.json b/src/main/resources/static/bidder-params/sa_lunamedia.json
    new file mode 100644
    index 00000000000..c1768081fa8
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/sa_lunamedia.json
    @@ -0,0 +1,17 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "Sa_Lunamedia Adapter Params",
    +  "description": "A schema which validates params accepted by the Sa_Lunamedia adapter",
    +  "type": "object",
    +  "properties": {
    +    "key": {
    +      "type": "string",
    +      "description": "network or placement key"
    +    },
    +    "type": {
    +      "type": "string",
    +      "enum": ["network", "publisher"]
    +    }
    +  },
    +  "required": ["key"]
    +}
    \ No newline at end of file
    diff --git a/src/main/resources/static/bidder-params/sharethrough.json b/src/main/resources/static/bidder-params/sharethrough.json
    index dd060b14259..d8be2d60f43 100644
    --- a/src/main/resources/static/bidder-params/sharethrough.json
    +++ b/src/main/resources/static/bidder-params/sharethrough.json
    @@ -26,6 +26,16 @@
         "bidfloor": {
           "type": "number",
           "description": "The floor price, or minimum amount, a publisher will accept for an impression, given in CPM in USD"
    +    },
    +    "data": {
    +      "type": "object",
    +      "description": "Ad-specific first party data",
    +      "properties": {
    +        "pbadslot": {
    +          "type": "string",
    +          "description": "Prebid Ad Slot, see: https://docs.prebid.org/features/pbAdSlot.html"
    +        }
    +      }
         }
       },
       "required": [
    diff --git a/src/main/resources/static/bidder-params/silvermob.json b/src/main/resources/static/bidder-params/silvermob.json
    new file mode 100644
    index 00000000000..0ded8714cb5
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/silvermob.json
    @@ -0,0 +1,17 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "SilverMob Adapter Params",
    +  "description": "A schema which validates params accepted by the SilverMob adapter",
    +  "type": "object",
    +  "properties": {
    +    "zoneid": {
    +      "type": "string",
    +      "description": "Zone ID"
    +    },
    +    "host": {
    +      "type": "string",
    +      "description": "Host"
    +    }
    +  },
    +  "required": ["zoneid", "host"]
    +}
    diff --git a/src/main/resources/static/bidder-params/smartyads.json b/src/main/resources/static/bidder-params/smartyads.json
    new file mode 100644
    index 00000000000..f86a2e4b66b
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/smartyads.json
    @@ -0,0 +1,21 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "SmartyAds Adapter Params",
    +  "description": "A schema which validates params accepted by the SmartyAds adapter",
    +  "type": "object",
    +  "properties": {
    +    "host": {
    +      "type": "string",
    +      "description": "Network host to send request"
    +    },
    +    "sourceid": {
    +      "type": "string",
    +      "description": "Partner id"
    +    },
    +    "accountid": {
    +      "type": "string",
    +      "description": "Account id"
    +    }
    +  },
    +  "required": ["host", "sourceid", "accountid"]
    +}
    diff --git a/src/main/resources/static/bidder-params/smilewanted.json b/src/main/resources/static/bidder-params/smilewanted.json
    new file mode 100644
    index 00000000000..be4f9bc142d
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/smilewanted.json
    @@ -0,0 +1,14 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "SmileWanted Adapter Params",
    +  "description": "A schema which validates params accepted by the SmileWanted adapter",
    +  "type": "object",
    +  "properties": {
    +    "zoneId": {
    +      "type": "string",
    +      "description": "An ID which identifies the SmileWanted zone code",
    +      "minLength": 1
    +    }
    +  },
    +  "required": ["zoneId"]
    +}
    diff --git a/src/main/resources/static/bidder-params/tappx.json b/src/main/resources/static/bidder-params/tappx.json
    index 2d0fcb4b9ec..1d19fbeec22 100644
    --- a/src/main/resources/static/bidder-params/tappx.json
    +++ b/src/main/resources/static/bidder-params/tappx.json
    @@ -12,6 +12,10 @@
           "type": "string",
           "description": "An ID which identifies the adunit"
         },
    +    "mktag": {
    +      "type": "string",
    +      "description": "Minimum bid for this impression expressed in CPM (USD)"
    +    },
         "endpoint": {
           "type": "string",
           "description": "Endpoint provided to publisher"
    @@ -19,11 +23,21 @@
         "bidfloor": {
           "type": "number",
           "description": "Minimum bid for this impression expressed in CPM (USD)"
    +    },
    +    "bcid": {
    +      "type": "array",
    +      "description": "Block list of CID",
    +      "items": {
    +        "type": "string"
    +      }
    +    },
    +    "bcrid": {
    +      "type": "array",
    +      "description": "Block list of CRID",
    +      "items": {
    +        "type": "string"
    +      }
         }
       },
    -  "required": [
    -    "host",
    -    "tappxkey",
    -    "endpoint"
    -  ]
    +  "required": ["host","tappxkey","endpoint"]
     }
    diff --git a/src/main/resources/static/bidder-params/unicorn.json b/src/main/resources/static/bidder-params/unicorn.json
    new file mode 100644
    index 00000000000..90ff919e4fa
    --- /dev/null
    +++ b/src/main/resources/static/bidder-params/unicorn.json
    @@ -0,0 +1,25 @@
    +{
    +  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "title": "UNICORN Adapter Params",
    +  "description": "A schema which validates params accepted by the UNICORN adapter",
    +  "type": "object",
    +  "properties": {
    +    "placementId": {
    +      "type": "string",
    +      "description": "In Application, if placementId is empty, prebid server configuration id will be used as placementId."
    +    },
    +    "publisherId": {
    +      "type": "integer",
    +      "description": "Account specific publisher id"
    +    },
    +    "mediaId": {
    +      "type": "string",
    +      "description": "Publisher specific media id"
    +    },
    +    "accountId": {
    +      "type": "integer",
    +      "description": "Account ID for charge request"
    +    }
    +  },
    +  "required" : ["mediaId", "accountId"]
    +}
    diff --git a/src/test/java/org/prebid/server/analytics/AnalyticsReporterDelegatorTest.java b/src/test/java/org/prebid/server/analytics/AnalyticsReporterDelegatorTest.java
    new file mode 100644
    index 00000000000..95863e100ba
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/analytics/AnalyticsReporterDelegatorTest.java
    @@ -0,0 +1,301 @@
    +package org.prebid.server.analytics;
    +
    +import com.fasterxml.jackson.databind.ObjectMapper;
    +import com.fasterxml.jackson.databind.node.IntNode;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.fasterxml.jackson.databind.node.TextNode;
    +import com.iab.openrtb.request.BidRequest;
    +import io.netty.channel.ConnectTimeoutException;
    +import io.vertx.core.Future;
    +import io.vertx.core.Handler;
    +import io.vertx.core.Vertx;
    +import org.apache.commons.lang3.StringUtils;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.mockito.ArgumentCaptor;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
    +import org.mockito.stubbing.Answer;
    +import org.prebid.server.analytics.model.AuctionEvent;
    +import org.prebid.server.auction.PrivacyEnforcementService;
    +import org.prebid.server.auction.model.AuctionContext;
    +import org.prebid.server.exception.InvalidRequestException;
    +import org.prebid.server.metric.MetricName;
    +import org.prebid.server.metric.Metrics;
    +import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction;
    +import org.prebid.server.privacy.gdpr.model.TcfContext;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +
    +import java.util.HashMap;
    +import java.util.Map;
    +import java.util.concurrent.TimeoutException;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singleton;
    +import static java.util.function.UnaryOperator.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.BDDMockito.willAnswer;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.never;
    +import static org.mockito.Mockito.times;
    +import static org.mockito.Mockito.verify;
    +
    +public class AnalyticsReporterDelegatorTest {
    +
    +    private static final String EVENT = StringUtils.EMPTY;
    +    private static final Integer FIRST_REPORTER_ID = 1;
    +    private static final Integer SECOND_REPORTER_ID = 2;
    +
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
    +    @Mock
    +    private Vertx vertx;
    +    @Mock
    +    private PrivacyEnforcementService privacyEnforcementService;
    +    @Mock
    +    private Metrics metrics;
    +
    +    private AnalyticsReporter firstReporter;
    +
    +    private AnalyticsReporter secondReporter;
    +
    +    private AnalyticsReporterDelegator target;
    +
    +    @Before
    +    public void setUp() {
    +        firstReporter = mock(AnalyticsReporter.class);
    +        given(firstReporter.vendorId()).willReturn(FIRST_REPORTER_ID);
    +        given(firstReporter.name()).willReturn("logAnalytics");
    +        given(firstReporter.processEvent(any())).willReturn(Future.succeededFuture());
    +
    +        secondReporter = mock(AnalyticsReporter.class);
    +        given(secondReporter.vendorId()).willReturn(SECOND_REPORTER_ID);
    +        given(secondReporter.name()).willReturn("adapter");
    +        given(secondReporter.processEvent(any())).willReturn(Future.succeededFuture());
    +
    +        willAnswer(withNullAndInvokeHandler()).given(vertx).runOnContext(any());
    +        final Map enforcementActionMap = new HashMap<>();
    +        enforcementActionMap.put(SECOND_REPORTER_ID, PrivacyEnforcementAction.allowAll());
    +        enforcementActionMap.put(FIRST_REPORTER_ID, PrivacyEnforcementAction.allowAll());
    +        given(privacyEnforcementService.resultForVendorIds(any(), any()))
    +                .willReturn(Future.succeededFuture(enforcementActionMap));
    +
    +        target = new AnalyticsReporterDelegator(asList(firstReporter, secondReporter), vertx,
    +                privacyEnforcementService, metrics);
    +    }
    +
    +    @Test
    +    public void shouldPassEventToAllDelegates() {
    +        // given
    +        willAnswer(withNullAndInvokeHandler()).given(vertx).runOnContext(any());
    +
    +        // when
    +        target.processEvent(EVENT);
    +
    +        // then
    +        verify(vertx, times(2)).runOnContext(any());
    +        assertThat(captureEvent(firstReporter)).isSameAs(EVENT);
    +        assertThat(captureEvent(secondReporter)).isSameAs(EVENT);
    +    }
    +
    +    @Test
    +    public void shouldTolerateInvalidExtPrebidAnalyticsNode() {
    +        // given
    +        final TextNode analyticsNode = new TextNode("invalid");
    +        final AuctionEvent givenAuctionEvent = givenAuctionEvent(bidRequestBuilder ->
    +                bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .analytics(analyticsNode)
    +                        .build())));
    +
    +        // when
    +        target.processEvent(givenAuctionEvent, TcfContext.empty());
    +
    +        // then
    +        verify(vertx, times(2)).runOnContext(any());
    +        assertThat(asList(captureAuctionEvent(firstReporter), captureAuctionEvent(secondReporter)))
    +                .extracting(AuctionEvent::getAuctionContext)
    +                .extracting(AuctionContext::getBidRequest)
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getAnalytics)
    +                .containsExactly(analyticsNode, analyticsNode);
    +    }
    +
    +    @Test
    +    public void shouldTolerateWithMissingBidRequest() {
    +        // given
    +        final AuctionEvent givenAuctionEventWithoutContext = AuctionEvent.builder().build();
    +        final AuctionEvent givenAuctionEventWithoutBidRequest = AuctionEvent.builder()
    +                .auctionContext(AuctionContext.builder().build())
    +                .build();
    +
    +        // when
    +        target.processEvent(givenAuctionEventWithoutContext, TcfContext.empty());
    +        target.processEvent(givenAuctionEventWithoutBidRequest, TcfContext.empty());
    +
    +        // then
    +        verify(vertx, times(4)).runOnContext(any());
    +    }
    +
    +    @Test
    +    public void shouldPassOnlyAdapterRelatedEntriesToAnalyticReporters() {
    +        // given
    +        final ObjectNode analyticsNode = new ObjectMapper().createObjectNode();
    +        analyticsNode.set("adapter", new TextNode("someValue"));
    +        analyticsNode.set("anotherAdapter", new IntNode(2));
    +        final AuctionEvent givenAuctionEvent = givenAuctionEvent(bidRequestBuilder ->
    +                bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .analytics(analyticsNode)
    +                        .build())));
    +
    +        // when
    +        target.processEvent(givenAuctionEvent, TcfContext.empty());
    +
    +        // then
    +        verify(vertx, times(2)).runOnContext(any());
    +        assertThat(singleton(captureAuctionEvent(firstReporter)))
    +                .extracting(AuctionEvent::getAuctionContext)
    +                .extracting(AuctionContext::getBidRequest)
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getAnalytics)
    +                .containsExactly(analyticsNode);
    +        final ObjectNode expectedAnalytics = new ObjectMapper().createObjectNode();
    +        expectedAnalytics.set("adapter", new TextNode("someValue"));
    +        assertThat(singleton(captureAuctionEvent(secondReporter)))
    +                .extracting(AuctionEvent::getAuctionContext)
    +                .extracting(AuctionContext::getBidRequest)
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getAnalytics)
    +                .containsExactly(expectedAnalytics);
    +    }
    +
    +    @Test
    +    public void shouldUpdateOkMetricsWithSpecificEventAndAdapterType() {
    +        // when
    +        target.processEvent(givenAuctionEvent(identity()), TcfContext.empty());
    +
    +        // then
    +        verify(metrics).updateAnalyticEventMetric("logAnalytics", MetricName.event_auction, MetricName.ok);
    +        verify(metrics).updateAnalyticEventMetric("adapter", MetricName.event_auction, MetricName.ok);
    +    }
    +
    +    @Test
    +    public void shouldUpdateTimeoutMetricsWithSpecificEventAndAdapterType() {
    +        // given
    +        given(firstReporter.processEvent(any())).willReturn(Future.failedFuture(new TimeoutException()));
    +        given(secondReporter.processEvent(any())).willReturn(Future.failedFuture(new ConnectTimeoutException()));
    +
    +        // when
    +        target.processEvent(givenAuctionEvent(identity()), TcfContext.empty());
    +
    +        // then
    +        verify(metrics).updateAnalyticEventMetric("logAnalytics", MetricName.event_auction, MetricName.timeout);
    +        verify(metrics).updateAnalyticEventMetric("adapter", MetricName.event_auction, MetricName.timeout);
    +    }
    +
    +    @Test
    +    public void shouldUpdateErrorMetricsWithSpecificEventAndAdapterType() {
    +        // given
    +        given(firstReporter.processEvent(any())).willReturn(Future.failedFuture(new RuntimeException()));
    +        given(secondReporter.processEvent(any())).willReturn(Future.failedFuture(new Exception()));
    +
    +        // when
    +        target.processEvent(givenAuctionEvent(identity()), TcfContext.empty());
    +
    +        // then
    +        verify(metrics).updateAnalyticEventMetric("logAnalytics", MetricName.event_auction, MetricName.err);
    +        verify(metrics).updateAnalyticEventMetric("adapter", MetricName.event_auction, MetricName.err);
    +    }
    +
    +    @Test
    +    public void shouldUpdateInvalidRequestMetricsWhenFutureContainsInvalidRequestException() {
    +        // given
    +        given(firstReporter.processEvent(any())).willReturn(Future.failedFuture(new InvalidRequestException("cause")));
    +        given(secondReporter.processEvent(any())).willReturn(Future.failedFuture(new InvalidRequestException("cause")));
    +
    +        // when
    +        target.processEvent(givenAuctionEvent(identity()), TcfContext.empty());
    +
    +        // then
    +        verify(metrics).updateAnalyticEventMetric("logAnalytics", MetricName.event_auction, MetricName.badinput);
    +        verify(metrics).updateAnalyticEventMetric("adapter", MetricName.event_auction, MetricName.badinput);
    +    }
    +
    +    @Test
    +    public void shouldPassEventToAllowedDelegatesWhenSomeVendorIdWasAllowed() {
    +        // given
    +        final Map enforcementActionMap = new HashMap<>();
    +        enforcementActionMap.put(FIRST_REPORTER_ID, PrivacyEnforcementAction.restrictAll());
    +        enforcementActionMap.put(SECOND_REPORTER_ID, PrivacyEnforcementAction.allowAll());
    +
    +        given(privacyEnforcementService.resultForVendorIds(any(), any()))
    +                .willReturn(Future.succeededFuture(enforcementActionMap));
    +
    +        willAnswer(withNullAndInvokeHandler()).given(vertx).runOnContext(any());
    +
    +        // when
    +        target.processEvent(EVENT, TcfContext.empty());
    +
    +        // then
    +        verify(vertx, times(1)).runOnContext(any());
    +        assertThat(captureEvent(secondReporter)).isSameAs(EVENT);
    +    }
    +
    +    @Test
    +    public void shouldNotPassEventToDelegatesWhenAllVendorIdsWasBlocked() {
    +        // given
    +        final Map enforcementActionMap = new HashMap<>();
    +        enforcementActionMap.put(FIRST_REPORTER_ID, PrivacyEnforcementAction.restrictAll());
    +        enforcementActionMap.put(SECOND_REPORTER_ID, PrivacyEnforcementAction.restrictAll());
    +
    +        given(privacyEnforcementService.resultForVendorIds(any(), any()))
    +                .willReturn(Future.succeededFuture(enforcementActionMap));
    +
    +        willAnswer(withNullAndInvokeHandler()).given(vertx).runOnContext(any());
    +
    +        // when
    +        target.processEvent(EVENT, TcfContext.empty());
    +
    +        // then
    +        verify(vertx, never()).runOnContext(any());
    +    }
    +
    +    @SuppressWarnings("unchecked")
    +    private static Answer withNullAndInvokeHandler() {
    +        return invocation -> {
    +            ((Handler) invocation.getArgument(0)).handle(null);
    +            return null;
    +        };
    +    }
    +
    +    private static String captureEvent(AnalyticsReporter reporter) {
    +        final ArgumentCaptor auctionEventCaptor = ArgumentCaptor.forClass(String.class);
    +        verify(reporter).processEvent(auctionEventCaptor.capture());
    +        return auctionEventCaptor.getValue();
    +    }
    +
    +    private static AuctionEvent captureAuctionEvent(AnalyticsReporter reporter) {
    +        final ArgumentCaptor auctionEventCaptor = ArgumentCaptor.forClass(AuctionEvent.class);
    +        verify(reporter).processEvent(auctionEventCaptor.capture());
    +        return auctionEventCaptor.getValue();
    +    }
    +
    +    private static AuctionEvent givenAuctionEvent(
    +            Function bidRequestCustomizer) {
    +
    +        return AuctionEvent.builder()
    +                .auctionContext(AuctionContext.builder()
    +                        .bidRequest(bidRequestCustomizer.apply(BidRequest.builder()).build())
    +                        .build())
    +                .build();
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/analytics/CompositeAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/CompositeAnalyticsReporterTest.java
    deleted file mode 100644
    index 7c24e814e8f..00000000000
    --- a/src/test/java/org/prebid/server/analytics/CompositeAnalyticsReporterTest.java
    +++ /dev/null
    @@ -1,64 +0,0 @@
    -package org.prebid.server.analytics;
    -
    -import io.vertx.core.Handler;
    -import io.vertx.core.Vertx;
    -import org.apache.commons.lang3.StringUtils;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.ArgumentCaptor;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.mockito.stubbing.Answer;
    -
    -import static java.util.Arrays.asList;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.mockito.ArgumentMatchers.any;
    -import static org.mockito.BDDMockito.willAnswer;
    -import static org.mockito.Mockito.mock;
    -import static org.mockito.Mockito.times;
    -import static org.mockito.Mockito.verify;
    -
    -public class CompositeAnalyticsReporterTest {
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private Vertx vertx;
    -
    -    @Test
    -    public void shouldPassEventToAllDelegates() {
    -        // given
    -        final String event = StringUtils.EMPTY;
    -
    -        final AnalyticsReporter reporter1 = mock(AnalyticsReporter.class);
    -        final AnalyticsReporter reporter2 = mock(AnalyticsReporter.class);
    -        final CompositeAnalyticsReporter analyticsReporter =
    -                new CompositeAnalyticsReporter(asList(reporter1, reporter2), vertx);
    -
    -        willAnswer(withNullAndInvokeHandler()).given(vertx).runOnContext(any());
    -
    -        // when
    -        analyticsReporter.processEvent(event);
    -
    -        // then
    -        verify(vertx, times(2)).runOnContext(any());
    -        assertThat(captureEvent(reporter1)).isSameAs(event);
    -        assertThat(captureEvent(reporter2)).isSameAs(event);
    -    }
    -
    -    @SuppressWarnings("unchecked")
    -    private static Answer withNullAndInvokeHandler() {
    -        return invocation -> {
    -            ((Handler) invocation.getArgument(0)).handle(null);
    -            return null;
    -        };
    -    }
    -
    -    private static String captureEvent(AnalyticsReporter reporter) {
    -        final ArgumentCaptor auctionEventCaptor = ArgumentCaptor.forClass(String.class);
    -        verify(reporter).processEvent(auctionEventCaptor.capture());
    -        return auctionEventCaptor.getValue();
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporterTest.java
    new file mode 100644
    index 00000000000..ae43831e80b
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporterTest.java
    @@ -0,0 +1,272 @@
    +package org.prebid.server.analytics.pubstack;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import io.vertx.core.Future;
    +import io.vertx.core.Vertx;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.analytics.model.AmpEvent;
    +import org.prebid.server.analytics.model.AuctionEvent;
    +import org.prebid.server.analytics.model.CookieSyncEvent;
    +import org.prebid.server.analytics.model.SetuidEvent;
    +import org.prebid.server.analytics.model.VideoEvent;
    +import org.prebid.server.analytics.pubstack.model.EventType;
    +import org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties;
    +import org.prebid.server.analytics.pubstack.model.PubstackConfig;
    +import org.prebid.server.exception.PreBidException;
    +import org.prebid.server.vertx.http.HttpClient;
    +import org.prebid.server.vertx.http.model.HttpClientResponse;
    +import org.springframework.test.util.ReflectionTestUtils;
    +
    +import java.util.Collections;
    +import java.util.HashMap;
    +import java.util.Map;
    +
    +import static org.assertj.core.api.Assertions.assertThatThrownBy;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyLong;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.ArgumentMatchers.eq;
    +import static org.mockito.ArgumentMatchers.same;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.times;
    +import static org.mockito.Mockito.verify;
    +import static org.mockito.Mockito.verifyZeroInteractions;
    +
    +public class PubstackAnalyticsReporterTest extends VertxTest {
    +
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
    +    @Mock
    +    private Vertx vertx;
    +
    +    @Mock
    +    private HttpClient httpClient;
    +
    +    @Mock
    +    private PubstackEventHandler auctionHandler;
    +
    +    @Mock
    +    private PubstackEventHandler setuidHandler;
    +
    +    private PubstackAnalyticsReporter pubstackAnalyticsReporter;
    +
    +    private PubstackAnalyticsProperties properties;
    +
    +    @Before
    +    public void setUp() {
    +        given(vertx.setPeriodic(anyLong(), any())).willReturn(1L, 2L);
    +        properties = PubstackAnalyticsProperties.builder()
    +                .endpoint("http://endpoint.com")
    +                .scopeId("scopeId")
    +                .sizeBytes(100000)
    +                .count(100)
    +                .reportTtlMs(10000L)
    +                .timeoutMs(5000L)
    +                .configurationRefreshDelayMs(200000L)
    +                .build();
    +
    +        final Map handlers = new HashMap<>();
    +        handlers.put(EventType.auction, auctionHandler);
    +        handlers.put(EventType.setuid, setuidHandler);
    +
    +        pubstackAnalyticsReporter = new PubstackAnalyticsReporter(properties, httpClient, jacksonMapper,
    +                vertx);
    +        // inject mocked handlers to private fields without accessor method
    +        ReflectionTestUtils.setField(pubstackAnalyticsReporter, "eventHandlers", handlers);
    +    }
    +
    +    @Test
    +    public void initializeShouldFetchConfigAndSetPeriodicTimerForConfigUpdate() throws JsonProcessingException {
    +        // given
    +        final PubstackConfig pubstackConfig = PubstackConfig.of("newScopeId", "http://newendpoint",
    +                Collections.singletonMap(EventType.auction, true));
    +        given(httpClient.get(anyString(), anyLong())).willReturn(
    +                Future.succeededFuture(HttpClientResponse.of(200, null, mapper.writeValueAsString(pubstackConfig))));
    +
    +        // when
    +        pubstackAnalyticsReporter.initialize();
    +
    +        // then
    +        verify(vertx).setPeriodic(anyLong(), any());
    +        verify(httpClient).get(anyString(), anyLong());
    +        verify(auctionHandler).reportEvents();
    +        verify(setuidHandler).reportEvents();
    +        verify(auctionHandler).updateConfig(eq(true), eq("http://newendpoint/intake/auction"), eq("newScopeId"));
    +        verify(setuidHandler).updateConfig(eq(false), eq("http://newendpoint/intake/setuid"), eq("newScopeId"));
    +    }
    +
    +    @Test
    +    public void initializeShouldFailUpdateSendBuffersAndSetTimerWhenEndpointFromRemoteConfigIsNotValid()
    +            throws JsonProcessingException {
    +        // given
    +        final PubstackConfig pubstackConfig = PubstackConfig.of("newScopeId", "invalid",
    +                Collections.singletonMap(EventType.auction, true));
    +        given(httpClient.get(anyString(), anyLong())).willReturn(
    +                Future.succeededFuture(HttpClientResponse.of(200, null, mapper.writeValueAsString(pubstackConfig))));
    +
    +        // when and then
    +        assertThatThrownBy(() -> pubstackAnalyticsReporter.initialize())
    +                .hasMessage("[pubstack] Failed to create event report url for endpoint: invalid")
    +                .isInstanceOf(PreBidException.class);
    +        verify(auctionHandler).reportEvents();
    +        verify(setuidHandler).reportEvents();
    +        verifyZeroInteractions(auctionHandler);
    +        verifyZeroInteractions(setuidHandler);
    +        verify(vertx).setPeriodic(anyLong(), any());
    +    }
    +
    +    @Test
    +    public void initializeShouldNotUpdateEventsIfFetchedConfigIsSameAsPrevious() throws JsonProcessingException {
    +        // given
    +        final PubstackConfig pubstackConfig = PubstackConfig.of("newScopeId", "http://newendpoint",
    +                Collections.singletonMap(EventType.auction, true));
    +        given(httpClient.get(anyString(), anyLong())).willReturn(
    +                Future.succeededFuture(HttpClientResponse.of(200, null, mapper.writeValueAsString(pubstackConfig))));
    +
    +        // when
    +        pubstackAnalyticsReporter.initialize();
    +        pubstackAnalyticsReporter.initialize();
    +
    +        // then
    +        verify(httpClient, times(2)).get(anyString(), anyLong());
    +        // just once for first initialization
    +        verify(auctionHandler).reportEvents();
    +        verify(setuidHandler).reportEvents();
    +        verify(auctionHandler).updateConfig(eq(true), eq("http://newendpoint/intake/auction"), eq("newScopeId"));
    +        verify(setuidHandler).updateConfig(eq(false), eq("http://newendpoint/intake/setuid"), eq("newScopeId"));
    +    }
    +
    +    @Test
    +    public void initializeShouldNotSendEventsAndUpdateConfigsWhenResponseStatusIsNot200() {
    +        // given
    +        given(httpClient.get(anyString(), anyLong())).willReturn(
    +                Future.succeededFuture(HttpClientResponse.of(400, null, null)));
    +
    +        // when
    +        pubstackAnalyticsReporter.initialize();
    +
    +        // then
    +        verify(vertx).setPeriodic(anyLong(), any());
    +        verify(httpClient).get(anyString(), anyLong());
    +        verifyZeroInteractions(auctionHandler);
    +        verifyZeroInteractions(setuidHandler);
    +    }
    +
    +    @Test
    +    public void initializeShouldNotSendEventsAndUpdateConfigsWhenCantParseResponse() {
    +        // given
    +        given(httpClient.get(anyString(), anyLong())).willReturn(
    +                Future.succeededFuture(HttpClientResponse.of(200, null, "{\"endpoint\" : {}}")));
    +
    +        // when
    +        pubstackAnalyticsReporter.initialize();
    +
    +        // then
    +        verify(vertx).setPeriodic(anyLong(), any());
    +        verify(httpClient).get(anyString(), anyLong());
    +        verifyZeroInteractions(auctionHandler);
    +        verifyZeroInteractions(setuidHandler);
    +    }
    +
    +    @Test
    +    public void shutdownShouldCallSendEventsOnAllEventHandlers() {
    +        // given and when
    +        pubstackAnalyticsReporter.shutdown();
    +
    +        // then
    +        verify(auctionHandler).reportEvents();
    +        verify(setuidHandler).reportEvents();
    +    }
    +
    +    @Test
    +    public void processEventShouldCallEventHandlerForAuction() {
    +        // given
    +        pubstackAnalyticsReporter = new PubstackAnalyticsReporter(properties, httpClient, jacksonMapper, vertx);
    +        // inject mocked handler to private fields without accessor method
    +        ReflectionTestUtils.setField(pubstackAnalyticsReporter, "eventHandlers",
    +                Collections.singletonMap(EventType.auction, auctionHandler));
    +        final AuctionEvent auctionEvent = AuctionEvent.builder().build();
    +
    +        // when
    +        pubstackAnalyticsReporter.processEvent(auctionEvent);
    +
    +        // then
    +        verify(auctionHandler).handle(same(auctionEvent));
    +    }
    +
    +    @Test
    +    public void processEventShouldCallEventHandlerForSetuid() {
    +        // given
    +        pubstackAnalyticsReporter = new PubstackAnalyticsReporter(properties, httpClient, jacksonMapper, vertx);
    +        // inject mocked handler to private fields without accessor method
    +        ReflectionTestUtils.setField(pubstackAnalyticsReporter, "eventHandlers",
    +                Collections.singletonMap(EventType.setuid, setuidHandler));
    +        final SetuidEvent setuidEvent = SetuidEvent.builder().build();
    +
    +        // when
    +        pubstackAnalyticsReporter.processEvent(setuidEvent);
    +
    +        // then
    +        verify(setuidHandler).handle(same(setuidEvent));
    +    }
    +
    +    @Test
    +    public void processEventShouldCallEventHandlerForCookieSync() {
    +        // given
    +        final PubstackEventHandler cookieSyncHandler = mock(PubstackEventHandler.class);
    +        pubstackAnalyticsReporter = new PubstackAnalyticsReporter(properties, httpClient, jacksonMapper, vertx);
    +        // inject mocked handler to private fields without accessor method
    +        ReflectionTestUtils.setField(pubstackAnalyticsReporter, "eventHandlers",
    +                Collections.singletonMap(EventType.cookiesync, cookieSyncHandler));
    +        final CookieSyncEvent cookieSyncEvent = CookieSyncEvent.builder().build();
    +
    +        // when
    +        pubstackAnalyticsReporter.processEvent(cookieSyncEvent);
    +
    +        // then
    +        verify(cookieSyncHandler).handle(same(cookieSyncEvent));
    +    }
    +
    +    @Test
    +    public void processEventShouldCallEventHandlerForAmp() {
    +        // given
    +        final PubstackEventHandler ampHandler = mock(PubstackEventHandler.class);
    +        pubstackAnalyticsReporter = new PubstackAnalyticsReporter(properties, httpClient, jacksonMapper, vertx);
    +        // inject mocked handler to private fields without accessor method
    +        ReflectionTestUtils.setField(pubstackAnalyticsReporter, "eventHandlers",
    +                Collections.singletonMap(EventType.amp, ampHandler));
    +        final AmpEvent ampEvent = AmpEvent.builder().build();
    +
    +        // when
    +        pubstackAnalyticsReporter.processEvent(ampEvent);
    +
    +        // then
    +        verify(ampHandler).handle(same(ampEvent));
    +    }
    +
    +    @Test
    +    public void processEventShouldCallEventHandlerForVideo() {
    +        // given
    +        final PubstackEventHandler videoHandler = mock(PubstackEventHandler.class);
    +        pubstackAnalyticsReporter = new PubstackAnalyticsReporter(properties, httpClient, jacksonMapper, vertx);
    +        // inject mocked handler to private fields without accessor method
    +        ReflectionTestUtils.setField(pubstackAnalyticsReporter, "eventHandlers",
    +                Collections.singletonMap(EventType.video, videoHandler));
    +
    +        final VideoEvent videoEvent = VideoEvent.builder().build();
    +
    +        // when
    +        pubstackAnalyticsReporter.processEvent(videoEvent);
    +
    +        // then
    +        verify(videoHandler).handle(same(videoEvent));
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/analytics/pubstack/PubstackEventHandlerTest.java b/src/test/java/org/prebid/server/analytics/pubstack/PubstackEventHandlerTest.java
    new file mode 100644
    index 00000000000..2ae7e4c0d7a
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/analytics/pubstack/PubstackEventHandlerTest.java
    @@ -0,0 +1,230 @@
    +package org.prebid.server.analytics.pubstack;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import io.vertx.core.Future;
    +import io.vertx.core.Vertx;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.analytics.model.AuctionEvent;
    +import org.prebid.server.analytics.model.SetuidEvent;
    +import org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties;
    +import org.prebid.server.auction.model.AuctionContext;
    +import org.prebid.server.cookie.UidsCookie;
    +import org.prebid.server.deals.model.DeepDebugLog;
    +import org.prebid.server.deals.model.TxnLog;
    +import org.prebid.server.execution.Timeout;
    +import org.prebid.server.vertx.http.HttpClient;
    +import org.prebid.server.vertx.http.model.HttpClientResponse;
    +import org.springframework.test.util.ReflectionTestUtils;
    +
    +import java.util.Set;
    +import java.util.concurrent.atomic.AtomicLong;
    +import java.util.concurrent.atomic.AtomicReference;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatCode;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyLong;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.times;
    +import static org.mockito.Mockito.verify;
    +import static org.mockito.Mockito.verifyZeroInteractions;
    +
    +public class PubstackEventHandlerTest extends VertxTest {
    +
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
    +    @Mock
    +    private Vertx vertx;
    +
    +    @Mock
    +    private HttpClient httpClient;
    +
    +    private PubstackEventHandler pubstackEventHandler;
    +
    +    @Before
    +    public void setUp() {
    +        given(vertx.setTimer(anyLong(), any())).willReturn(1L, 2L);
    +        final PubstackAnalyticsProperties properties = PubstackAnalyticsProperties.builder()
    +                .endpoint("http://endpoint.com")
    +                .scopeId("scopeId")
    +                .sizeBytes(100000)
    +                .count(100)
    +                .reportTtlMs(10000L)
    +                .timeoutMs(5000L)
    +                .build();
    +        pubstackEventHandler = new PubstackEventHandler(properties, true, "http://example.com", jacksonMapper,
    +                httpClient, vertx);
    +    }
    +
    +    @Test
    +    public void handleShouldNotAcceptEventsWhenNotEnabled() {
    +        // given
    +        final PubstackAnalyticsProperties properties = PubstackAnalyticsProperties.builder()
    +                .endpoint("http://endpoint.com")
    +                .scopeId("scopeId")
    +                .sizeBytes(1)
    +                .count(1)
    +                .reportTtlMs(10000L)
    +                .timeoutMs(5000L)
    +                .build();
    +        pubstackEventHandler = new PubstackEventHandler(properties, false, "http://example.com", jacksonMapper,
    +                httpClient, vertx);
    +
    +        // when
    +        pubstackEventHandler.handle(SetuidEvent.builder().bidder("bidder1").build());
    +
    +        // then
    +        @SuppressWarnings("unchecked") final AtomicReference> events =
    +                (AtomicReference>) ReflectionTestUtils
    +                        .getField(pubstackEventHandler, "events");
    +        assertThat(events.get()).isEmpty();
    +        verifyZeroInteractions(httpClient);
    +    }
    +
    +    @Test
    +    public void handleShouldAddEventWithScopeIdAndIncreaseByteSize() throws JsonProcessingException {
    +        // given and when
    +        final SetuidEvent setuidEvent = SetuidEvent.builder().bidder("bidder1").build();
    +        pubstackEventHandler.handle(setuidEvent);
    +
    +        // then
    +        final AtomicLong byteSize = (AtomicLong) ReflectionTestUtils.getField(pubstackEventHandler, "byteSize");
    +        @SuppressWarnings("unchecked") final AtomicReference> events =
    +                (AtomicReference>) ReflectionTestUtils
    +                        .getField(pubstackEventHandler, "events");
    +        final ObjectNode eventJsonNode = mapper.valueToTree(setuidEvent);
    +        eventJsonNode.put("scope", "scopeId");
    +        final String eventJsonRow = mapper.writeValueAsString(eventJsonNode);
    +        assertThat(byteSize.get()).isEqualTo(eventJsonRow.getBytes().length);
    +        assertThat(events.get()).hasSize(1)
    +                .containsOnly(eventJsonRow);
    +    }
    +
    +    @Test
    +    public void handleShouldSendEventsWhenMaxByteBufferSizeExceedsSize() {
    +        // given
    +        final PubstackAnalyticsProperties properties = PubstackAnalyticsProperties.builder()
    +                .endpoint("http://endpoint.com")
    +                .scopeId("scopeId")
    +                .sizeBytes(20)
    +                .count(100)
    +                .reportTtlMs(10000L)
    +                .timeoutMs(5000L)
    +                .build();
    +        pubstackEventHandler = new PubstackEventHandler(properties, true, "http://example.com", jacksonMapper,
    +                httpClient, vertx);
    +
    +        // when
    +        pubstackEventHandler.handle(SetuidEvent.builder().bidder("bidder1").build());
    +
    +        // then
    +        verify(httpClient).request(any(), anyString(), any(), (byte[]) any(), anyLong());
    +    }
    +
    +    @Test
    +    public void handleShouldSendEventsWhenMaxCountEventsBufferExceeds() {
    +        // given
    +        final PubstackAnalyticsProperties properties = PubstackAnalyticsProperties.builder()
    +                .endpoint("http://endpoint.com")
    +                .scopeId("scopeId")
    +                .sizeBytes(20000)
    +                .count(1)
    +                .reportTtlMs(10000L)
    +                .timeoutMs(5000L)
    +                .build();
    +        pubstackEventHandler = new PubstackEventHandler(properties, true, "http://example.com", jacksonMapper,
    +                httpClient, vertx);
    +
    +        // when
    +        pubstackEventHandler.handle(SetuidEvent.builder().bidder("bidder1").build());
    +        pubstackEventHandler.handle(SetuidEvent.builder().bidder("bidder2").build());
    +
    +        // then
    +        verify(httpClient).request(any(), anyString(), any(), (byte[]) any(), anyLong());
    +    }
    +
    +    @Test
    +    public void handleShouldBeAbleToEncodeAuctionEvent() {
    +        // given
    +        final AuctionEvent event = AuctionEvent.builder()
    +                .auctionContext(AuctionContext.builder()
    +                        .uidsCookie(mock(UidsCookie.class))
    +                        .timeout(mock(Timeout.class))
    +                        .txnLog(mock(TxnLog.class))
    +                        .deepDebugLog(mock(DeepDebugLog.class))
    +                        .build())
    +                .build();
    +
    +        // when and then
    +        assertThatCode(() -> pubstackEventHandler.handle(event))
    +                .doesNotThrowAnyException();
    +    }
    +
    +    @Test
    +    public void sendEventsShouldSendEventsAndResetSendConditionParameters() {
    +        // given
    +        given(httpClient.request(any(), anyString(), any(), (byte[]) any(), anyLong()))
    +                .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, null)));
    +
    +        pubstackEventHandler.handle(SetuidEvent.builder().bidder("bidder1").build());
    +
    +        // when
    +        pubstackEventHandler.reportEvents();
    +
    +        // then
    +        verify(vertx).cancelTimer(anyLong());
    +        // one time in constructor and second after the send request
    +        verify(vertx, times(2)).setTimer(anyLong(), any());
    +        final AtomicLong byteSize = (AtomicLong) ReflectionTestUtils.getField(pubstackEventHandler, "byteSize");
    +        assertThat(byteSize.get()).isEqualTo(0);
    +        final Long currentTimerId = (Long) ReflectionTestUtils.getField(pubstackEventHandler,
    +                "reportTimerId");
    +        assertThat(currentTimerId).isEqualTo(2);
    +    }
    +
    +    @Test
    +    public void updateConfigShouldSetNewValuesToEndpointScopeIdAndEnabledConfigs() {
    +        // given and when
    +        pubstackEventHandler.updateConfig(false, "newEndpoint", "newScope");
    +
    +        // then
    +        final Boolean enabled = (Boolean) ReflectionTestUtils.getField(pubstackEventHandler, "enabled");
    +        final String endpoint = (String) ReflectionTestUtils.getField(pubstackEventHandler, "endpoint");
    +        final String newScope = (String) ReflectionTestUtils.getField(pubstackEventHandler, "scopeId");
    +        assertThat(enabled).isFalse();
    +        assertThat(endpoint).isEqualTo("newEndpoint");
    +        assertThat(newScope).isEqualTo("newScope");
    +    }
    +
    +    @Test
    +    public void updateConfigShouldCancelReportTimerOnDisablingHandler() {
    +        // given and when
    +        pubstackEventHandler.updateConfig(false, "newEndpoint", "newScope");
    +
    +        // then
    +        verify(vertx).setTimer(anyLong(), any());
    +        verify(vertx).cancelTimer(anyLong());
    +    }
    +
    +    @Test
    +    public void updateConfigShouldSetReportTimerOnDisablingHandler() {
    +        // given and when
    +        pubstackEventHandler.updateConfig(false, "newEndpoint", "newScope");
    +        pubstackEventHandler.updateConfig(true, "newEndpoint", "newScope");
    +
    +        // then
    +        // first time on handler creation, second time on enabling after it was disabled
    +        verify(vertx, times(2)).setTimer(anyLong(), any());
    +        verify(vertx).cancelTimer(anyLong());
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java
    deleted file mode 100644
    index 5aa052baa56..00000000000
    --- a/src/test/java/org/prebid/server/auction/AmpRequestFactoryTest.java
    +++ /dev/null
    @@ -1,1441 +0,0 @@
    -package org.prebid.server.auction;
    -
    -import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.Banner;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.Format;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Publisher;
    -import com.iab.openrtb.request.Regs;
    -import com.iab.openrtb.request.Site;
    -import com.iab.openrtb.request.User;
    -import io.vertx.core.Future;
    -import io.vertx.core.MultiMap;
    -import io.vertx.core.http.HttpServerRequest;
    -import io.vertx.ext.web.RoutingContext;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.ArgumentCaptor;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.mockito.stubbing.Answer;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AuctionContext;
    -import org.prebid.server.exception.InvalidRequestException;
    -import org.prebid.server.metric.MetricName;
    -import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange;
    -import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAmp;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheBids;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheVastxml;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
    -import org.prebid.server.proto.openrtb.ext.request.ExtSite;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.request.Targeting;
    -
    -import java.math.BigDecimal;
    -import java.util.Arrays;
    -import java.util.HashMap;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.function.Function;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.singletonList;
    -import static java.util.function.Function.identity;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.tuple;
    -import static org.mockito.ArgumentMatchers.any;
    -import static org.mockito.ArgumentMatchers.anyList;
    -import static org.mockito.ArgumentMatchers.anyLong;
    -import static org.mockito.ArgumentMatchers.anyString;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -import static org.mockito.Mockito.verify;
    -import static org.mockito.Mockito.verifyZeroInteractions;
    -
    -public class AmpRequestFactoryTest extends VertxTest {
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private StoredRequestProcessor storedRequestProcessor;
    -    @Mock
    -    private AuctionRequestFactory auctionRequestFactory;
    -    @Mock
    -    private TimeoutResolver timeoutResolver;
    -
    -    private AmpRequestFactory factory;
    -    @Mock
    -    private HttpServerRequest httpRequest;
    -    @Mock
    -    private RoutingContext routingContext;
    -    @Mock
    -    private OrtbTypesResolver ortbTypesResolver;
    -    @Mock
    -    private ImplicitParametersExtractor implicitParametersExtractor;
    -    @Mock
    -    private FpdResolver fpdResolver;
    -
    -    @Before
    -    public void setUp() {
    -        given(timeoutResolver.resolve(any())).willReturn(2000L);
    -        given(timeoutResolver.adjustTimeout(anyLong())).willReturn(1900L);
    -
    -        given(httpRequest.getParam(eq("tag_id"))).willReturn("tagId");
    -        given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap());
    -        given(routingContext.request()).willReturn(httpRequest);
    -        given(fpdResolver.resolveApp(any(), any()))
    -                .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
    -        given(fpdResolver.resolveSite(any(), any()))
    -                .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
    -        given(fpdResolver.resolveUser(any(), any()))
    -                .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
    -        given(fpdResolver.resolveImpExt(any(), any())).willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
    -        given(fpdResolver.resolveBidRequestExt(any(), any())).willAnswer(invocationOnMock -> invocationOnMock
    -                .getArgument(0));
    -
    -        factory = new AmpRequestFactory(storedRequestProcessor, auctionRequestFactory, ortbTypesResolver,
    -                implicitParametersExtractor, fpdResolver, timeoutResolver, jacksonMapper);
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfRequestHasNoTagId() {
    -        // given
    -        given(httpRequest.getParam("tag_id")).willReturn(null);
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        verifyZeroInteractions(storedRequestProcessor);
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    -        assertThat(((InvalidRequestException) future.cause()).getMessages())
    -                .hasSize(1).containsOnly("AMP requests require an AMP tag_id");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfStoredBidRequestHasNoImp() {
    -        // given
    -        givenBidRequest(identity());
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    -        assertThat(((InvalidRequestException) future.cause()).getMessages())
    -                .hasSize(1).containsOnly("data for tag_id='tagId' does not define the required imp array.");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfStoredBidRequestHasMoreThenOneImp() {
    -        // given
    -        final Imp imp = Imp.builder().build();
    -        givenBidRequest(identity(), imp, imp);
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    -        assertThat(((InvalidRequestException) future.cause()).getMessages())
    -                .hasSize(1).containsOnly("data for tag_id 'tagId' includes 2 imp elements. Only one is allowed");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfStoredBidRequestHasApp() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .app(App.builder().build())
    -                .imp(singletonList(Imp.builder().build()))
    -                .build();
    -        given(storedRequestProcessor.processAmpRequest(anyString())).willReturn(Future.succeededFuture(bidRequest));
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    -        assertThat(((InvalidRequestException) future.cause()).getMessages())
    -                .hasSize(1).containsOnly("request.app must not exist in AMP stored requests.");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfStoredBidRequestHasNoExt() {
    -        // given
    -        givenBidRequest(identity(), Imp.builder().build());
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    -        assertThat(((InvalidRequestException) future.cause()).getMessages())
    -                .hasSize(1).containsOnly("AMP requests require Ext to be set");
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithDefaultPrebidValuesIfPrebidIsNull() {
    -        // given
    -        givenBidRequest(builder -> builder.ext(ExtRequest.empty()), Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        // result was wrapped to list because extracting method works different on iterable and not iterable objects,
    -        // which force to make type casting or exception handling in lambdas
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .containsExactly(ExtRequestPrebid.builder()
    -                        .targeting(ExtRequestTargeting.builder()
    -                                .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(2,
    -                                        singletonList(ExtGranularityRange.of(BigDecimal.valueOf(20),
    -                                                BigDecimal.valueOf(0.1))))))
    -                                .includewinners(true)
    -                                .includebidderkeys(true)
    -                                .build())
    -                        .cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(null, null),
    -                                ExtRequestPrebidCacheVastxml.of(null, null), null))
    -                        .channel(ExtRequestPrebidChannel.of("amp"))
    -                        .build());
    -    }
    -
    -    @Test
    -    public void shouldCallOrtbTypeResolver() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(null)),
    -                Imp.builder().build());
    -
    -        // when
    -        factory.fromRequest(routingContext, 0L).result();
    -
    -        // then
    -        verify(ortbTypesResolver).normalizeTargeting(any(), anyList(), any());
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithDefaultTargetingIfStoredBidRequestExtHasNoTargeting() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(null)),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt).isNotNull()
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getPricegranularity, ExtRequestTargeting::getIncludewinners)
    -                .containsExactly(tuple(
    -                        // default priceGranularity
    -                        mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of(
    -                                BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))),
    -                        // default includeWinners
    -                        true));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithDefaultIncludeWinnersIfStoredBidRequestExtTargetingHasNoIncludeWinners() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(
    -                                ExtRequestTargeting.builder()
    -                                        .pricegranularity(mapper.createObjectNode().put("foo", "bar"))
    -                                        .build())),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt).isNotNull()
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getIncludewinners, ExtRequestTargeting::getPricegranularity)
    -                // assert that includeWinners was set with default value and priceGranularity remained unchanged
    -                .containsExactly(
    -                        tuple(true, mapper.createObjectNode().put("foo", "bar")));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithIncludeWinnersFromStoredBidRequest() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(
    -                                ExtRequestTargeting.builder()
    -                                        .pricegranularity(mapper.createObjectNode().put("foo", "bar"))
    -                                        .includewinners(false)
    -                                        .build())),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt).isNotNull()
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getIncludewinners)
    -                .containsExactly(false);
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithDefaultIncludeBidderKeysIfStoredRequestExtTargetingHasNoIncludeBidderKeys() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(ExtRequestTargeting.builder().includewinners(false).build())),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt).isNotNull()
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getIncludewinners, ExtRequestTargeting::getIncludebidderkeys)
    -                // assert that includeBidderKeys was set with default value and includewinners remained unchanged
    -                .containsExactly(tuple(false, true));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithIncludeBidderKeysFromStoredBidRequest() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(
    -                                ExtRequestTargeting.builder()
    -                                        .pricegranularity(mapper.createObjectNode().put("foo", "bar"))
    -                                        .includebidderkeys(false)
    -                                        .build())),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt).isNotNull()
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getIncludebidderkeys)
    -                .containsExactly(false);
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithDefaultPriceGranularityIfStoredBidRequestExtTargetingHasNoPriceGranularity() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(ExtRequestTargeting.builder().includewinners(false).build())),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt).isNotNull()
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getIncludewinners, ExtRequestTargeting::getPricegranularity)
    -                // assert that priceGranularity was set with default value and includeWinners remained unchanged
    -                .containsExactly(
    -                        tuple(false, mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(
    -                                ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1)))))));
    -    }
    -
    -    private Answer answerWithFirstArgument() {
    -        return invocationOnMock -> invocationOnMock.getArguments()[0];
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithDefaultCachingIfStoredBidRequestExtHasNoCaching() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(null)),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt).isNotNull()
    -                .extracting(extBidRequest -> extBidRequest.getPrebid().getCache().getBids())
    -                .containsExactly(ExtRequestPrebidCacheBids.of(null, null));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithChannelIfStoredBidRequestExtHasNoChannel() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(null)),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(extBidRequest -> extBidRequest.getPrebid().getChannel())
    -                .containsExactly(ExtRequestPrebidChannel.of("amp"));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithChannelFromStoredBidRequest() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                                .channel(ExtRequestPrebidChannel.of("custom"))
    -                                .build())),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(extBidRequest -> extBidRequest.getPrebid().getChannel())
    -                .containsExactly(ExtRequestPrebidChannel.of("custom"));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithImpSecureEqualsToOneIfInitiallyItWasNotSecured() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(null)),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getSecure)
    -                .containsExactly(1);
    -    }
    -
    -    @Test
    -    public void shouldRespondWithBidRequestWithTestFlagOn() {
    -        // given
    -        given(httpRequest.getParam("debug")).willReturn("1");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .test(0)
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(auctionRequestFactory).fillImplicitParameters(captor.capture(), any(), any());
    -
    -        assertThat(captor.getValue().getTest()).isEqualTo(1);
    -    }
    -
    -    @Test
    -    public void shouldRespondWithBidRequestWithDebugFlagOn() {
    -        // given
    -        given(httpRequest.getParam("debug")).willReturn("1");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.of(ExtRequestPrebid.builder().debug(0).build())),
    -                Imp.builder().build());
    -
    -        // when
    -        factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(auctionRequestFactory).fillImplicitParameters(captor.capture(), any(), any());
    -
    -        final ExtRequest extRequest = captor.getValue().getExt();
    -        assertThat(extRequest.getPrebid().getDebug()).isEqualTo(1);
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOverriddenTagIdBySlotParamValue() {
    -        // given
    -        given(httpRequest.getParam("slot")).willReturn("Overridden-tagId");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(givenRequestExt(null)),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getTagid)
    -                .containsOnly("Overridden-tagId");
    -    }
    -
    -    @Test
    -    public void shouldSetBidRequestSiteExt() {
    -        // given
    -        given(httpRequest.getParam("curl")).willReturn("");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getSite)
    -                .extracting(Site::getExt)
    -                .containsOnly(ExtSite.of(1, null));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOverriddenSitePageByCurlParamValue() {
    -        // given
    -        given(httpRequest.getParam("curl")).willReturn("overridden-site-page");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty())
    -                        .site(Site.builder().page("will-be-overridden").build()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getSite)
    -                .extracting(Site::getPage, Site::getExt)
    -                .containsOnly(tuple("overridden-site-page", ExtSite.of(1, null)));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithSitePageContainingCurlParamValueWhenSitePreviouslyNotExistInRequest() {
    -        // given
    -        given(httpRequest.getParam("curl")).willReturn("overridden-site-page");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty())
    -                        .site(null),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getSite)
    -                .extracting(Site::getPage, Site::getExt)
    -                .containsOnly(tuple("overridden-site-page", ExtSite.of(1, null)));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithSitePublisherIdOverriddenWithAccountParamValue() {
    -        // given
    -        given(httpRequest.getParam("account")).willReturn("accountId");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty())
    -                        .site(Site.builder().publisher(Publisher.builder().id("will-be-overridden").build()).build()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getSite)
    -                .extracting(Site::getPublisher, Site::getExt)
    -                .containsOnly(tuple(
    -                        Publisher.builder().id("accountId").build(),
    -                        ExtSite.of(1, null)));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithSitePublisherIdFromAccountParamWhenSiteDoesNotExist() {
    -        // given
    -        given(httpRequest.getParam("account")).willReturn("accountId");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty())
    -                        .site(null),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getSite)
    -                .extracting(Site::getPublisher, Site::getExt)
    -                .containsOnly(tuple(
    -                        Publisher.builder().id("accountId").build(),
    -                        ExtSite.of(1, null)));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithSitePublisherIdFromAccountParamWhenSitePublisherDoesNotExist() {
    -        // given
    -        given(httpRequest.getParam("account")).willReturn("accountId");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty())
    -                        .site(Site.builder().build()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getSite)
    -                .extracting(Site::getPublisher, Site::getExt)
    -                .containsOnly(tuple(
    -                        Publisher.builder().id("accountId").build(),
    -                        ExtSite.of(1, null)));
    -    }
    -
    -    @Test
    -    public void shouldReturnRequestWithOverriddenBannerFormatByOverwriteWHParamsRespectingThemOverWH() {
    -        // given
    -        given(httpRequest.getParam("w")).willReturn("10");
    -        given(httpRequest.getParam("ow")).willReturn("1000");
    -        given(httpRequest.getParam("h")).willReturn("20");
    -        given(httpRequest.getParam("oh")).willReturn("2000");
    -        given(httpRequest.getParam("ms")).willReturn("44x88,66x99");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(1000, 2000), tuple(44, 88), tuple(66, 99));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOverriddenBannerFromOWAndHParamAndMultiListIfOHIsMissed() {
    -        // given
    -        given(httpRequest.getParam("ow")).willReturn("10");
    -        given(httpRequest.getParam("w")).willReturn("30");
    -        given(httpRequest.getParam("h")).willReturn("40");
    -        given(httpRequest.getParam("ms")).willReturn("50x60");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(10, 40), tuple(50, 60));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOverriddenBannerFromWAndOHParamAndMultiListIfOWIsMissed() {
    -        // given
    -        given(httpRequest.getParam("oh")).willReturn("20");
    -        given(httpRequest.getParam("w")).willReturn("30");
    -        given(httpRequest.getParam("h")).willReturn("40");
    -        given(httpRequest.getParam("ms")).willReturn("50x60");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(30, 20), tuple(50, 60));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithBannerFromHWParamsAndMultiList() {
    -        // given
    -        given(httpRequest.getParam("w")).willReturn("30");
    -        given(httpRequest.getParam("h")).willReturn("40");
    -        given(httpRequest.getParam("ms")).willReturn("50x60");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(30, 40), tuple(50, 60));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOverriddenBannerFromWAndHParamsIfOwOhAndMultiListAreMissed() {
    -        // given
    -        given(httpRequest.getParam("w")).willReturn("30");
    -        given(httpRequest.getParam("h")).willReturn("40");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(30, 40));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithUpdatedWidthForAllBannerFormatsWhenOnlyWIsPresentInParams() {
    -        // given
    -        given(httpRequest.getParam("w")).willReturn("30");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(asList(Format.builder().w(1).h(2).build(),
    -                                        Format.builder().w(3).h(4).build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(30, 2), tuple(30, 4));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithUpdatedHeightForAllBannerFormatsWhenOnlyHIsPresentInParams() {
    -        // given
    -        given(httpRequest.getParam("h")).willReturn("40");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(asList(Format.builder().w(1).h(2).build(),
    -                                        Format.builder().w(3).h(4).build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(1, 40), tuple(3, 40));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOverriddenBannerFormatsByMultiSizeParams() {
    -        // given
    -        given(httpRequest.getParam("ms")).willReturn("44x88,66x99");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(44, 88), tuple(66, 99));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOriginalBannerFormatsWhenMultiSizeParamContainsCompletelyInvalidValue() {
    -        // given
    -        given(httpRequest.getParam("ms")).willReturn(",");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(1, 2));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOriginBannerFormatsWhenMultiSizeParamContainsAtLeastOneInvalidValue() {
    -        // given
    -        given(httpRequest.getParam("ms")).willReturn(",33x,44x77,abc,");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(1, 2));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestOverriddenBannerFormatsWhenMsParamSizePairHasOneInvalidValue() {
    -        // given
    -        given(httpRequest.getParam("ms")).willReturn("900xZ");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(900, 0));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOriginBannerFormatsWhenMultiSizeParamContainsAtLeastOneZeroPairSize() {
    -        // given
    -        given(httpRequest.getParam("ms")).willReturn("44x77, 0x0");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(1, 2));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOverriddenBannerFormatsWhenMultiSizeParamContainsPartiallyInvalidParams() {
    -        // given
    -        given(httpRequest.getParam("ms")).willReturn("33x,44x77");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(33, 0), tuple(44, 77));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOriginBannerFormatsWhenAllParametersAreZero() {
    -        // given
    -        given(httpRequest.getParam("ow")).willReturn("0");
    -        given(httpRequest.getParam("oh")).willReturn("0");
    -        given(httpRequest.getParam("w")).willReturn("0");
    -        given(httpRequest.getParam("h")).willReturn("0");
    -        given(httpRequest.getParam("ms")).willReturn("0x0");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(1, 2));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOverriddenBannerWhenInvalidParamTreatedAsZeroValue() {
    -        // given
    -        given(httpRequest.getParam("ow")).willReturn("100");
    -        given(httpRequest.getParam("oh")).willReturn("invalid");
    -        given(httpRequest.getParam("h")).willReturn("200");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder()
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder()
    -                                        .w(1)
    -                                        .h(2)
    -                                        .build()))
    -                                .build()).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .extracting(Format::getW, Format::getH)
    -                .containsOnly(tuple(100, 200));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOverriddenTmaxWhenTimeoutParamIsAvailable() {
    -        // given
    -        given(httpRequest.getParam("timeout")).willReturn("1000");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getTmax()).isEqualTo(1000L);
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithUnmodifiedUserWhenGdprConsentParamIsNullOrBlank() {
    -        // given
    -        given(httpRequest.getParam("gdpr_consent")).willReturn(null, "");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .user(User.builder()
    -                                .ext(ExtUser.builder().consent("should-remain").build())
    -                                .build())
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest firstResult = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -        final BidRequest secondResult = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        final User expectedUser = User.builder()
    -                .ext(ExtUser.builder().consent("should-remain").build())
    -                .build();
    -
    -        assertThat(firstResult.getUser()).isEqualTo(expectedUser);
    -        assertThat(secondResult.getUser()).isEqualTo(expectedUser);
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithOverriddenUserExtConsentWhenGdprConsentParamIsValid() {
    -        // given
    -        given(httpRequest.getParam("gdpr_consent")).willReturn("BONV8oqONXwgmADACHENAO7pqzAAppY");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .user(User.builder()
    -                                .id("1")
    -                                .ext(ExtUser.builder().consent("should-be-overridden").build())
    -                                .build())
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest result = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(result.getUser())
    -                .isEqualTo(User.builder()
    -                        .id("1")
    -                        .ext(ExtUser.builder().consent("BONV8oqONXwgmADACHENAO7pqzAAppY").build())
    -                        .build());
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithNewUserThatContainsUserExtConsentWhenInitialUserIsMissing() {
    -        // given
    -        given(httpRequest.getParam("gdpr_consent")).willReturn("BONV8oqONXwgmADACHENAO7pqzAAppY");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest result = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(result.getUser())
    -                .isEqualTo(User.builder()
    -                        .ext(ExtUser.builder().consent("BONV8oqONXwgmADACHENAO7pqzAAppY").build())
    -                        .build());
    -    }
    -
    -    @Test
    -    public void shouldKeepEmptyUserWhenGdprConsentIsInvalid() {
    -        // given
    -        given(httpRequest.getParam("gdpr_consent")).willReturn("consent-value");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .user(User.builder().build())
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest result = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(result.getUser())
    -                .isEqualTo(User.builder().build());
    -    }
    -
    -    @Test
    -    public void shouldReturnAddErrorToAuctionContextWhenPrivacyIsNotValid() {
    -        // given
    -        given(httpRequest.getParam("gdpr_consent")).willReturn("consent-value");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .user(User.builder().build())
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        factory.fromRequest(routingContext, 0L).result();
    -
    -        // then
    -        @SuppressWarnings("unchecked") final ArgumentCaptor> errorsCaptor = ArgumentCaptor.forClass(
    -                List.class);
    -        verify(auctionRequestFactory).toAuctionContext(any(), any(), any(), errorsCaptor.capture(), anyLong(), any());
    -        assertThat(errorsCaptor.getValue()).contains("Amp request parameter consent_string or gdpr_consent have"
    -                + " invalid format: consent-value");
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithExtPrebidDataBiddersUpdatedByFpdResolver() throws JsonProcessingException {
    -        // given
    -        given(httpRequest.getParam("targeting"))
    -                .willReturn(mapper.writeValueAsString(Targeting.of(Arrays.asList("appnexus", "rubicon"), null, null)));
    -
    -        given(fpdResolver.resolveBidRequestExt(any(), any()))
    -                .willReturn(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .data(ExtRequestPrebidData.of(Arrays.asList("appnexus", "rubicon"))).build()));
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        verify(fpdResolver).resolveBidRequestExt(any(), any());
    -        assertThat(request)
    -                .extracting(BidRequest::getExt)
    -                .containsOnly(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .data(ExtRequestPrebidData.of(Arrays.asList("appnexus", "rubicon"))).build()));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestImpExtContextDataWithTargetingAttributes() throws JsonProcessingException {
    -        // given
    -        given(httpRequest.getParam("targeting"))
    -                .willReturn(mapper.writeValueAsString(Targeting.of(Arrays.asList("appnexus", "rubicon"), null, null)));
    -
    -        given(fpdResolver.resolveImpExt(any(), any()))
    -                .willReturn(mapper.createObjectNode().set("context", mapper.createObjectNode()
    -                        .set("data", mapper.createObjectNode().put("attr1", "value1").put("attr2", "value2"))));
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        verify(fpdResolver).resolveBidRequestExt(any(), any());
    -        assertThat(singletonList(request))
    -                .flatExtracting(BidRequest::getImp)
    -                .containsOnly(Imp.builder().secure(1).ext(mapper.createObjectNode().set("context",
    -                        mapper.createObjectNode().set("data", mapper.createObjectNode().put("attr1", "value1")
    -                                .put("attr2", "value2")))).build());
    -    }
    -
    -    @Test
    -    public void shouldThrowInvalidRequestExceptionWhenTargetingHasTypeOtherToObject() {
    -        // given
    -        given(httpRequest.getParam("targeting")).willReturn("[\"a\"]", null, null);
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final Future result = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(result.failed()).isTrue();
    -        assertThat(result.cause())
    -                .isInstanceOf(InvalidRequestException.class)
    -                .hasMessage("Error decoding targeting, expected type is `object` but was ARRAY");
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithoutRegsExtWhenNoPrivacyPolicyIsExist() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder.ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest result = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(result.getRegs()).isNull();
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithRegsExtUsPrivacyWhenUsPrivacyParamIsValid() {
    -        // given
    -        given(httpRequest.getParam("gdpr_consent")).willReturn("1N--");
    -
    -        givenBidRequest(
    -                builder -> builder.ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest result = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(result.getRegs())
    -                .isEqualTo(Regs.of(null, ExtRegs.of(null, "1N--")));
    -    }
    -
    -    @Test
    -    public void shouldReturnBidRequestWithRegsExtUsPrivacyWhenConsentStringIsValid() {
    -        // given
    -        given(httpRequest.getParam("consent_string")).willReturn("1Y-N");
    -
    -        givenBidRequest(
    -                builder -> builder
    -                        .user(User.builder().build())
    -                        .regs(Regs.of(1, ExtRegs.of(1, "replaced")))
    -                        .ext(ExtRequest.empty()),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest result = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(result.getUser())
    -                .isEqualTo(User.builder().build());
    -        assertThat(result.getRegs())
    -                .isEqualTo(Regs.of(1, ExtRegs.of(1, "1Y-N")));
    -    }
    -
    -    @Test
    -    public void shouldPassExtPrebidDebugFlagIfPresent() {
    -        // given
    -        givenBidRequest(
    -                builder -> builder
    -                        .ext(ExtRequest.of(ExtRequestPrebid.builder().debug(1).build())),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getDebug)
    -                .containsOnly(1);
    -    }
    -
    -    @SuppressWarnings("unchecked")
    -    @Test
    -    public void shouldReturnBidRequestWithCreatedExtPrebidAmpData() {
    -        // given
    -        given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap()
    -                .add("queryParam1", "value1")
    -                .add("queryParam2", "value2"));
    -
    -        givenBidRequest(
    -                builder -> builder.ext(ExtRequest.of(null)),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest result = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        final Map expectedAmpData = new HashMap<>();
    -        expectedAmpData.put("queryParam1", "value1");
    -        expectedAmpData.put("queryParam2", "value2");
    -        assertThat(singletonList(result))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getAmp)
    -                .extracting(ExtRequestPrebidAmp::getData)
    -                .containsOnly(expectedAmpData);
    -    }
    -
    -    @SuppressWarnings("unchecked")
    -    @Test
    -    public void shouldReturnBidRequestWithUpdatedExtPrebidAmpData() {
    -        // given
    -        given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap()
    -                .add("queryParam1", "value1")
    -                .add("queryParam2", "value2"));
    -
    -        final Map existingAmpData = new HashMap<>();
    -        existingAmpData.put("queryParam2", "value2InRequest");
    -        existingAmpData.put("queryParam3", "value3");
    -        givenBidRequest(
    -                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .amp(ExtRequestPrebidAmp.of(existingAmpData))
    -                        .build())),
    -                Imp.builder().build());
    -
    -        // when
    -        final BidRequest result = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        final Map expectedAmpData = new HashMap<>();
    -        expectedAmpData.put("queryParam1", "value1");
    -        expectedAmpData.put("queryParam2", "value2");
    -        expectedAmpData.put("queryParam3", "value3");
    -        assertThat(singletonList(result))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getAmp)
    -                .extracting(ExtRequestPrebidAmp::getData)
    -                .containsOnly(expectedAmpData);
    -    }
    -
    -    private void givenBidRequest(
    -            Function bidRequestBuilderCustomizer,
    -            Imp... imps) {
    -        final List impList = imps.length > 0 ? asList(imps) : null;
    -
    -        final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(BidRequest.builder().imp(impList)).build();
    -
    -        given(storedRequestProcessor.processAmpRequest(anyString())).willReturn(Future.succeededFuture(bidRequest));
    -
    -        given(auctionRequestFactory.fillImplicitParameters(any(), any(), any())).willAnswer(answerWithFirstArgument());
    -        given(auctionRequestFactory.validateRequest(any())).willAnswer(answerWithFirstArgument());
    -        given(auctionRequestFactory.toAuctionContext(any(), any(), eq(MetricName.amp), anyList(), anyLong(), any()))
    -                .willAnswer(invocationOnMock -> Future.succeededFuture(
    -                        AuctionContext.builder()
    -                                .bidRequest((BidRequest) invocationOnMock.getArguments()[1])
    -                                .build()));
    -    }
    -
    -    private static ExtRequest givenRequestExt(ExtRequestTargeting extRequestTargeting) {
    -        return ExtRequest.of(ExtRequestPrebid.builder()
    -                .targeting(extRequestTargeting)
    -                .build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java
    deleted file mode 100644
    index b491a3f3ad0..00000000000
    --- a/src/test/java/org/prebid/server/auction/AuctionRequestFactoryTest.java
    +++ /dev/null
    @@ -1,1696 +0,0 @@
    -package org.prebid.server.auction;
    -
    -import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.fasterxml.jackson.databind.JsonNode;
    -import com.fasterxml.jackson.databind.node.TextNode;
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.Banner;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.Device;
    -import com.iab.openrtb.request.Geo;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Publisher;
    -import com.iab.openrtb.request.Site;
    -import com.iab.openrtb.request.Source;
    -import com.iab.openrtb.request.User;
    -import com.iab.openrtb.request.Video;
    -import io.vertx.core.Future;
    -import io.vertx.core.buffer.Buffer;
    -import io.vertx.core.http.CaseInsensitiveHeaders;
    -import io.vertx.core.http.HttpServerRequest;
    -import io.vertx.ext.web.RoutingContext;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.assertion.FutureAssertion;
    -import org.prebid.server.auction.model.AuctionContext;
    -import org.prebid.server.auction.model.IpAddress;
    -import org.prebid.server.bidder.BidderCatalog;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.cookie.UidsCookieService;
    -import org.prebid.server.cookie.model.UidWithExpiry;
    -import org.prebid.server.cookie.proto.Uids;
    -import org.prebid.server.exception.BlacklistedAccountException;
    -import org.prebid.server.exception.BlacklistedAppException;
    -import org.prebid.server.exception.InvalidRequestException;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.exception.UnauthorizedAccountException;
    -import org.prebid.server.execution.Timeout;
    -import org.prebid.server.execution.TimeoutFactory;
    -import org.prebid.server.geolocation.model.GeoInfo;
    -import org.prebid.server.identity.IdGenerator;
    -import org.prebid.server.metric.MetricName;
    -import org.prebid.server.privacy.ccpa.Ccpa;
    -import org.prebid.server.privacy.gdpr.model.TcfContext;
    -import org.prebid.server.privacy.model.Privacy;
    -import org.prebid.server.privacy.model.PrivacyContext;
    -import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange;
    -import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity;
    -import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity;
    -import org.prebid.server.proto.openrtb.ext.request.ExtPublisher;
    -import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheBids;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheVastxml;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
    -import org.prebid.server.proto.openrtb.ext.request.ExtSite;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust;
    -import org.prebid.server.settings.ApplicationSettings;
    -import org.prebid.server.settings.model.Account;
    -import org.prebid.server.validation.RequestValidator;
    -import org.prebid.server.validation.model.ValidationResult;
    -
    -import java.math.BigDecimal;
    -import java.time.Clock;
    -import java.time.Instant;
    -import java.time.ZoneId;
    -import java.util.List;
    -import java.util.Map;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.singletonList;
    -import static java.util.Collections.singletonMap;
    -import static org.apache.commons.lang3.StringUtils.EMPTY;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.tuple;
    -import static org.mockito.ArgumentMatchers.any;
    -import static org.mockito.ArgumentMatchers.anyLong;
    -import static org.mockito.ArgumentMatchers.anyString;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -import static org.mockito.Mockito.mock;
    -import static org.mockito.Mockito.never;
    -import static org.mockito.Mockito.verify;
    -import static org.mockito.Mockito.verifyZeroInteractions;
    -
    -public class AuctionRequestFactoryTest extends VertxTest {
    -
    -    private static final List BLACKLISTED_APPS = singletonList("bad_app");
    -    private static final List BLACKLISTED_ACCOUNTS = singletonList("bad_acc");
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private StoredRequestProcessor storedRequestProcessor;
    -    @Mock
    -    private ImplicitParametersExtractor paramsExtractor;
    -    @Mock
    -    private IpAddressHelper ipAddressHelper;
    -    @Mock
    -    private UidsCookieService uidsCookieService;
    -    @Mock
    -    private BidderCatalog bidderCatalog;
    -    @Mock
    -    private RequestValidator requestValidator;
    -    @Mock
    -    private InterstitialProcessor interstitialProcessor;
    -    @Mock
    -    private ApplicationSettings applicationSettings;
    -    @Mock
    -    private IdGenerator idGenerator;
    -    @Mock
    -    private PrivacyEnforcementService privacyEnforcementService;
    -
    -    private AuctionRequestFactory factory;
    -    @Mock
    -    private RoutingContext routingContext;
    -    @Mock
    -    private HttpServerRequest httpRequest;
    -    @Mock
    -    private OrtbTypesResolver ortbTypesResolver;
    -    @Mock
    -    private TimeoutResolver timeoutResolver;
    -    @Mock
    -    private TimeoutFactory timeoutFactory;
    -
    -    @Before
    -    public void setUp() {
    -        given(interstitialProcessor.process(any())).will(invocationOnMock -> invocationOnMock.getArgument(0));
    -        given(idGenerator.generateId()).willReturn(null);
    -
    -        given(routingContext.request()).willReturn(httpRequest);
    -        given(httpRequest.headers()).willReturn(new CaseInsensitiveHeaders());
    -
    -        given(timeoutResolver.resolve(any())).willReturn(2000L);
    -        given(timeoutResolver.adjustTimeout(anyLong())).willReturn(1900L);
    -
    -        given(privacyEnforcementService.contextFromBidRequest(any(), any(), any(), any()))
    -                .willReturn(Future.succeededFuture(PrivacyContext.of(
    -                        Privacy.of("0", EMPTY, Ccpa.EMPTY, 0),
    -                        TcfContext.empty())));
    -
    -        factory = new AuctionRequestFactory(
    -                Integer.MAX_VALUE,
    -                false,
    -                false,
    -                "USD",
    -                BLACKLISTED_APPS,
    -                BLACKLISTED_ACCOUNTS,
    -                storedRequestProcessor,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                uidsCookieService,
    -                bidderCatalog,
    -                requestValidator,
    -                interstitialProcessor,
    -                ortbTypesResolver,
    -                timeoutResolver,
    -                timeoutFactory,
    -                applicationSettings,
    -                idGenerator,
    -                privacyEnforcementService,
    -                jacksonMapper);
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfRequestBodyIsMissing() {
    -        // given
    -        given(routingContext.getBody()).willReturn(null);
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause())
    -                .isInstanceOf(InvalidRequestException.class)
    -                .hasMessage("Incoming request has no body");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfAccountIsEnforcedAndIdIsNotProvided() {
    -        // given
    -        factory = new AuctionRequestFactory(
    -                1000,
    -                true,
    -                false,
    -                "USD",
    -                BLACKLISTED_APPS,
    -                BLACKLISTED_ACCOUNTS,
    -                storedRequestProcessor,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                uidsCookieService,
    -                bidderCatalog,
    -                requestValidator,
    -                interstitialProcessor,
    -                ortbTypesResolver,
    -                timeoutResolver,
    -                timeoutFactory,
    -                applicationSettings,
    -                idGenerator,
    -                privacyEnforcementService,
    -                jacksonMapper);
    -
    -        givenValidBidRequest();
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        verify(applicationSettings, never()).getAccountById(any(), any());
    -
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause())
    -                .isInstanceOf(UnauthorizedAccountException.class)
    -                .hasMessage("Unauthorized account id: ");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfAccountIsEnforcedAndFailedGetAccountById() {
    -        // given
    -        factory = new AuctionRequestFactory(
    -                1000,
    -                true,
    -                false,
    -                "USD",
    -                BLACKLISTED_APPS,
    -                BLACKLISTED_ACCOUNTS,
    -                storedRequestProcessor,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                uidsCookieService,
    -                bidderCatalog,
    -                requestValidator,
    -                interstitialProcessor,
    -                ortbTypesResolver,
    -                timeoutResolver,
    -                timeoutFactory,
    -                applicationSettings,
    -                idGenerator,
    -                privacyEnforcementService,
    -                jacksonMapper);
    -
    -        given(applicationSettings.getAccountById(any(), any()))
    -                .willReturn(Future.failedFuture(new PreBidException("Not found")));
    -
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .app(App.builder()
    -                        .publisher(Publisher.builder().id("absentId").build())
    -                        .build())
    -                .build();
    -
    -        givenBidRequest(bidRequest);
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        verify(applicationSettings).getAccountById(eq("absentId"), any());
    -
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause())
    -                .isInstanceOf(UnauthorizedAccountException.class)
    -                .hasMessage("Unauthorized account id: absentId");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() {
    -        // given
    -        factory = new AuctionRequestFactory(
    -                1,
    -                false,
    -                false,
    -                "USD",
    -                BLACKLISTED_APPS,
    -                BLACKLISTED_ACCOUNTS,
    -                storedRequestProcessor,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                uidsCookieService,
    -                bidderCatalog,
    -                requestValidator,
    -                interstitialProcessor,
    -                ortbTypesResolver,
    -                timeoutResolver,
    -                timeoutFactory,
    -                applicationSettings,
    -                idGenerator,
    -                privacyEnforcementService,
    -                jacksonMapper);
    -
    -        given(routingContext.getBody()).willReturn(Buffer.buffer("body"));
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause())
    -                .isInstanceOf(InvalidRequestException.class)
    -                .hasMessage("Request size exceeded max size of 1 bytes.");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfRequestBodyCouldNotBeParsed() {
    -        // given
    -        given(routingContext.getBody()).willReturn(Buffer.buffer("body"));
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    -        assertThat(((InvalidRequestException) future.cause()).getMessages()).hasSize(1)
    -                .element(0).asString().startsWith("Error decoding bidRequest: Unrecognized token 'body'");
    -    }
    -
    -    @Test
    -    public void shouldCallOrtbFieldsResolver() {
    -        // given
    -        givenValidBidRequest();
    -
    -        // when
    -        factory.fromRequest(routingContext, 0L).result();
    -
    -        // then
    -        verify(ortbTypesResolver).normalizeBidRequest(any(), any(), any());
    -    }
    -
    -    @Test
    -    public void shouldSetFieldsFromHeadersIfBodyFieldsEmptyForIpv4() {
    -        // given
    -        givenValidBidRequest();
    -
    -        givenImplicitParams("http://example.com", "example.com", "192.168.244.1", IpAddress.IP.v4, "UnitTest");
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite()).isEqualTo(Site.builder()
    -                .page("http://example.com")
    -                .domain("example.com")
    -                .ext(ExtSite.of(0, null))
    -                .build());
    -        assertThat(request.getDevice())
    -                .isEqualTo(Device.builder().ip("192.168.244.1").ua("UnitTest").build());
    -    }
    -
    -    @Test
    -    public void shouldSetFieldsFromHeadersIfBodyFieldsInvalidForIpv4() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .device(Device.builder().ip("127.0.0.1").build())
    -                .build();
    -
    -        givenBidRequest(bidRequest);
    -
    -        given(ipAddressHelper.toIpAddress(eq("127.0.0.1"))).willReturn(null);
    -
    -        givenImplicitParams("http://example.com", "example.com", "192.168.244.1", IpAddress.IP.v4, "UnitTest");
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite()).isEqualTo(Site.builder()
    -                .page("http://example.com")
    -                .domain("example.com")
    -                .ext(ExtSite.of(0, null))
    -                .build());
    -        assertThat(request.getDevice())
    -                .isEqualTo(Device.builder().ip("192.168.244.1").ua("UnitTest").build());
    -    }
    -
    -    @Test
    -    public void shouldSetFieldsFromHeadersIfBodyFieldsEmptyForIpv6() {
    -        // given
    -        givenValidBidRequest();
    -
    -        givenImplicitParams(
    -                "http://example.com",
    -                "example.com",
    -                "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
    -                IpAddress.IP.v6,
    -                "UnitTest");
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite()).isEqualTo(Site.builder()
    -                .page("http://example.com")
    -                .domain("example.com")
    -                .ext(ExtSite.of(0, null))
    -                .build());
    -        assertThat(request.getDevice())
    -                .isEqualTo(Device.builder().ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334").ua("UnitTest").build());
    -    }
    -
    -    @Test
    -    public void shouldSetFieldsFromHeadersIfBodyFieldsInvalidForIpv6() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .device(Device.builder().ipv6("::1").build())
    -                .build();
    -
    -        givenBidRequest(bidRequest);
    -
    -        given(ipAddressHelper.toIpAddress(eq("::1"))).willReturn(null);
    -
    -        givenImplicitParams(
    -                "http://example.com",
    -                "example.com",
    -                "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
    -                IpAddress.IP.v6,
    -                "UnitTest");
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite()).isEqualTo(Site.builder()
    -                .page("http://example.com")
    -                .domain("example.com")
    -                .ext(ExtSite.of(0, null))
    -                .build());
    -        assertThat(request.getDevice())
    -                .isEqualTo(Device.builder().ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334").ua("UnitTest").build());
    -    }
    -
    -    @Test
    -    public void shouldSetAnonymizedIpv6FromField() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .device(Device.builder().ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334").build())
    -                .build();
    -
    -        givenBidRequest(bidRequest);
    -
    -        given(ipAddressHelper.toIpAddress(eq("2001:0db8:85a3:0000:0000:8a2e:0370:7334")))
    -                .willReturn(IpAddress.of("2001:0db8:85a3:0000::", IpAddress.IP.v6));
    -
    -        givenImplicitParams(
    -                "http://example.com",
    -                "example.com",
    -                "1111:2222:3333:4444:5555:6666:7777:8888",
    -                IpAddress.IP.v6,
    -                "UnitTest");
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite()).isEqualTo(Site.builder()
    -                .page("http://example.com")
    -                .domain("example.com")
    -                .ext(ExtSite.of(0, null))
    -                .build());
    -        assertThat(request.getDevice())
    -                .isEqualTo(Device.builder().ipv6("2001:0db8:85a3:0000::").ua("UnitTest").build());
    -    }
    -
    -    @Test
    -    public void shouldUpdateImpsWithSecurityOneIfRequestIsSecuredAndImpSecurityNotDefined() {
    -        // given
    -        givenBidRequest(BidRequest.builder().imp(singletonList(Imp.builder().build())).build());
    -        given(paramsExtractor.secureFrom(any())).willReturn(1);
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getImp()).extracting(Imp::getSecure).containsOnly(1);
    -    }
    -
    -    @Test
    -    public void shouldNotUpdateImpsWithSecurityOneIfRequestIsSecureAndImpSecurityIsZero() {
    -        // given
    -        givenBidRequest(BidRequest.builder().imp(singletonList(Imp.builder().secure(0).build())).build());
    -        given(paramsExtractor.secureFrom(any())).willReturn(1);
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getImp()).extracting(Imp::getSecure).containsOnly(0);
    -    }
    -
    -    @Test
    -    public void shouldUpdateImpsOnlyWithNotDefinedSecurityWithSecurityOneIfRequestIsSecure() {
    -        // given
    -        givenBidRequest(BidRequest.builder().imp(asList(Imp.builder().build(), Imp.builder().secure(0).build()))
    -                .build());
    -        given(paramsExtractor.secureFrom(any())).willReturn(1);
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getImp()).extracting(Imp::getSecure).containsOnly(1, 0);
    -    }
    -
    -    @Test
    -    public void shouldNotUpdateImpsWithSecurityOneIfRequestIsNotSecureAndImpSecurityIsNotDefined() {
    -        // given
    -        givenBidRequest(BidRequest.builder().imp(singletonList(Imp.builder().build())).build());
    -        given(paramsExtractor.secureFrom(any())).willReturn(0);
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getImp()).extracting(Imp::getSecure).containsNull();
    -    }
    -
    -    @Test
    -    public void shouldNotSetFieldsFromHeadersIfRequestFieldsNotEmpty() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .site(Site.builder().domain("test.com").page("http://test.com")
    -                        .ext(ExtSite.of(0, null)).build())
    -                .device(Device.builder().ua("UnitTestUA").ip("56.76.12.3").build())
    -                .user(User.builder().id("userId").build())
    -                .cur(singletonList("USD"))
    -                .tmax(2000L)
    -                .at(1)
    -                .build();
    -
    -        givenBidRequest(bidRequest);
    -
    -        given(ipAddressHelper.toIpAddress(eq("56.76.12.3")))
    -                .willReturn(IpAddress.of("56.76.12.3", IpAddress.IP.v4));
    -
    -        givenImplicitParams(
    -                "http://anotherexample.com", "anotherexample.com", "192.168.244.2", IpAddress.IP.v4, "UnitTest2");
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request).isSameAs(bidRequest);
    -    }
    -
    -    @Test
    -    public void shouldSetSiteExtIfNoReferer() {
    -        // given
    -        givenValidBidRequest();
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite())
    -                .extracting(Site::getExt)
    -                .containsOnly(ExtSite.of(0, null));
    -    }
    -
    -    @Test
    -    public void shouldNotSetSitePageIfNoReferer() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder().domain("home.com").build())
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite()).isEqualTo(
    -                Site.builder().domain("home.com").ext(ExtSite.of(0, null)).build());
    -    }
    -
    -    @Test
    -    public void shouldNotSetSitePageIfDomainCouldNotBeDerived() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder().domain("home.com").build())
    -                .build());
    -
    -        given(paramsExtractor.refererFrom(any())).willReturn("http://not-valid-site");
    -        given(paramsExtractor.domainFrom(anyString())).willThrow(new PreBidException("Couldn't derive domain"));
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite()).isEqualTo(
    -                Site.builder().domain("home.com").ext(ExtSite.of(0, null)).build());
    -    }
    -
    -    @Test
    -    public void shouldSetSiteExtAmpIfSiteHasNoExt() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder().domain("test.com").page("http://test.com").build())
    -                .build());
    -        givenImplicitParams(
    -                "http://anotherexample.com", "anotherexample.com", "192.168.244.2", IpAddress.IP.v4, "UnitTest2");
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite()).isEqualTo(
    -                Site.builder().domain("test.com").page("http://test.com")
    -                        .ext(ExtSite.of(0, null)).build());
    -    }
    -
    -    @Test
    -    public void shouldSetSiteExtAmpIfSiteExtHasNoAmp() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder().domain("test.com").page("http://test.com")
    -                        .ext(ExtSite.of(null, null)).build())
    -                .build());
    -        givenImplicitParams(
    -                "http://anotherexample.com", "anotherexample.com", "192.168.244.2", IpAddress.IP.v4, "UnitTest2");
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite()).isEqualTo(
    -                Site.builder().domain("test.com").page("http://test.com")
    -                        .ext(ExtSite.of(0, null)).build());
    -    }
    -
    -    @Test
    -    public void shouldSetSiteExtAmpIfNoReferer() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder().domain("test.com").page("http://test.com").build())
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSite()).isEqualTo(
    -                Site.builder().domain("test.com").page("http://test.com")
    -                        .ext(ExtSite.of(0, null)).build());
    -    }
    -
    -    @Test
    -    public void shouldSetUserExtDigitrustPerfIfNotDefined() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .user(User.builder()
    -                        .ext(ExtUser.builder()
    -                                .digitrust(ExtUserDigiTrust.of("id", 123, null))
    -                                .build())
    -                        .build())
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getUser().getExt())
    -                .isEqualTo(ExtUser.builder()
    -                        .digitrust(ExtUserDigiTrust.of("id", 123, 0))
    -                        .build());
    -    }
    -
    -    @Test
    -    public void shouldSetSourceTidIfNotDefined() {
    -        // given
    -        given(idGenerator.generateId()).willReturn("f6965ea7-f281-4eb9-9de2-560a52d954a3");
    -
    -        givenBidRequest(BidRequest.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getSource())
    -                .isEqualTo(Source.builder().tid("f6965ea7-f281-4eb9-9de2-560a52d954a3").build());
    -    }
    -
    -    @Test
    -    public void shouldSetDefaultAtIfInitialValueIsEqualsToZero() {
    -        // given
    -        givenBidRequest(BidRequest.builder().at(0).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getAt()).isEqualTo(1);
    -    }
    -
    -    @Test
    -    public void shouldSetDefaultAtIfInitialValueIsEqualsToNull() {
    -        // given
    -        givenBidRequest(BidRequest.builder().at(null).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getAt()).isEqualTo(1);
    -    }
    -
    -    @Test
    -    public void shouldSetCurrencyIfMissedInRequestAndPresentInAdServerCurrencyConfig() {
    -        // given
    -        givenBidRequest(BidRequest.builder().cur(null).build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getCur()).isEqualTo(singletonList("USD"));
    -    }
    -
    -    @Test
    -    public void shouldSetTimeoutFromTimeoutResolver() {
    -        // given
    -        givenBidRequest(BidRequest.builder().build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getTmax()).isEqualTo(2000L);
    -    }
    -
    -    @Test
    -    public void shouldConvertStringPriceGranularityViewToCustom() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .targeting(ExtRequestTargeting.builder().pricegranularity(new TextNode("low")).build())
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        // request was wrapped to list because extracting method works different on iterable and not iterable objects,
    -        // which force to make type casting or exception handling in lambdas
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getPricegranularity)
    -                .containsOnly(mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of(
    -                        BigDecimal.valueOf(5), BigDecimal.valueOf(0.5))))));
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureWithInvalidRequestExceptionWhenStringPriceGranularityInvalid() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .targeting(ExtRequestTargeting.builder().pricegranularity(new TextNode("invalid")).build())
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause())
    -                .isInstanceOf(InvalidRequestException.class)
    -                .hasMessage("Invalid string price granularity with value: invalid");
    -    }
    -
    -    @Test
    -    public void shouldSetDefaultPriceGranularityIfPriceGranularityAndMediaTypePriceGranularityIsMissing() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().video(Video.builder().build()).ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .targeting(ExtRequestTargeting.builder().build())
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getPricegranularity)
    -                .containsOnly(mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of(
    -                        BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))));
    -    }
    -
    -    @Test
    -    public void shouldNotSetDefaultPriceGranularityIfThereIsAMediaTypePriceGranularityForImpType() {
    -        // given
    -        final ExtMediaTypePriceGranularity mediaTypePriceGranularity = ExtMediaTypePriceGranularity.of(
    -                mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of(
    -                        BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))), null, null);
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().banner(Banner.builder().build())
    -                        .ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .targeting(ExtRequestTargeting.builder()
    -                                .mediatypepricegranularity(mediaTypePriceGranularity)
    -                                .build())
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getPricegranularity)
    -                .containsOnly((JsonNode) null);
    -    }
    -
    -    @Test
    -    public void shouldSetDefaultIncludeWinnersIfIncludeWinnersIsMissed() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .targeting(ExtRequestTargeting.builder().build())
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getIncludewinners)
    -                .containsOnly(true);
    -    }
    -
    -    @Test
    -    public void shouldSetDefaultIncludeBidderKeysIfIncludeBidderKeysIsMissed() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .targeting(ExtRequestTargeting.builder().build())
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getIncludebidderkeys)
    -                .containsOnly(true);
    -    }
    -
    -    @Test
    -    public void shouldSetDefaultIncludeBidderKeysToFalseIfIncludeBidderKeysIsMissedAndWinningonlyIsTrue() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .targeting(ExtRequestTargeting.builder().build())
    -                        .cache(ExtRequestPrebidCache.of(null, null, true))
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getIncludebidderkeys)
    -                .containsOnly(false);
    -    }
    -
    -    @Test
    -    public void shouldSetDefaultIncludeBidderKeysToFalseIfIncludeBidderKeysIsMissedAndWinningonlyIsTrueInConfig() {
    -        // given
    -        factory = new AuctionRequestFactory(
    -                Integer.MAX_VALUE,
    -                false,
    -                true,
    -                "USD",
    -                BLACKLISTED_APPS,
    -                BLACKLISTED_ACCOUNTS,
    -                storedRequestProcessor,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                uidsCookieService,
    -                bidderCatalog,
    -                requestValidator,
    -                interstitialProcessor,
    -                ortbTypesResolver,
    -                timeoutResolver,
    -                timeoutFactory,
    -                applicationSettings,
    -                idGenerator,
    -                privacyEnforcementService,
    -                jacksonMapper);
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .targeting(ExtRequestTargeting.builder().build())
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getTargeting)
    -                .extracting(ExtRequestTargeting::getIncludebidderkeys)
    -                .containsOnly(false);
    -    }
    -
    -    @Test
    -    public void shouldSetCacheWinningonlyFromConfigWhenExtRequestPrebidIsNull() {
    -        // given
    -        factory = new AuctionRequestFactory(
    -                Integer.MAX_VALUE,
    -                false,
    -                true,
    -                "USD",
    -                BLACKLISTED_APPS,
    -                BLACKLISTED_ACCOUNTS,
    -                storedRequestProcessor,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                uidsCookieService,
    -                bidderCatalog,
    -                requestValidator,
    -                interstitialProcessor,
    -                ortbTypesResolver,
    -                timeoutResolver,
    -                timeoutFactory,
    -                applicationSettings,
    -                idGenerator,
    -                privacyEnforcementService,
    -                jacksonMapper);
    -
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.empty())
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getCache)
    -                .extracting(ExtRequestPrebidCache::getWinningonly)
    -                .containsOnly(true);
    -    }
    -
    -    @Test
    -    public void shouldSetCacheWinningonlyFromConfigWhenExtRequestPrebidCacheIsNull() {
    -        // given
    -        factory = new AuctionRequestFactory(
    -                Integer.MAX_VALUE,
    -                false,
    -                true,
    -                "USD",
    -                BLACKLISTED_APPS,
    -                BLACKLISTED_ACCOUNTS,
    -                storedRequestProcessor,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                uidsCookieService,
    -                bidderCatalog,
    -                requestValidator,
    -                interstitialProcessor,
    -                ortbTypesResolver,
    -                timeoutResolver,
    -                timeoutFactory,
    -                applicationSettings,
    -                idGenerator,
    -                privacyEnforcementService,
    -                jacksonMapper);
    -
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getCache)
    -                .extracting(ExtRequestPrebidCache::getWinningonly)
    -                .containsOnly(true);
    -    }
    -
    -    @Test
    -    public void shouldSetCacheWinningonlyFromConfigWhenCacheWinningonlyIsNull() {
    -        // given
    -        factory = new AuctionRequestFactory(
    -                Integer.MAX_VALUE,
    -                false,
    -                true,
    -                "USD",
    -                BLACKLISTED_APPS,
    -                BLACKLISTED_ACCOUNTS,
    -                storedRequestProcessor,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                uidsCookieService,
    -                bidderCatalog,
    -                requestValidator,
    -                interstitialProcessor,
    -                ortbTypesResolver,
    -                timeoutResolver,
    -                timeoutFactory,
    -                applicationSettings,
    -                idGenerator,
    -                privacyEnforcementService,
    -                jacksonMapper);
    -
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .cache(ExtRequestPrebidCache.of(null, null, null))
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getCache)
    -                .extracting(ExtRequestPrebidCache::getWinningonly)
    -                .containsOnly(true);
    -    }
    -
    -    @Test
    -    public void shouldNotChangeAnyOtherExtRequestPrebidCacheFields() {
    -        // given
    -        final ExtRequestPrebidCacheBids cacheBids = ExtRequestPrebidCacheBids.of(100, true);
    -        final ExtRequestPrebidCacheVastxml cacheVastxml = ExtRequestPrebidCacheVastxml.of(100, true);
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .cache(ExtRequestPrebidCache.of(cacheBids, cacheVastxml, null))
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getCache)
    -                .extracting(ExtRequestPrebidCache::getBids, ExtRequestPrebidCache::getVastxml)
    -                .containsOnly(tuple(cacheBids, cacheVastxml));
    -    }
    -
    -    @Test
    -    public void shouldSetCacheWinningonlyFromRequestWhenCacheWinningonlyIsPresent() {
    -        // given
    -        factory = new AuctionRequestFactory(
    -                Integer.MAX_VALUE,
    -                false,
    -                true,
    -                "USD",
    -                BLACKLISTED_APPS,
    -                BLACKLISTED_ACCOUNTS,
    -                storedRequestProcessor,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                uidsCookieService,
    -                bidderCatalog,
    -                requestValidator,
    -                interstitialProcessor,
    -                ortbTypesResolver,
    -                timeoutResolver,
    -                timeoutFactory,
    -                applicationSettings,
    -                idGenerator,
    -                privacyEnforcementService,
    -                jacksonMapper);
    -
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .cache(ExtRequestPrebidCache.of(null, null, false))
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getCache)
    -                .extracting(ExtRequestPrebidCache::getWinningonly)
    -                .containsOnly(false);
    -    }
    -
    -    @Test
    -    public void shouldNotSetCacheWinningonlyFromConfigWhenCacheWinningonlyIsNullAndConfigValueIsFalse() {
    -        // given
    -        factory = new AuctionRequestFactory(
    -                Integer.MAX_VALUE,
    -                false,
    -                false,
    -                "USD",
    -                BLACKLISTED_APPS,
    -                BLACKLISTED_ACCOUNTS,
    -                storedRequestProcessor,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                uidsCookieService,
    -                bidderCatalog,
    -                requestValidator,
    -                interstitialProcessor,
    -                ortbTypesResolver,
    -                timeoutResolver,
    -                timeoutFactory,
    -                applicationSettings,
    -                idGenerator,
    -                privacyEnforcementService,
    -                jacksonMapper);
    -
    -        final ExtRequest extBidRequest = ExtRequest.of(ExtRequestPrebid.builder()
    -                .cache(ExtRequestPrebidCache.of(null, null, null))
    -                .build());
    -
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(extBidRequest)
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(request.getExt()).isSameAs(extBidRequest);
    -    }
    -
    -    @Test
    -    public void shouldAddMissingAliases() {
    -        // given
    -        final Imp imp1 = Imp.builder()
    -                .ext(mapper.createObjectNode()
    -                        .set("requestScopedBidderAlias", mapper.createObjectNode()))
    -                .build();
    -        final Imp imp2 = Imp.builder()
    -                .ext(mapper.createObjectNode()
    -                        .set("configScopedBidderAlias", mapper.createObjectNode()))
    -                .build();
    -
    -        givenBidRequest(BidRequest.builder()
    -                .imp(asList(imp1, imp2))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .aliases(singletonMap("requestScopedBidderAlias", "bidder1"))
    -                        .targeting(ExtRequestTargeting.builder().build())
    -                        .build()))
    -                .build());
    -
    -        given(bidderCatalog.isAlias("configScopedBidderAlias")).willReturn(true);
    -        given(bidderCatalog.nameByAlias("configScopedBidderAlias")).willReturn("bidder2");
    -
    -        // when
    -        final Future auctionContextFuture = factory.fromRequest(routingContext, 0L);
    -        final BidRequest request = auctionContextFuture.result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .flatExtracting(extRequestPrebid -> extRequestPrebid.getAliases().entrySet())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(
    -                        tuple("requestScopedBidderAlias", "bidder1"),
    -                        tuple("configScopedBidderAlias", "bidder2"));
    -    }
    -
    -    @Test
    -    public void shouldSetRequestPrebidChannelWhenMissingInRequestAndSite() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder().build())
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getChannel)
    -                .containsOnly(ExtRequestPrebidChannel.of("web"));
    -    }
    -
    -    @Test
    -    public void shouldSetRequestPrebidChannelWhenMissingInRequestAndApp() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .app(App.builder().build())
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getChannel)
    -                .containsOnly(ExtRequestPrebidChannel.of("app"));
    -    }
    -
    -    @Test
    -    public void shouldNotSetRequestPrebidChannelWhenMissingInRequestAndNotSiteOrApp() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getChannel)
    -                .containsOnly((ExtRequestPrebidChannel) null);
    -    }
    -
    -    @Test
    -    public void shouldNotSetRequestPrebidChannelWhenPresentInRequestAndApp() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .app(App.builder().build())
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .channel(ExtRequestPrebidChannel.of("custom"))
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getChannel)
    -                .containsOnly(ExtRequestPrebidChannel.of("custom"));
    -    }
    -
    -    @Test
    -    public void shouldTolerateMissingImpExtWhenProcessingAliases() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(null).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .aliases(singletonMap("alias", "bidder"))
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.succeeded()).isTrue();
    -    }
    -
    -    @Test
    -    public void shouldPassExtPrebidDebugFlagIfPresent() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .debug(1)
    -                        .build()))
    -                .build());
    -
    -        // when
    -        final BidRequest request = factory.fromRequest(routingContext, 0L).result().getBidRequest();
    -
    -        // then
    -        assertThat(singletonList(request))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getDebug)
    -                .containsOnly(1);
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfRequestValidationFailed() {
    -        // given
    -        given(routingContext.getBody()).willReturn(Buffer.buffer("{}"));
    -
    -        given(storedRequestProcessor.processStoredRequests(any())).willReturn(Future.succeededFuture(
    -                BidRequest.builder().build()));
    -
    -        given(requestValidator.validate(any())).willReturn(new ValidationResult(asList("error1", "error2")));
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    -        assertThat(((InvalidRequestException) future.cause()).getMessages()).containsOnly("error1", "error2");
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithRoutingContext() {
    -        // given
    -        givenValidBidRequest();
    -
    -        // when
    -        final RoutingContext context = factory.fromRequest(routingContext, 0L).result().getRoutingContext();
    -
    -        // then
    -        assertThat(context).isSameAs(routingContext);
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithUidsCookie() {
    -        // given
    -        givenValidBidRequest();
    -
    -        final UidsCookie givenUidsCookie = new UidsCookie(Uids.builder()
    -                .uids(singletonMap("bidder", UidWithExpiry.live("uid")))
    -                .build(), jacksonMapper);
    -        given(uidsCookieService.parseFromRequest(any())).willReturn(givenUidsCookie);
    -
    -        // when
    -        final UidsCookie uidsCookie = factory.fromRequest(routingContext, 0L).result().getUidsCookie();
    -
    -        // then
    -        assertThat(uidsCookie).isSameAs(givenUidsCookie);
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithTimeout() {
    -        // given
    -        givenValidBidRequest();
    -
    -        given(timeoutFactory.create(anyLong(), anyLong())).willReturn(mock(Timeout.class));
    -
    -        final long startTime = Clock.fixed(Instant.now(), ZoneId.systemDefault()).millis();
    -
    -        // when
    -        final Timeout timeout = factory.fromRequest(routingContext, startTime).result().getTimeout();
    -
    -        // then
    -        verify(timeoutFactory).create(eq(startTime), anyLong());
    -        assertThat(timeout).isNotNull();
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureWhenAccountIdIsBlacklisted() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder()
    -                        .publisher(Publisher.builder().id("bad_acc").build()).build())
    -                .build());
    -
    -        // when
    -        final Future result = factory.fromRequest(routingContext, 0);
    -
    -        // then
    -        assertThat(result.failed()).isTrue();
    -        assertThat(result.cause())
    -                .isInstanceOf(BlacklistedAccountException.class)
    -                .hasMessage("Prebid-server has blacklisted Account ID: bad_acc, please reach out to the prebid "
    -                        + "server host.");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureWhenAppIdIsBlacklisted() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .app(App.builder().id("bad_app").build())
    -                .build());
    -
    -        // when
    -        final Future result = factory.fromRequest(routingContext, 0);
    -
    -        // then
    -        assertThat(result.failed()).isTrue();
    -        assertThat(result.cause())
    -                .isInstanceOf(BlacklistedAppException.class)
    -                .hasMessage("Prebid-server does not process requests from App ID: bad_app");
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherExt() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder()
    -                        .publisher(Publisher.builder().id("accountId")
    -                                .ext(ExtPublisher.of(ExtPublisherPrebid.of("parentAccount")))
    -                                .build())
    -                        .build())
    -                .build());
    -
    -        final Account givenAccount = Account.builder().id("parentAccount").build();
    -        given(applicationSettings.getAccountById(any(), any()))
    -                .willReturn(Future.succeededFuture(givenAccount));
    -
    -        // when
    -        final Account account = factory.fromRequest(routingContext, 0L).result().getAccount();
    -
    -        // then
    -        verify(applicationSettings).getAccountById(eq("parentAccount"), any());
    -
    -        assertThat(account).isSameAs(givenAccount);
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherIdWhenExtIsNull() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder()
    -                        .publisher(Publisher.builder().id("accountId").ext(null).build())
    -                        .build())
    -                .build());
    -
    -        final Account givenAccount = Account.builder().id("accountId").build();
    -        given(applicationSettings.getAccountById(any(), any()))
    -                .willReturn(Future.succeededFuture(givenAccount));
    -
    -        // when
    -        final Account account = factory.fromRequest(routingContext, 0L).result().getAccount();
    -
    -        // then
    -        verify(applicationSettings).getAccountById(eq("accountId"), any());
    -
    -        assertThat(account).isSameAs(givenAccount);
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherIdWhenExtPublisherPrebidIsNull() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder()
    -                        .publisher(Publisher.builder().id("accountId")
    -                                .ext(ExtPublisher.empty())
    -                                .build())
    -                        .build())
    -                .build());
    -
    -        final Account givenAccount = Account.builder().id("accountId").build();
    -        given(applicationSettings.getAccountById(any(), any()))
    -                .willReturn(Future.succeededFuture(givenAccount));
    -
    -        // when
    -        final Account account = factory.fromRequest(routingContext, 0L).result().getAccount();
    -
    -        // then
    -        verify(applicationSettings).getAccountById(eq("accountId"), any());
    -
    -        assertThat(account).isSameAs(givenAccount);
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithAccountIdTakenFromPublisherIdWhenExtParentIsEmpty() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder()
    -                        .publisher(Publisher.builder().id("accountId")
    -                                .ext(ExtPublisher.of(ExtPublisherPrebid.of("")))
    -                                .build())
    -                        .build())
    -                .build());
    -
    -        final Account givenAccount = Account.builder().id("accountId").build();
    -        given(applicationSettings.getAccountById(any(), any()))
    -                .willReturn(Future.succeededFuture(givenAccount));
    -
    -        // when
    -        final Account account = factory.fromRequest(routingContext, 0L).result().getAccount();
    -
    -        // then
    -        verify(applicationSettings).getAccountById(eq("accountId"), any());
    -
    -        assertThat(account).isSameAs(givenAccount);
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithEmptyAccountIfNotFound() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder()
    -                        .publisher(Publisher.builder().id("accountId")
    -                                .ext(ExtPublisher.of(ExtPublisherPrebid.of("parentAccount")))
    -                                .build())
    -                        .build())
    -                .build());
    -
    -        given(applicationSettings.getAccountById(any(), any()))
    -                .willReturn(Future.failedFuture(new PreBidException("not found")));
    -
    -        // when
    -        final Account account = factory.fromRequest(routingContext, 0L).result().getAccount();
    -
    -        // then
    -        verify(applicationSettings).getAccountById(eq("parentAccount"), any());
    -
    -        assertThat(account).isEqualTo(Account.builder().id("parentAccount").build());
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithEmptyAccountIfExceptionOccurred() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .site(Site.builder()
    -                        .publisher(Publisher.builder().id("accountId").build())
    -                        .build())
    -                .build());
    -
    -        given(applicationSettings.getAccountById(any(), any()))
    -                .willReturn(Future.failedFuture(new RuntimeException("error")));
    -
    -        // when
    -        final Account account = factory.fromRequest(routingContext, 0L).result().getAccount();
    -
    -        // then
    -        verify(applicationSettings).getAccountById(eq("accountId"), any());
    -
    -        assertThat(account).isEqualTo(Account.builder().id("accountId").build());
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithEmptyAccountIfItIsMissingInRequest() {
    -        // given
    -        givenValidBidRequest();
    -
    -        // when
    -        final Account account = factory.fromRequest(routingContext, 0L).result().getAccount();
    -
    -        // then
    -        assertThat(account).isEqualTo(Account.builder().id("").build());
    -        verifyZeroInteractions(applicationSettings);
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithIntegrationFromAccount() {
    -        // given
    -        givenBidRequest(BidRequest.builder()
    -                .imp(emptyList())
    -                .site(Site.builder()
    -                        .publisher(Publisher.builder().id("123").build())
    -                        .build())
    -                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    -                .build());
    -
    -        final Account givenAccount = Account.builder().id("123").defaultIntegration("integration").build();
    -        given(applicationSettings.getAccountById(any(), any()))
    -                .willReturn(Future.succeededFuture(givenAccount));
    -
    -        // when
    -        final AuctionContext auctionContext = factory.fromRequest(routingContext, 0L).result();
    -
    -        // then
    -        assertThat(singletonList(auctionContext.getBidRequest()))
    -                .extracting(BidRequest::getExt)
    -                .extracting(ExtRequest::getPrebid)
    -                .extracting(ExtRequestPrebid::getIntegration)
    -                .containsOnly("integration");
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithWebRequestTypeMetric() {
    -        // given
    -        givenValidBidRequest();
    -
    -        // when
    -        final Future auctionContextFuture = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        FutureAssertion.assertThat(auctionContextFuture).isSucceeded();
    -        assertThat(auctionContextFuture.result().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web);
    -    }
    -
    -    @Test
    -    public void shouldReturnAuctionContextWithAppRequestTypeMetric() {
    -        // given
    -        givenBidRequest(BidRequest.builder().app(App.builder().build()).build());
    -
    -        // when
    -        final Future auctionContextFuture = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        FutureAssertion.assertThat(auctionContextFuture).isSucceeded();
    -        assertThat(auctionContextFuture.result().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2app);
    -    }
    -
    -    @Test
    -    public void shouldEnrichRequestWithIpAddressAndCountryAndSaveAuctionContext() {
    -        // given
    -        givenBidRequest(BidRequest.builder().build());
    -
    -        final PrivacyContext privacyContext = PrivacyContext.of(
    -                Privacy.of(EMPTY, EMPTY, Ccpa.EMPTY, 0),
    -                TcfContext.builder()
    -                        .geoInfo(GeoInfo.builder().vendor("v").country("ua").build())
    -                        .build(),
    -                "ip");
    -        given(privacyEnforcementService.contextFromBidRequest(any(), any(), any(), any()))
    -                .willReturn(Future.succeededFuture(privacyContext));
    -
    -        // when
    -        final Future auctionContextFuture = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        FutureAssertion.assertThat(auctionContextFuture).isSucceeded();
    -
    -        final AuctionContext auctionContext = auctionContextFuture.result();
    -        assertThat(auctionContext.getBidRequest().getDevice()).isEqualTo(
    -                Device.builder()
    -                        .ip("ip")
    -                        .geo(Geo.builder().country("ua").build())
    -                        .build());
    -        assertThat(auctionContext.getPrivacyContext()).isSameAs(privacyContext);
    -    }
    -
    -    private void givenImplicitParams(String referer, String domain, String ip, IpAddress.IP ipVersion, String ua) {
    -        given(paramsExtractor.refererFrom(any())).willReturn(referer);
    -        given(paramsExtractor.domainFrom(anyString())).willReturn(domain);
    -        given(paramsExtractor.ipFrom(any())).willReturn(singletonList(ip));
    -        given(ipAddressHelper.toIpAddress(eq(ip))).willReturn(IpAddress.of(ip, ipVersion));
    -        given(paramsExtractor.uaFrom(any())).willReturn(ua);
    -    }
    -
    -    private void givenBidRequest(BidRequest bidRequest) {
    -        try {
    -            given(routingContext.getBody()).willReturn(Buffer.buffer(mapper.writeValueAsString(bidRequest)));
    -        } catch (JsonProcessingException e) {
    -            throw new RuntimeException(e);
    -        }
    -
    -        given(storedRequestProcessor.processStoredRequests(any())).willReturn(Future.succeededFuture(bidRequest));
    -
    -        given(requestValidator.validate(any())).willReturn(ValidationResult.success());
    -    }
    -
    -    private void givenValidBidRequest() {
    -        givenBidRequest(BidRequest.builder().build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java
    index 413c9fc9ff2..af66486ca43 100644
    --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java
    +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java
    @@ -1,15 +1,17 @@
     package org.prebid.server.auction;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.fasterxml.jackson.core.type.TypeReference;
    +import com.fasterxml.jackson.databind.JsonNode;
     import com.fasterxml.jackson.databind.node.ObjectNode;
     import com.iab.openrtb.request.App;
     import com.iab.openrtb.request.Asset;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.DataObject;
    +import com.iab.openrtb.request.Deal;
     import com.iab.openrtb.request.ImageObject;
     import com.iab.openrtb.request.Imp;
     import com.iab.openrtb.request.Native;
    +import com.iab.openrtb.request.Pmp;
     import com.iab.openrtb.request.Request;
     import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
    @@ -17,31 +19,47 @@
     import com.iab.openrtb.response.Response;
     import com.iab.openrtb.response.SeatBid;
     import io.vertx.core.Future;
    +import lombok.Value;
    +import lombok.experimental.Accessors;
    +import org.apache.commons.collections4.MapUtils;
     import org.junit.Before;
     import org.junit.Rule;
     import org.junit.Test;
     import org.mockito.ArgumentCaptor;
     import org.mockito.Mock;
    +import org.mockito.Spy;
     import org.mockito.junit.MockitoJUnit;
     import org.mockito.junit.MockitoRule;
     import org.prebid.server.VertxTest;
     import org.prebid.server.auction.model.AuctionContext;
    +import org.prebid.server.auction.model.BidInfo;
     import org.prebid.server.auction.model.BidRequestCacheInfo;
     import org.prebid.server.auction.model.BidderResponse;
    +import org.prebid.server.auction.model.DebugContext;
    +import org.prebid.server.auction.model.MultiBidConfig;
    +import org.prebid.server.auction.model.TargetingInfo;
     import org.prebid.server.bidder.BidderCatalog;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderError;
     import org.prebid.server.bidder.model.BidderSeatBid;
     import org.prebid.server.cache.CacheService;
     import org.prebid.server.cache.model.CacheContext;
    -import org.prebid.server.cache.model.CacheIdInfo;
    +import org.prebid.server.cache.model.CacheInfo;
     import org.prebid.server.cache.model.CacheServiceResult;
     import org.prebid.server.cache.model.DebugHttpCall;
    +import org.prebid.server.deals.model.DeepDebugLog;
    +import org.prebid.server.deals.model.TxnLog;
     import org.prebid.server.events.EventsContext;
     import org.prebid.server.events.EventsService;
     import org.prebid.server.execution.Timeout;
     import org.prebid.server.execution.TimeoutFactory;
    -import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.hooks.execution.HookStageExecutor;
    +import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
    +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload;
    +import org.prebid.server.identity.IdGenerator;
    +import org.prebid.server.identity.IdGeneratorType;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDeal;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine;
     import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange;
     import org.prebid.server.proto.openrtb.ext.request.ExtImp;
     import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
    @@ -54,6 +72,7 @@
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
     import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest;
    +import org.prebid.server.proto.openrtb.ext.response.BidType;
     import org.prebid.server.proto.openrtb.ext.response.CacheAsset;
     import org.prebid.server.proto.openrtb.ext.response.Events;
     import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
    @@ -61,30 +80,34 @@
     import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse;
     import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid;
     import org.prebid.server.proto.openrtb.ext.response.ExtBidderError;
    +import org.prebid.server.proto.openrtb.ext.response.ExtDebugPgmetrics;
     import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall;
     import org.prebid.server.proto.openrtb.ext.response.ExtResponseCache;
     import org.prebid.server.settings.model.Account;
     import org.prebid.server.settings.model.AccountAnalyticsConfig;
    +import org.prebid.server.settings.model.AccountAuctionConfig;
    +import org.prebid.server.settings.model.AccountEventsConfig;
     import org.prebid.server.settings.model.VideoStoredDataResult;
    +import org.prebid.server.vast.VastModifier;
     
    -import java.io.IOException;
     import java.math.BigDecimal;
     import java.time.Clock;
     import java.time.Instant;
     import java.time.ZoneId;
     import java.time.ZoneOffset;
    -import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.Collections;
     import java.util.HashMap;
     import java.util.List;
     import java.util.Map;
    -import java.util.UUID;
     import java.util.function.UnaryOperator;
    +import java.util.stream.Collectors;
    +import java.util.stream.IntStream;
     
     import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
     import static java.util.Collections.emptyMap;
    +import static java.util.Collections.singleton;
     import static java.util.Collections.singletonList;
     import static java.util.Collections.singletonMap;
     import static java.util.function.UnaryOperator.identity;
    @@ -93,22 +116,30 @@
     import static org.assertj.core.api.Assertions.entry;
     import static org.assertj.core.api.Assertions.tuple;
     import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyBoolean;
     import static org.mockito.ArgumentMatchers.anyList;
    -import static org.mockito.ArgumentMatchers.anyLong;
     import static org.mockito.ArgumentMatchers.anyString;
     import static org.mockito.ArgumentMatchers.argThat;
     import static org.mockito.ArgumentMatchers.eq;
     import static org.mockito.ArgumentMatchers.same;
     import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.doAnswer;
     import static org.mockito.Mockito.never;
    +import static org.mockito.Mockito.times;
     import static org.mockito.Mockito.verify;
     import static org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule.Source.xStatic;
     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;
     
     public class BidResponseCreatorTest extends VertxTest {
     
         private static final BidRequestCacheInfo CACHE_INFO = BidRequestCacheInfo.builder().build();
    +    private static final Map MULTI_BIDS = emptyMap();
    +
    +    private static final String IMP_ID = "impId1";
    +    private static final String BID_ADM = "adm";
    +    private static final String BID_NURL = "nurl";
     
         @Rule
         public final MockitoRule mockitoRule = MockitoJUnit.rule();
    @@ -118,9 +149,19 @@ public class BidResponseCreatorTest extends VertxTest {
         @Mock
         private BidderCatalog bidderCatalog;
         @Mock
    +    private VastModifier vastModifier;
    +    @Mock
         private EventsService eventsService;
         @Mock
         private StoredRequestProcessor storedRequestProcessor;
    +    @Mock
    +    private IdGenerator idGenerator;
    +    @Mock
    +    private HookStageExecutor hookStageExecutor;
    +
    +    @Spy
    +    private WinningBidComparatorFactory winningBidComparatorFactory;
    +
         private Clock clock;
     
         private Timeout timeout;
    @@ -133,17 +174,28 @@ public void setUp() {
             given(cacheService.getEndpointPath()).willReturn("testPath");
             given(cacheService.getCachedAssetURLTemplate()).willReturn("uuid=");
     
    -        given(storedRequestProcessor.videoStoredDataResult(any(), any(), any()))
    +        given(storedRequestProcessor.videoStoredDataResult(any(), anyList(), anyList(), any()))
                     .willReturn(Future.succeededFuture(VideoStoredDataResult.empty()));
    +        given(idGenerator.getType()).willReturn(IdGeneratorType.none);
    +
    +        given(hookStageExecutor.executeProcessedBidderResponseStage(any(), any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                        false,
    +                        BidderResponsePayloadImpl.of(((BidderResponse) invocation.getArgument(0))
    +                                .getSeatBid()
    +                                .getBids()))));
     
             clock = Clock.fixed(Instant.ofEpochMilli(1000L), ZoneOffset.UTC);
     
             bidResponseCreator = new BidResponseCreator(
                     cacheService,
                     bidderCatalog,
    +                vastModifier,
                     eventsService,
                     storedRequestProcessor,
    -                false,
    +                winningBidComparatorFactory,
    +                idGenerator,
    +                hookStageExecutor,
                     0,
                     clock,
                     jacksonMapper);
    @@ -151,14 +203,117 @@ public void setUp() {
             timeout = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())).create(500);
         }
     
    +    @Test
    +    public void shouldPassBidWithGeneratedIdAndPreserveExtFieldsWhenIdGeneratorTypeUuid() {
    +        // given
    +        final Imp imp = givenImp();
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(imp));
    +        final String generatedId = "generatedId";
    +        given(idGenerator.getType()).willReturn(IdGeneratorType.uuid);
    +        given(idGenerator.generateId()).willReturn(generatedId);
    +
    +        final ObjectNode receivedBidExt = mapper.createObjectNode()
    +                .put("origbidcur", "test");
    +        final Bid bid = Bid.builder()
    +                .id("bidId1")
    +                .impid(IMP_ID)
    +                .price(BigDecimal.valueOf(5.67))
    +                .ext(receivedBidExt)
    +                .build();
    +        final String bidder = "bidder1";
    +        final List bidderResponses = singletonList(
    +                BidderResponse.of(bidder, givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
    +
    +        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
    +
    +        givenCacheServiceResult(singletonList(CacheInfo.empty()));
    +
    +        // when
    +        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS);
    +
    +        // then
    +        final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder()
    +                .bidid(generatedId)
    +                .type(banner)
    +                .build();
    +        final Bid expectedBid = bid.toBuilder()
    +                .ext(mapper.createObjectNode()
    +                        .put("origbidcur", "test")
    +                        .set("prebid", mapper.valueToTree(extBidPrebid)))
    +                .build();
    +        final BidInfo bidInfo = toBidInfo(expectedBid, imp, bidder, banner, true);
    +        verify(cacheService).cacheBidsOpenrtb(eq(singletonList(bidInfo)), any(), any(), any());
    +    }
    +
    +    @Test
    +    public void shouldSkipBidderWhenRejectedByProcessedBidderResponseHooks() {
    +        // given
    +        doAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(true, null)))
    +                .when(hookStageExecutor).executeProcessedBidderResponseStage(any(), any());
    +
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
    +
    +        final Bid bid = Bid.builder()
    +                .id("bidId1")
    +                .impid(IMP_ID)
    +                .price(BigDecimal.valueOf(5.67))
    +                .build();
    +        final List bidderResponses = singletonList(
    +                BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
    +
    +        // when
    +        final BidResponse bidResponse =
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
    +
    +        // then
    +        assertThat(bidResponse.getSeatbid())
    +                .flatExtracting(SeatBid::getBid)
    +                .isEmpty();
    +    }
    +
    +    @Test
    +    public void shouldPassRequestModifiedByBidderRequestHooks() {
    +        doAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                false,
    +                BidderResponsePayloadImpl.of(singletonList(BidderBid.of(
    +                        Bid.builder()
    +                                .id("bidIdModifiedByHook")
    +                                .impid(IMP_ID)
    +                                .price(BigDecimal.valueOf(1.23))
    +                                .build(),
    +                        video,
    +                        "EUR"))))))
    +                .when(hookStageExecutor).executeProcessedBidderResponseStage(any(), any());
    +
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
    +
    +        final Bid bid = Bid.builder()
    +                .id("bidId1")
    +                .impid(IMP_ID)
    +                .price(BigDecimal.valueOf(5.67))
    +                .build();
    +        final List bidderResponses = singletonList(
    +                BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
    +
    +        // when
    +        final BidResponse bidResponse =
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
    +
    +        // then
    +        assertThat(bidResponse.getSeatbid())
    +                .flatExtracting(SeatBid::getBid)
    +                .extracting(Bid::getId, Bid::getImpid, Bid::getPrice)
    +                .containsOnly(tuple("bidIdModifiedByHook", IMP_ID, BigDecimal.valueOf(1.23)));
    +    }
    +
         @Test
         public void shouldPassOriginalTimeoutToCacheServiceIfCachingIsRequested() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
     
             final Bid bid = Bid.builder()
                     .id("bidId1")
    -                .impid("impId1")
    +                .impid(IMP_ID)
                     .price(BigDecimal.valueOf(5.67))
                     .build();
             final List bidderResponses = singletonList(
    @@ -166,25 +321,31 @@ public void shouldPassOriginalTimeoutToCacheServiceIfCachingIsRequested() {
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
     
    -        givenCacheServiceResult(singletonMap(bid, CacheIdInfo.of(null, null)));
    +        givenCacheServiceResult(singletonList(CacheInfo.empty()));
     
             // when
    -        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false);
    +        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS);
     
             // then
             verify(cacheService).cacheBidsOpenrtb(anyList(), any(), any(), any());
         }
     
    +    @SuppressWarnings("unchecked")
         @Test
         public void shouldRequestCacheServiceWithExpectedArguments() {
             // given
    +        final Imp imp1 = givenImp("impId1");
    +        final Imp imp2 = givenImp("impId2");
             final AuctionContext auctionContext = givenAuctionContext(
                     givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .events(mapper.createObjectNode())
    -                        .build()))),
    +                                .events(mapper.createObjectNode())
    +                                .build())),
    +                        imp1, imp2),
                     builder -> builder.account(Account.builder()
                             .id("accountId")
    -                        .eventsEnabled(true)
    +                        .auction(AccountAuctionConfig.builder()
    +                                .events(AccountEventsConfig.of(true))
    +                                .build())
                             .build()));
     
             final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build();
    @@ -192,10 +353,16 @@ public void shouldRequestCacheServiceWithExpectedArguments() {
             final Bid bid3 = Bid.builder().id("bidId3").impid("impId1").price(BigDecimal.valueOf(3.74)).build();
             final Bid bid4 = Bid.builder().id("bidId4").impid("impId2").price(BigDecimal.valueOf(6.74)).build();
             final List bidderResponses = asList(
    -                BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid1, banner, "USD"),
    -                        BidderBid.of(bid2, banner, "USD")), 100),
    -                BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(bid3, banner, "USD"),
    -                        BidderBid.of(bid4, banner, "USD")), 100));
    +                BidderResponse.of("bidder1",
    +                        givenSeatBid(
    +                                BidderBid.of(bid1, banner, "USD"),
    +                                BidderBid.of(bid2, banner, "USD")),
    +                        100),
    +                BidderResponse.of("bidder2",
    +                        givenSeatBid(
    +                                BidderBid.of(bid3, banner, "USD"),
    +                                BidderBid.of(bid4, banner, "USD")),
    +                        100));
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
                     .doCaching(true)
    @@ -206,81 +373,100 @@ public void shouldRequestCacheServiceWithExpectedArguments() {
                     .build();
     
             // just a stub to get through method call chain
    -        givenCacheServiceResult(singletonMap(bid1, CacheIdInfo.of(null, null)));
    +        givenCacheServiceResult(singletonList(CacheInfo.empty()));
     
             // when
    -        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false);
    +        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS);
     
             // then
    -        Map> biddersToCacheBidIds = new HashMap<>();
    -        biddersToCacheBidIds.put("bidder1", Arrays.asList("bidId1", "bidId2"));
    -        biddersToCacheBidIds.put("bidder2", Arrays.asList("bidId3", "bidId4"));
    +        final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class);
    +        final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
             verify(cacheService).cacheBidsOpenrtb(
    -                argThat(t -> t.containsAll(asList(bid1, bid4, bid3, bid2))),
    +                bidsArgumentCaptor.capture(),
                     same(auctionContext),
    -                eq(CacheContext.builder()
    -                        .shouldCacheBids(true)
    -                        .shouldCacheVideoBids(true)
    -                        .cacheBidsTtl(99)
    -                        .cacheVideoBidsTtl(101)
    -                        .bidderToVideoBidIdsToModify(emptyMap())
    -                        .bidderToBidIds(biddersToCacheBidIds)
    -                        .build()),
    +                contextArgumentCaptor.capture(),
                     eq(EventsContext.builder()
    +                        .auctionId("123")
                             .enabledForAccount(true)
                             .enabledForRequest(true)
                             .auctionTimestamp(1000L)
                             .build()));
    +
    +        assertThat(bidsArgumentCaptor.getValue()).extracting(bidInfo -> bidInfo.getBid().getId())
    +                .containsOnly("bidId1", "bidId2", "bidId3", "bidId4");
    +        assertThat(contextArgumentCaptor.getValue())
    +                .satisfies(context -> {
    +                    assertThat(context.isShouldCacheBids()).isTrue();
    +                    assertThat(context.isShouldCacheVideoBids()).isTrue();
    +                    assertThat(context.getCacheBidsTtl()).isEqualTo(99);
    +                    assertThat(context.getCacheVideoBidsTtl()).isEqualTo(101);
    +                });
         }
     
    +    @SuppressWarnings("unchecked")
         @Test
         public void shouldRequestCacheServiceWithWinningBidsOnlyWhenWinningonlyIsTrue() {
             // given
    +        final Imp imp1 = givenImp("impId1");
    +        final Imp imp2 = givenImp("impId2");
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(givenTargeting())));
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                imp1, imp2, givenImp("impId3"), givenImp("impId4")));
     
             final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build();
             final Bid bid2 = Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.valueOf(7.19)).build();
             final Bid bid3 = Bid.builder().id("bidId3").impid("impId1").price(BigDecimal.valueOf(3.74)).build();
             final Bid bid4 = Bid.builder().id("bidId4").impid("impId2").price(BigDecimal.valueOf(6.74)).build();
             final List bidderResponses = asList(
    -                BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid1, banner, "USD"),
    -                        BidderBid.of(bid2, banner, "USD")), 100),
    -                BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(bid3, banner, "USD"),
    -                        BidderBid.of(bid4, banner, "USD")), 100));
    +                BidderResponse.of("bidder1",
    +                        givenSeatBid(
    +                                BidderBid.of(bid1, banner, "USD"),
    +                                BidderBid.of(bid2, banner, "USD")),
    +                        100),
    +                BidderResponse.of("bidder2",
    +                        givenSeatBid(
    +                                BidderBid.of(bid3, banner, "USD"),
    +                                BidderBid.of(bid4, banner, "USD")),
    +                        100));
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
    -                .doCaching(true).shouldCacheWinningBidsOnly(true).build();
    +                .doCaching(true)
    +                .shouldCacheWinningBidsOnly(true)
    +                .build();
     
             // just a stub to get through method call chain
    -        givenCacheServiceResult(singletonMap(bid1, CacheIdInfo.of(null, null)));
    +        givenCacheServiceResult(singletonList(CacheInfo.empty()));
     
             // when
    -        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false);
    +        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS);
     
             // then
    -        Map> biddersToCacheBidIds = new HashMap<>();
    -        biddersToCacheBidIds.put("bidder1", Arrays.asList("bidId1", "bidId2"));
    -        biddersToCacheBidIds.put("bidder2", Arrays.asList("bidId3", "bidId4"));
    +        final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
             verify(cacheService).cacheBidsOpenrtb(
    -                argThat(t -> t.containsAll(asList(bid1, bid2)) && t.size() == 2),
    +                bidsArgumentCaptor.capture(),
                     same(auctionContext),
    -                eq(CacheContext.builder()
    -                        .bidderToVideoBidIdsToModify(emptyMap())
    -                        .bidderToBidIds(biddersToCacheBidIds)
    -                        .build()),
    -                eq(EventsContext.builder().auctionTimestamp(1000L).build()));
    +                any(),
    +                eq(EventsContext.builder().auctionId("123").auctionTimestamp(1000L).build()));
    +
    +        assertThat(bidsArgumentCaptor.getValue()).extracting(bidInfo -> bidInfo.getBid().getId())
    +                .containsOnly("bidId1", "bidId2");
         }
     
    +    @SuppressWarnings("unchecked")
         @Test
    -    public void shouldRequestCacheServiceWithVideoBidsToModifyWhenEventsEnabledAndForBidderThatAllowsModifyVastXml() {
    +    public void shouldRequestCacheServiceWithVideoBidsToModify() {
             // given
    -        final Account account = Account.builder().id("accountId").eventsEnabled(true).build();
    -
    -        final Imp imp1 = Imp.builder().id("impId1").video(Video.builder().build()).build();
    -        final Imp imp2 = Imp.builder().id("impId2").video(Video.builder().build()).build();
    +        final String accountId = "accountId";
    +        final Account account = Account.builder()
    +                .id(accountId)
    +                .auction(AccountAuctionConfig.builder()
    +                        .events(AccountEventsConfig.of(true))
    +                        .build())
    +                .build();
     
    +        final Imp imp1 = givenImp("impId1");
    +        final Imp imp2 = givenImp("impId2");
             final AuctionContext auctionContext = givenAuctionContext(
                     givenBidRequest(
                             identity(),
    @@ -288,10 +474,23 @@ public void shouldRequestCacheServiceWithVideoBidsToModifyWhenEventsEnabledAndFo
                             imp1, imp2),
                     contextBuilder -> contextBuilder.account(account));
     
    -        final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build();
    -        final Bid bid2 = Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.valueOf(7.19)).build();
    +        final String bidId1 = "bidId1";
    +        final Bid bid1 = Bid.builder()
    +                .id(bidId1)
    +                .impid("impId1")
    +                .price(BigDecimal.valueOf(5.67))
    +                .nurl(BID_NURL)
    +                .build();
    +        final Bid bid2 = Bid.builder()
    +                .id("bidId2")
    +                .impid("impId2")
    +                .price(BigDecimal.valueOf(7.19))
    +                .adm("adm2")
    +                .build();
    +
    +        final String bidder1 = "bidder1";
             final List bidderResponses = asList(
    -                BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid1, banner, "USD")), 100),
    +                BidderResponse.of(bidder1, givenSeatBid(BidderBid.of(bid1, video, "USD")), 100),
                     BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(bid2, banner, "USD")), 100));
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
    @@ -299,94 +498,148 @@ public void shouldRequestCacheServiceWithVideoBidsToModifyWhenEventsEnabledAndFo
                     .shouldCacheVideoBids(true)
                     .build();
     
    -        // just a stub to get through method call chain
    -        givenCacheServiceResult(singletonMap(bid1, CacheIdInfo.of(null, null)));
    +        final String modifiedAdm = "modifiedAdm";
    +        given(vastModifier.createBidVastXml(any(), any(), any(), any(), any(), any(), any(), any()))
    +                .willReturn(modifiedAdm);
     
    -        given(bidderCatalog.isModifyingVastXmlAllowed(eq("bidder1"))).willReturn(true);
    +        // just a stub to get through method call chain
    +        givenCacheServiceResult(singletonList(CacheInfo.empty()));
     
             // when
    -        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false);
    +        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS);
     
             // then
    -        Map> biddersToCacheBidIds = new HashMap<>();
    -        biddersToCacheBidIds.put("bidder1", Collections.singletonList("bidId1"));
    -        biddersToCacheBidIds.put("bidder2", Collections.singletonList("bidId2"));
    +        final EventsContext expectedEventContext = EventsContext.builder()
    +                .auctionId("123")
    +                .enabledForAccount(true)
    +                .enabledForRequest(true)
    +                .auctionTimestamp(1000L)
    +                .build();
    +
    +        verify(vastModifier)
    +                .createBidVastXml(eq(bidder1), eq(null), eq(BID_NURL), eq(bidId1),
    +                        eq(accountId), eq(expectedEventContext), eq(emptyList()), eq(null));
    +
    +        final ArgumentCaptor> bidInfoCaptor = ArgumentCaptor.forClass(List.class);
             verify(cacheService).cacheBidsOpenrtb(
    -                argThat(t -> t.containsAll(asList(bid1, bid2))),
    +                bidInfoCaptor.capture(),
                     same(auctionContext),
    -                eq(CacheContext.builder()
    -                        .shouldCacheVideoBids(true)
    -                        .bidderToVideoBidIdsToModify(singletonMap("bidder1", singletonList("bidId1")))
    -                        .bidderToBidIds(biddersToCacheBidIds)
    -                        .build()),
    -                eq(EventsContext.builder()
    -                        .enabledForAccount(true)
    -                        .enabledForRequest(true)
    -                        .auctionTimestamp(1000L)
    -                        .build()));
    +                eq(CacheContext.builder().shouldCacheVideoBids(true).build()),
    +                eq(expectedEventContext));
    +
    +        assertThat(bidInfoCaptor.getValue())
    +                .extracting(bidInfo -> bidInfo.getBid().getId(), bidInfo -> bidInfo.getBid().getAdm())
    +                .containsOnly(tuple("bidId1", "modifiedAdm"), tuple("bidId2", "adm2"));
    +    }
    +
    +    @Test
    +    public void shouldModifyBidAdmWhenBidVideoAndVastModifierReturnValue() {
    +        // given
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
    +                identity(),
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp()));
    +
    +        final String bidId = "bid_id";
    +        final Bid bid = Bid.builder()
    +                .id(bidId)
    +                .price(BigDecimal.ONE)
    +                .adm(BID_ADM)
    +                .nurl(BID_NURL)
    +                .impid(IMP_ID)
    +                .build();
    +
    +        final String bidder = "bidder1";
    +        final List bidderResponses = singletonList(
    +                BidderResponse.of(bidder, givenSeatBid(BidderBid.of(bid, video, "USD")), 100));
    +
    +        final String modifiedVast = "modifiedVast";
    +        given(vastModifier
    +                .createBidVastXml(anyString(), anyString(), anyString(),
    +                        anyString(), anyString(), any(), any(), any()))
    +                .willReturn(modifiedVast);
    +
    +        // when
    +        final BidResponse bidResponse =
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
    +
    +        // then
    +        assertThat(bidResponse.getSeatbid())
    +                .flatExtracting(SeatBid::getBid).hasSize(1)
    +                .extracting(Bid::getAdm)
    +                .containsOnly(modifiedVast);
    +
    +        verify(vastModifier)
    +                .createBidVastXml(eq(bidder), eq(BID_ADM), eq(BID_NURL), eq(bidId), eq("accountId"), any(), any(),
    +                        any());
         }
     
    +    @SuppressWarnings("unchecked")
         @Test
         public void shouldCallCacheServiceEvenRoundedCpmIsZero() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    +        final Imp imp1 = givenImp();
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(imp1));
     
    -        final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(0.05)).build();
    +        final Bid bid1 = Bid.builder().id("bidId1").impid(IMP_ID).price(BigDecimal.valueOf(0.05)).build();
             final List bidderResponses = singletonList(
                     BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid1, banner, "USD")), 100));
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
             // just a stub to get through method call chain
    -        givenCacheServiceResult(singletonMap(bid1, CacheIdInfo.of(null, null)));
    +        givenCacheServiceResult(singletonList(CacheInfo.empty()));
     
             // when
    -        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false);
    +        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS);
     
             // then
    +        final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
             verify(cacheService).cacheBidsOpenrtb(
    -                argThat(bids -> bids.contains(bid1)),
    +                bidsArgumentCaptor.capture(),
                     same(auctionContext),
    -                eq(CacheContext.builder()
    -                        .bidderToVideoBidIdsToModify(emptyMap())
    -                        .bidderToBidIds(singletonMap("bidder1", Collections.singletonList("bidId1")))
    -                        .build()),
    -                eq(EventsContext.builder().auctionTimestamp(1000L).build()));
    +                eq(CacheContext.builder().build()),
    +                eq(EventsContext.builder().auctionId("123").auctionTimestamp(1000L).build()));
    +
    +        assertThat(bidsArgumentCaptor.getValue()).extracting(bidInfo -> bidInfo.getBid().getId())
    +                .containsOnly("bidId1");
         }
     
         @Test
         public void shouldSetExpectedConstantResponseFields() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
     
             final List bidderResponses = singletonList(BidderResponse.of("bidder1", givenSeatBid(), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, null, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, null, MULTI_BIDS).result();
     
             // then
             final BidResponse responseWithExpectedFields = BidResponse.builder()
                     .id("123")
                     .cur("USD")
    -                .ext(mapper.valueToTree(
    -                        ExtBidResponse.of(null, null, singletonMap("bidder1", 100), 1000L, null,
    -                                ExtBidResponsePrebid.of(1000L))))
    +                .ext(ExtBidResponse.builder()
    +                        .responsetimemillis(singletonMap("bidder1", 100))
    +                        .tmaxrequest(1000L)
    +                        .prebid(ExtBidResponsePrebid.of(1000L, null))
    +                        .build())
                     .build();
     
             assertThat(bidResponse)
                     .isEqualToIgnoringGivenFields(responseWithExpectedFields, "nbr", "seatbid");
     
             verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
    -
         }
     
         @Test
         public void shouldSetNbrValueTwoAndEmptySeatbidWhenIncomingBidResponsesAreEmpty() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
     
             // when
    -        final BidResponse bidResponse = bidResponseCreator.create(emptyList(), auctionContext, null, false).result();
    +        final BidResponse bidResponse =
    +                bidResponseCreator.create(emptyList(), auctionContext, null, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse).returns(0, BidResponse::getNbr);
    @@ -398,13 +651,13 @@ public void shouldSetNbrValueTwoAndEmptySeatbidWhenIncomingBidResponsesAreEmpty(
         @Test
         public void shouldSetNbrValueTwoAndEmptySeatbidWhenIncomingBidResponsesDoNotContainAnyBids() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
     
             final List bidderResponses = singletonList(BidderResponse.of("bidder1", givenSeatBid(), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, null, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, null, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse).returns(0, BidResponse::getNbr);
    @@ -416,15 +669,15 @@ public void shouldSetNbrValueTwoAndEmptySeatbidWhenIncomingBidResponsesDoNotCont
         @Test
         public void shouldSetNbrNullAndPopulateSeatbidWhenAtLeastOneBidIsPresent() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
     
    -        final Bid bid = Bid.builder().impid("imp1").build();
    +        final Bid bid = Bid.builder().impid(IMP_ID).id("bidId").build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
    -                givenSeatBid(BidderBid.of(bid, null, null)), 100));
    +                givenSeatBid(BidderBid.of(bid, banner, null)), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getNbr()).isNull();
    @@ -436,16 +689,16 @@ public void shouldSetNbrNullAndPopulateSeatbidWhenAtLeastOneBidIsPresent() {
         @Test
         public void shouldSkipBidderResponsesWhereSeatBidContainEmptyBids() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
     
    -        final Bid bid = Bid.builder().impid("imp1").build();
    +        final Bid bid = Bid.builder().impid(IMP_ID).id("bidId").build();
             final List bidderResponses = asList(
                     BidderResponse.of("bidder1", givenSeatBid(), 0),
                     BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(bid, banner, "USD")), 0));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).hasSize(1);
    @@ -454,31 +707,24 @@ public void shouldSkipBidderResponsesWhereSeatBidContainEmptyBids() {
         }
     
         @Test
    -    public void shouldOverrideBidIdWhenGenerateBidIdIsTurnedOn() {
    +    public void shouldOverrideBidIdWhenIdGeneratorIsUUID() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    -        final ExtPrebid prebid = ExtPrebid.of(ExtBidPrebid.builder().type(banner).build(), null);
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
             final Bid bid = Bid.builder()
                     .id("123")
    -                .impid("imp123")
    -                .ext(mapper.valueToTree(prebid))
    +                .impid(IMP_ID)
    +                .ext(mapper.createObjectNode()
    +                        .set("prebid", mapper.valueToTree(ExtBidPrebid.builder().type(banner).build())))
                     .build();
             final List bidderResponses = singletonList(
                     BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(bid, banner, "USD")), 0));
     
    -        final BidResponseCreator bidResponseCreator = new BidResponseCreator(
    -                cacheService,
    -                bidderCatalog,
    -                eventsService,
    -                storedRequestProcessor,
    -                true,
    -                0,
    -                clock,
    -                jacksonMapper);
    +        given(idGenerator.getType()).willReturn(IdGeneratorType.uuid);
    +        given(idGenerator.generateId()).willReturn("de7fc739-0a6e-41ad-8961-701c30c82166");
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
    @@ -489,80 +735,161 @@ public void shouldOverrideBidIdWhenGenerateBidIdIsTurnedOn() {
                     .extracting(ExtBidPrebid::getBidid)
                     .hasSize(1)
                     .first()
    -                .satisfies(bidId -> assertThat(UUID.fromString(bidId)).isInstanceOf(UUID.class));
    +                .isEqualTo("de7fc739-0a6e-41ad-8961-701c30c82166");
     
             verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
         }
     
    +    @Test
    +    public void shouldUseGeneratedBidIdForEventAndCacheWhenIdGeneratorIsUUIDAndEventEnabledForAccountAndRequest() {
    +        // given
    +        final Account account = Account.builder()
    +                .id("accountId")
    +                .auction(AccountAuctionConfig.builder()
    +                        .events(AccountEventsConfig.of(true))
    +                        .build())
    +                .build();
    +        final Imp imp = givenImp();
    +
    +        // Allow events for request
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                extBuilder -> extBuilder
    +                        .events(mapper.createObjectNode())
    +                        .integration("pbjs"),
    +                imp);
    +        final AuctionContext auctionContext = givenAuctionContext(
    +                bidRequest,
    +                contextBuilder -> contextBuilder.account(account));
    +
    +        final Bid bid = Bid.builder()
    +                .id("bidId1")
    +                .impid(IMP_ID)
    +                .price(BigDecimal.ONE)
    +                .build();
    +
    +        final String bidder = "bidder1";
    +        final List bidderResponses = singletonList(
    +                BidderResponse.of(bidder, givenSeatBid(BidderBid.of(bid, banner, "USD")), 0));
    +
    +        final String generatedBidId = "de7fc739-0a6e-41ad-8961-701c30c82166";
    +        given(idGenerator.getType()).willReturn(IdGeneratorType.uuid);
    +        given(idGenerator.generateId()).willReturn(generatedBidId);
    +
    +        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
    +        givenCacheServiceResult(singletonList(CacheInfo.of("id", null, null, null)));
    +
    +        // when
    +        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
    +
    +        // then
    +        final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().bidid(generatedBidId).type(banner).build();
    +        final Bid expectedBid = bid.toBuilder()
    +                .ext(mapper.createObjectNode().set("prebid", mapper.valueToTree(extBidPrebid)))
    +                .build();
    +
    +        final BidInfo expectedBidInfo = toBidInfo(expectedBid, imp, bidder, banner, true);
    +        verify(cacheService).cacheBidsOpenrtb(eq(singletonList(expectedBidInfo)), any(), any(), any());
    +
    +        verify(eventsService).createEvent(eq(generatedBidId), anyString(), anyString(), any(), anyBoolean(), any());
    +    }
    +
    +    @SuppressWarnings("unchecked")
         @Test
         public void shouldSetExpectedResponseSeatBidAndBidFields() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.ONE).adm("adm").impid("i1")
    -                .ext(mapper.valueToTree(singletonMap("bidExt", 1))).build();
    -        final List bidderResponses = singletonList(BidderResponse.of("bidder1",
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
    +        final ObjectNode bidExt = mapper.createObjectNode()
    +                .put("origbidcpm", BigDecimal.ONE)
    +                .put("origbidcur", "USD");
    +        final Bid bid = Bid.builder()
    +                .id("bidId")
    +                .price(BigDecimal.ONE)
    +                .adm(BID_ADM)
    +                .impid(IMP_ID)
    +                .ext(bidExt)
    +                .build();
    +
    +        final String bidder = "bidder1";
    +        final List bidderResponses = singletonList(BidderResponse.of(bidder,
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
    +        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
    +        givenCacheServiceResult(emptyList());
    +
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
    -        assertThat(bidResponse.getSeatbid()).containsOnly(SeatBid.builder()
    -                .seat("bidder1")
    -                .group(0)
    -                .bid(singletonList(Bid.builder()
    -                        .id("bidId")
    -                        .impid("i1")
    -                        .price(BigDecimal.ONE)
    -                        .adm("adm")
    -                        .ext(mapper.valueToTree(ExtPrebid.of(
    -                                ExtBidPrebid.builder().type(banner).build(),
    -                                singletonMap("bidExt", 1))))
    -                        .build()))
    -                .build());
    +        final ObjectNode expectedBidExt = mapper.createObjectNode();
    +        expectedBidExt.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().type(banner).build()));
    +        expectedBidExt.put("origbidcpm", BigDecimal.ONE);
    +        expectedBidExt.put("origbidcur", "USD");
     
    -        verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
    +        final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(cacheService).cacheBidsOpenrtb(bidsArgumentCaptor.capture(), any(), any(), any());
    +
    +        assertThat(bidsArgumentCaptor.getValue()).extracting(bidInfo -> bidInfo.getBid().getExt())
    +                .containsOnly(expectedBidExt);
    +
    +        assertThat(bidResponse.getSeatbid())
    +                .containsOnly(SeatBid.builder()
    +                        .seat(bidder)
    +                        .group(0)
    +                        .bid(singletonList(Bid.builder()
    +                                .id("bidId")
    +                                .impid(IMP_ID)
    +                                .price(BigDecimal.ONE)
    +                                .adm(BID_ADM)
    +                                .ext(expectedBidExt)
    +                                .build()))
    +                        .build());
         }
     
    +    @SuppressWarnings("unchecked")
         @Test
    -    public void shouldFilterByDealsAndPriceBidsWhenImpIdsAreEqual() {
    +    public void shouldNotWriteSkadnAttributeToBidderSection() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    -
    -        final Bid simpleBidImp1 = Bid.builder().id("bidId1i1").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    -        final Bid simpleBid1Imp2 = Bid.builder().id("bidId1i2").price(BigDecimal.valueOf(15.67)).impid("i2").build();
    -        final Bid simpleBid2Imp2 = Bid.builder().id("bidId2i2").price(BigDecimal.valueOf(17.67)).impid("i2").build();
    -        final Bid dealBid1Imp1 = Bid.builder().id("bidId1i1d").dealid("d1").price(BigDecimal.valueOf(4.98)).impid("i1")
    -                .build();
    -        final Bid dealBid2Imp1 = Bid.builder().id("bidId2i1d").dealid("d2").price(BigDecimal.valueOf(5.00)).impid("i1")
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
    +        final Map bidExtProperties = new HashMap<>();
    +        bidExtProperties.put("skadn", singletonMap("skadnKey", "skadnValue"));
    +        bidExtProperties.put("origbidcur", "USD");
    +        final Bid bid = Bid.builder()
    +                .id("bidId")
    +                .price(BigDecimal.ONE)
    +                .adm(BID_ADM)
    +                .impid(IMP_ID)
    +                .ext(mapper.valueToTree(bidExtProperties))
                     .build();
    -        final BidderSeatBid seatBidWithDeals = givenSeatBid(
    -                BidderBid.of(simpleBidImp1, banner, null),
    -                BidderBid.of(simpleBid1Imp2, banner, null),
    -                BidderBid.of(simpleBid2Imp2, banner, null), // will stay (top price)
    -                BidderBid.of(dealBid2Imp1, banner, null),   // will stay (deal + topPrice)
    -                BidderBid.of(dealBid1Imp1, banner, null));
     
    -        final Bid simpleBid3Imp2 = Bid.builder().id("bidId3i2").price(BigDecimal.valueOf(7.25)).impid("i2").build();
    -        final Bid simpleBid4Imp2 = Bid.builder().id("bidId4i2").price(BigDecimal.valueOf(7.26)).impid("i2").build();
    -        final BidderSeatBid seatBidWithSimpleBids = givenSeatBid(
    -                BidderBid.of(simpleBid3Imp2, banner, null),
    -                BidderBid.of(simpleBid4Imp2, banner, null)); // will stay (top price)
    +        final String bidder = "bidder1";
    +        final List bidderResponses = singletonList(BidderResponse.of(bidder,
    +                givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
    -        final List bidderResponses = asList(
    -                BidderResponse.of("bidder1", seatBidWithDeals, 100),
    -                BidderResponse.of("bidder2", seatBidWithSimpleBids, 111));
    +        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
    +        givenCacheServiceResult(emptyList());
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
    +        final ObjectNode expectedBidExt = mapper.createObjectNode();
    +        expectedBidExt.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().type(banner).build()));
    +        expectedBidExt.put("origbidcur", "USD");
    +        expectedBidExt.set("skadn", mapper.convertValue(singletonMap("skadnKey", "skadnValue"), JsonNode.class));
    +
    +        final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(cacheService).cacheBidsOpenrtb(bidsArgumentCaptor.capture(), any(), any(), any());
    +
    +        assertThat(bidsArgumentCaptor.getValue()).extracting(bidInfo -> bidInfo.getBid().getExt())
    +                .containsOnly(expectedBidExt);
    +
             assertThat(bidResponse.getSeatbid())
    -                .flatExtracting(SeatBid::getBid).hasSize(3)
    -                .extracting(Bid::getId)
    -                .containsOnly("bidId2i2", "bidId2i1d", "bidId4i2");
    +                .flatExtracting(SeatBid::getBid)
    +                .extracting(Bid::getExt)
    +                .containsExactly(expectedBidExt);
         }
     
         @Test
    @@ -579,9 +906,8 @@ public void shouldAddTypeToNativeBidAdm() throws JsonProcessingException {
             final BidRequest bidRequest = BidRequest.builder()
                     .cur(singletonList("USD"))
                     .tmax(1000L)
    -                .app(App.builder().build())
                     .imp(singletonList(Imp.builder()
    -                        .id("imp1")
    +                        .id(IMP_ID)
                             .xNative(Native.builder().request(mapper.writeValueAsString(nativeRequest)).build())
                             .build()))
                     .build();
    @@ -595,7 +921,7 @@ public void shouldAddTypeToNativeBidAdm() throws JsonProcessingException {
                             .build()))
                     .build();
     
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.ONE).impid("imp1")
    +        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.ONE).impid(IMP_ID)
                     .adm(mapper.writeValueAsString(responseAdm))
                     .ext(mapper.valueToTree(singletonMap("bidExt", 1))).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
    @@ -603,7 +929,7 @@ public void shouldAddTypeToNativeBidAdm() throws JsonProcessingException {
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).hasSize(1)
    @@ -636,7 +962,7 @@ public void shouldReturnEmptyAssetIfImageTypeIsEmpty() throws JsonProcessingExce
                     .tmax(1000L)
                     .app(App.builder().build())
                     .imp(singletonList(Imp.builder()
    -                        .id("imp1")
    +                        .id(IMP_ID)
                             .xNative(Native.builder().request(mapper.writeValueAsString(nativeRequest)).build())
                             .build()))
                     .build();
    @@ -651,15 +977,19 @@ public void shouldReturnEmptyAssetIfImageTypeIsEmpty() throws JsonProcessingExce
                             .build()))
                     .build();
     
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.ONE).impid("imp1")
    +        final Bid bid = Bid.builder()
    +                .id("bidId")
    +                .price(BigDecimal.ONE)
    +                .impid(IMP_ID)
                     .adm(mapper.writeValueAsString(responseAdm))
    -                .ext(mapper.valueToTree(singletonMap("bidExt", 1))).build();
    +                .ext(mapper.valueToTree(singletonMap("bidExt", 1)))
    +                .build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, xNative, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).hasSize(1)
    @@ -686,7 +1016,7 @@ public void shouldReturnEmptyAssetIfDataTypeIsEmpty() throws JsonProcessingExcep
                     .tmax(1000L)
                     .app(App.builder().build())
                     .imp(singletonList(Imp.builder()
    -                        .id("imp1")
    +                        .id(IMP_ID)
                             .xNative(Native.builder().request(mapper.writeValueAsString(nativeRequest)).build())
                             .build()))
                     .build();
    @@ -701,15 +1031,18 @@ public void shouldReturnEmptyAssetIfDataTypeIsEmpty() throws JsonProcessingExcep
                             .build()))
                     .build();
     
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.ONE).impid("imp1")
    +        final Bid bid = Bid.builder().id("bidId")
    +                .price(BigDecimal.ONE)
    +                .impid(IMP_ID)
                     .adm(mapper.writeValueAsString(responseAdm))
    -                .ext(mapper.valueToTree(singletonMap("bidExt", 1))).build();
    +                .ext(mapper.valueToTree(singletonMap("bidExt", 1)))
    +                .build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, xNative, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).hasSize(1)
    @@ -725,19 +1058,25 @@ public void shouldSetBidAdmToNullIfCacheIdIsPresentAndReturnCreativeBidsIsFalse(
             // given
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(givenTargeting())));
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp()));
     
    -        final Bid bid = Bid.builder().price(BigDecimal.ONE).adm("adm").impid("i1").build();
    +        final Bid bid = Bid.builder()
    +                .price(BigDecimal.ONE)
    +                .adm(BID_ADM)
    +                .id("bidId")
    +                .impid(IMP_ID)
    +                .build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
    -        givenCacheServiceResult(singletonMap(bid, CacheIdInfo.of("id", null)));
    +        givenCacheServiceResult(singletonList(CacheInfo.of("id", null, null, null)));
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
    @@ -753,19 +1092,20 @@ public void shouldSetBidAdmToNullIfVideoCacheIdIsPresentAndReturnCreativeVideoBi
             // given
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(givenTargeting())));
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp()));
     
    -        final Bid bid = Bid.builder().price(BigDecimal.ONE).adm("adm").impid("i1").build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).adm(BID_ADM).id("bidId").impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
     
    -        givenCacheServiceResult(singletonMap(bid, CacheIdInfo.of("id", null)));
    +        givenCacheServiceResult(singletonList(CacheInfo.of("id", null, null, null)));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
    @@ -776,51 +1116,150 @@ public void shouldSetBidAdmToNullIfVideoCacheIdIsPresentAndReturnCreativeVideoBi
             verify(cacheService).cacheBidsOpenrtb(anyList(), any(), any(), any());
         }
     
    +    @Test
    +    public void shouldSetBidExpWhenCacheIdIsMatched() {
    +        // given
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
    +                identity(),
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp(Collections.singletonMap("dealId", "lineItemId1"))));
    +
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).impid(IMP_ID).id("bidId").build();
    +        final List bidderResponses = singletonList(BidderResponse.of("bidder1",
    +                givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
    +
    +        givenCacheServiceResult(singletonList(CacheInfo.of("id", null, 100, null)));
    +
    +        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
    +
    +        // when
    +        final BidResponse bidResponse =
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
    +
    +        // then
    +        assertThat(bidResponse.getSeatbid())
    +                .flatExtracting(SeatBid::getBid).hasSize(1)
    +                .extracting(Bid::getExp)
    +                .containsOnly(100);
    +
    +        verify(cacheService).cacheBidsOpenrtb(anyList(), any(), any(), any());
    +    }
    +
    +    @Test
    +    public void shouldSetBidExpMaxTtlWhenCacheIdIsMatchedAndBothTtlIsSet() {
    +        // given
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
    +                identity(),
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp()));
    +
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).impid(IMP_ID).id("bidId").build();
    +        final List bidderResponses = singletonList(BidderResponse.of("bidder1",
    +                givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
    +
    +        givenCacheServiceResult(singletonList(CacheInfo.of("id", null, 100, 200)));
    +
    +        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
    +
    +        // when
    +        final BidResponse bidResponse =
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
    +
    +        // then
    +        assertThat(bidResponse.getSeatbid())
    +                .flatExtracting(SeatBid::getBid).hasSize(1)
    +                .extracting(Bid::getExp)
    +                .containsOnly(200);
    +
    +        verify(cacheService).cacheBidsOpenrtb(anyList(), any(), any(), any());
    +    }
    +
         @Test
         public void shouldTolerateMissingExtInSeatBidAndBid() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.ONE).impid("i1").build();
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
    +
    +        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.ONE).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
    +        final ObjectNode expectedBidExt = mapper.createObjectNode();
    +        expectedBidExt.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().type(banner).build()));
             assertThat(bidResponse.getSeatbid()).hasSize(1)
                     .flatExtracting(SeatBid::getBid)
                     .containsOnly(Bid.builder()
                             .id("bidId")
    -                        .impid("i1")
    +                        .impid(IMP_ID)
                             .price(BigDecimal.ONE)
    -                        .ext(mapper.valueToTree(
    -                                ExtPrebid.of(ExtBidPrebid.builder().type(banner).build(), null)))
    +                        .ext(expectedBidExt)
                             .build());
     
             verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
         }
     
         @Test
    -    public void shouldPopulateTargetingKeywords() {
    +    public void shouldPassPreferDealsToWinningComparatorFactoryWhenBidRequestTrue() {
             // given
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
    -                identity(),
    +                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(givenImp("i1"))),
    +                extBuilder -> extBuilder.targeting(givenTargeting().toBuilder().preferdeals(true).build())));
    +
    +        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +
    +        final List bidderResponses = singletonList(BidderResponse.of("bidder1",
    +                givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
    +
    +        // when
    +        bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
    +
    +        // then
    +        verify(winningBidComparatorFactory, times(2)).create(eq(true));
    +    }
    +
    +    @Test
    +    public void shouldPassPreferDealsFalseWhenBidRequestPreferDealsIsNotDefined() {
    +        // given
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(givenImp("i1"))),
                     extBuilder -> extBuilder.targeting(givenTargeting())));
     
             final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +
    +        final List bidderResponses = singletonList(BidderResponse.of("bidder1",
    +                givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
    +
    +        // when
    +        bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
    +
    +        // then
    +        verify(winningBidComparatorFactory, times(2)).create(eq(false));
    +    }
    +
    +    @Test
    +    public void shouldPopulateTargetingKeywords() {
    +        // given
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
    +                identity(),
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp()));
    +
    +        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
                     .flatExtracting(SeatBid::getBid).hasSize(1)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getTargeting())
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting())
                     .flatExtracting(Map::entrySet)
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
                     .containsOnly(
    @@ -837,30 +1276,34 @@ public void shouldTruncateTargetingKeywordsByGlobalConfig() {
             // given
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(givenTargeting())));
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp()));
     
    -        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("someVeryLongBidderName",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             final BidResponseCreator bidResponseCreator = new BidResponseCreator(
                     cacheService,
                     bidderCatalog,
    +                vastModifier,
                     eventsService,
                     storedRequestProcessor,
    -                false,
    +                winningBidComparatorFactory,
    +                idGenerator,
    +                hookStageExecutor,
                     20,
                     clock,
                     jacksonMapper);
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
                     .flatExtracting(SeatBid::getBid).hasSize(1)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getTargeting())
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting())
                     .flatExtracting(Map::entrySet)
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
                     .containsOnly(
    @@ -874,26 +1317,32 @@ public void shouldTruncateTargetingKeywordsByGlobalConfig() {
         @Test
         public void shouldTruncateTargetingKeywordsByAccountConfig() {
             // given
    -        final Account account = Account.builder().id("accountId").truncateTargetAttr(20).build();
    +        final Account account = Account.builder()
    +                .id("accountId")
    +                .auction(AccountAuctionConfig.builder()
    +                        .truncateTargetAttr(20)
    +                        .build())
    +                .build();
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(givenTargeting()));
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp());
             final AuctionContext auctionContext = givenAuctionContext(
                     bidRequest,
                     contextBuilder -> contextBuilder.account(account));
     
    -        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("someVeryLongBidderName",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
                     .flatExtracting(SeatBid::getBid).hasSize(1)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getTargeting())
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting())
                     .flatExtracting(Map::entrySet)
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
                     .containsOnly(
    @@ -906,33 +1355,41 @@ public void shouldTruncateTargetingKeywordsByAccountConfig() {
         @Test
         public void shouldTruncateTargetingKeywordsByRequestPassedValue() {
             // given
    -        final Account account = Account.builder().id("accountId").truncateTargetAttr(25).build();
    +        final Account account = Account.builder()
    +                .id("accountId")
    +                .auction(AccountAuctionConfig.builder()
    +                        .truncateTargetAttr(25)
    +                        .build())
    +                .build();
    +        final ExtRequestTargeting targeting = ExtRequestTargeting.builder()
    +                .pricegranularity(mapper.valueToTree(
    +                        ExtPriceGranularity.of(2,
    +                                singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5))))))
    +                .includewinners(true)
    +                .includebidderkeys(true)
    +                .includeformat(false)
    +                .truncateattrchars(20)
    +                .build();
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(ExtRequestTargeting.builder()
    -                        .pricegranularity(mapper.valueToTree(
    -                                ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5),
    -                                        BigDecimal.valueOf(0.5))))))
    -                        .includewinners(true)
    -                        .includebidderkeys(true)
    -                        .truncateattrchars(20)
    -                        .build()));
    +                extBuilder -> extBuilder.targeting(targeting),
    +                givenImp());
             final AuctionContext auctionContext = givenAuctionContext(
                     bidRequest,
                     contextBuilder -> contextBuilder.account(account));
     
    -        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("someVeryLongBidderName",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
                     .flatExtracting(SeatBid::getBid).hasSize(1)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getTargeting())
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting())
                     .flatExtracting(Map::entrySet)
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
                     .containsOnly(
    @@ -942,26 +1399,146 @@ public void shouldTruncateTargetingKeywordsByRequestPassedValue() {
                             tuple("hb_bidder_someVeryLo", "someVeryLongBidderName"));
         }
     
    +    @Test
    +    public void shouldReduceAndNotPopulateTargetingKeywordsForExtraBidsWhenCodePrefixIsNotDefined() {
    +        // given
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
    +                identity(),
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp("i1"), givenImp("i2")));
    +
    +        final String bidder1 = "bidder1";
    +        final Map multiBidMap = singletonMap(bidder1, MultiBidConfig.of(bidder1, 3, null));
    +
    +        final Bid bidder1Bid1 = Bid.builder().id("bidder1Bid1").price(BigDecimal.valueOf(3.67)).impid("i1").build();
    +        final Bid bidder1Bid2 = Bid.builder().id("bidder1Bid2").price(BigDecimal.valueOf(4.98)).impid("i1").build();
    +        final Bid bidder1Bid3 = Bid.builder().id("bidder1Bid3").price(BigDecimal.valueOf(1.08)).impid("i1").build();
    +        final Bid bidder1Bid4 = Bid.builder().id("bidder1Bid4").price(BigDecimal.valueOf(11.8)).impid("i1").build();
    +        final Bid bidder1Bid5 = Bid.builder().id("bidder1Bid5").price(BigDecimal.valueOf(1.08)).impid("i2").build();
    +
    +        final List bidderResponses = singletonList(
    +                BidderResponse.of(bidder1,
    +                        givenSeatBid(
    +                                BidderBid.of(bidder1Bid1, banner, null),  // extra bid
    +                                BidderBid.of(bidder1Bid2, banner, null),  // extra bid
    +                                BidderBid.of(bidder1Bid3, banner, null),  // Will be removed by price
    +                                BidderBid.of(bidder1Bid4, banner, null),
    +                                BidderBid.of(bidder1Bid5, banner, null)),
    +                        100));
    +
    +        // when
    +        final BidResponse result =
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, multiBidMap).result();
    +
    +        // then
    +        assertThat(result.getSeatbid())
    +                .flatExtracting(SeatBid::getBid).hasSize(4)
    +                .extracting(
    +                        Bid::getId,
    +                        bid -> toTargetingByKey(bid, "hb_bidder"),
    +                        bid -> toTargetingByKey(bid, "hb_bidder_bidder1"),
    +                        BidResponseCreatorTest::getTargetingBidderCode)
    +                .containsOnly(
    +                        tuple("bidder1Bid4", bidder1, bidder1, bidder1),
    +                        tuple("bidder1Bid2", null, null, null),
    +                        tuple("bidder1Bid1", null, null, null),
    +                        tuple("bidder1Bid5", bidder1, bidder1, null));
    +
    +        verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
    +    }
    +
    +    @Test
    +    public void shouldReduceAndPopulateTargetingKeywordsForExtraBidsWhenCodePrefixIsDefined() {
    +        // given
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
    +                identity(),
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp("i1"), givenImp("i2")));
    +
    +        final String bidder1 = "bidder1";
    +        final String codePrefix = "bN";
    +        final Map multiBidMap = singletonMap(bidder1,
    +                MultiBidConfig.of(bidder1, 3, codePrefix));
    +
    +        final Bid bidder1Bid1 = Bid.builder().id("bidder1Bid1").price(BigDecimal.valueOf(3.67)).impid("i1").build();
    +        final Bid bidder1Bid2 = Bid.builder().id("bidder1Bid2").price(BigDecimal.valueOf(4.88)).impid("i1").build();
    +        final Bid bidder1Bid3 = Bid.builder().id("bidder1Bid3").price(BigDecimal.valueOf(1.08)).impid("i1").build();
    +        final Bid bidder1Bid4 = Bid.builder().id("bidder1Bid4").price(BigDecimal.valueOf(11.8)).impid("i1").build();
    +        final Bid bidder1Bid5 = Bid.builder().id("bidder1Bid5").price(BigDecimal.valueOf(1.08)).impid("i2").build();
    +
    +        final List bidderResponses = singletonList(
    +                BidderResponse.of(bidder1,
    +                        givenSeatBid(
    +                                BidderBid.of(bidder1Bid1, banner, null),  // extra bid
    +                                BidderBid.of(bidder1Bid2, banner, null),  // extra bid
    +                                BidderBid.of(bidder1Bid3, banner, null),  // Will be removed by price
    +                                BidderBid.of(bidder1Bid4, banner, null),
    +                                BidderBid.of(bidder1Bid5, banner, null)),
    +                        100));
    +
    +        // when
    +        final BidResponse result =
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, multiBidMap).result();
    +
    +        // then
    +        final Map bidder1Bid4Targeting = new HashMap<>();
    +        bidder1Bid4Targeting.put("hb_pb", "5.00");
    +        bidder1Bid4Targeting.put("hb_pb_" + bidder1, "5.00");
    +        bidder1Bid4Targeting.put("hb_bidder_" + bidder1, bidder1);
    +        bidder1Bid4Targeting.put("hb_bidder", bidder1);
    +        final ObjectNode bidder1Bid4Ext = extWithTargeting(bidder1, bidder1Bid4Targeting);
    +        final Bid expectedBidder1Bid4 = bidder1Bid4.toBuilder().ext(bidder1Bid4Ext).build();
    +
    +        final String bidderCodeForBidder1Bid2 = String.format("%s%s", codePrefix, 2);
    +        final Map bidder1Bid2Targeting = new HashMap<>();
    +        bidder1Bid2Targeting.put("hb_bidder_" + bidderCodeForBidder1Bid2, bidderCodeForBidder1Bid2);
    +        bidder1Bid2Targeting.put("hb_pb_" + bidderCodeForBidder1Bid2, "4.50");
    +        final ObjectNode bidder1Bid2Ext = extWithTargeting(bidderCodeForBidder1Bid2, bidder1Bid2Targeting);
    +        final Bid expectedBidder1Bid2 = bidder1Bid2.toBuilder().ext(bidder1Bid2Ext).build();
    +
    +        final String bidderCodeForBidder1Bid1 = String.format("%s%s", codePrefix, 3);
    +        final Map bidder1Bid1Targeting = new HashMap<>();
    +        bidder1Bid1Targeting.put("hb_bidder_" + bidderCodeForBidder1Bid1, bidderCodeForBidder1Bid1);
    +        bidder1Bid1Targeting.put("hb_pb_" + bidderCodeForBidder1Bid1, "3.50");
    +        final ObjectNode bidder1Bid1Ext = extWithTargeting(bidderCodeForBidder1Bid1, bidder1Bid1Targeting);
    +        final Bid expectedBidder1Bid1 = bidder1Bid1.toBuilder().ext(bidder1Bid1Ext).build();
    +
    +        final Map bidder1Bid5Targeting = new HashMap<>();
    +        bidder1Bid5Targeting.put("hb_pb", "1.00");
    +        bidder1Bid5Targeting.put("hb_pb_" + bidder1, "1.00");
    +        bidder1Bid5Targeting.put("hb_bidder_" + bidder1, bidder1);
    +        bidder1Bid5Targeting.put("hb_bidder", bidder1);
    +        final ObjectNode bidder1Bid5Ext = extWithTargeting(null, bidder1Bid5Targeting);
    +        final Bid expectedBidder1Bid5 = bidder1Bid5.toBuilder().ext(bidder1Bid5Ext).build();
    +
    +        assertThat(result.getSeatbid())
    +                .flatExtracting(SeatBid::getBid)
    +                .contains(expectedBidder1Bid4, expectedBidder1Bid2, expectedBidder1Bid1, expectedBidder1Bid5);
    +        verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
    +    }
    +
         @Test
         public void shouldPopulateTargetingKeywordsForWinningBidsAndWinningBidsByBidder() {
             // given
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(givenTargeting())));
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp("i1"), givenImp("i2")));
     
             final Bid firstBid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid("i1").build();
             final Bid secondBid = Bid.builder().id("bidId2").price(BigDecimal.valueOf(4.98)).impid("i2").build();
             final Bid thirdBid = Bid.builder().id("bidId3").price(BigDecimal.valueOf(7.25)).impid("i2").build();
             final List bidderResponses = asList(
                     BidderResponse.of("bidder1",
    -                        givenSeatBid(BidderBid.of(firstBid, banner, null),
    +                        givenSeatBid(
    +                                BidderBid.of(firstBid, banner, null),
                                     BidderBid.of(secondBid, banner, null)), 100),
                     BidderResponse.of("bidder2",
                             givenSeatBid(BidderBid.of(thirdBid, banner, null)), 111));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
    @@ -979,37 +1556,120 @@ public void shouldPopulateTargetingKeywordsForWinningBidsAndWinningBidsByBidder(
             verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
         }
     
    +    @Test
    +    public void shouldPopulateAuctionLostToMetricByWinningDealBid() {
    +        // given
    +        final String dealId1 = "dealId1";
    +        final String dealId2 = "dealId2";
    +        final String lineItemId1 = "lineItemId1";
    +        final String lineItemId2 = "lineItemId2";
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
    +                identity(),
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                Imp.builder()
    +                        .id(IMP_ID)
    +                        .pmp(Pmp.builder()
    +                                // Order defines winning bid
    +                                .deals(asList(
    +                                        Deal.builder().id("dealId1")
    +                                                .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of(
    +                                                        "lineItemId1", null, null, null)))).build(),
    +                                        Deal.builder().id("dealId2")
    +                                                .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of(
    +                                                        "lineItemId2", null, null, null)))).build()))
    +                                .build())
    +                        .build()));
    +
    +        final Bid firstBid = Bid.builder().id("bidId1").impid(IMP_ID).price(BigDecimal.valueOf(5.67))
    +                .dealid(dealId1).build();
    +        final Bid secondBid = Bid.builder().id("bidId2").impid(IMP_ID).price(BigDecimal.valueOf(4.98))
    +                .dealid(dealId2).build();
    +
    +        final List bidderResponses = asList(
    +                BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(firstBid, banner, null)), 100),
    +                BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(secondBid, banner, null)), 100));
    +
    +        // when
    +        bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
    +
    +        // then
    +        assertThat(auctionContext.getTxnLog().lostAuctionToLineItems().entrySet())
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .containsOnly(tuple(lineItemId2, singleton(lineItemId1)));
    +    }
    +
    +    @Test
    +    public void shouldIncreaseLineItemSentToClientAsTopMatchMetricInTransactionLog() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp(Collections.singletonMap("dealId", "lineItemId1")));
    +
    +        final AuctionContext auctionContext = givenAuctionContext(
    +                bidRequest,
    +                context -> context.debugContext(DebugContext.of(true, null)));
    +
    +        final Bid bid = Bid.builder()
    +                .id("bidId1")
    +                .impid(IMP_ID)
    +                .price(BigDecimal.valueOf(5.67))
    +                .dealid("dealId")
    +                .build();
    +        final List bidderResponses = singletonList(
    +                BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, null)), 100));
    +
    +        // when
    +        final BidResponse bidResponse =
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
    +
    +        // then
    +        final ExtBidResponse responseExt = bidResponse.getExt();
    +
    +        assertThat(responseExt.getDebug()).isNotNull();
    +        assertThat(responseExt.getDebug().getPgmetrics()).isNotNull();
    +        assertThat(singletonList(responseExt.getDebug().getPgmetrics()))
    +                .flatExtracting(ExtDebugPgmetrics::getSentToClientAsTopMatch)
    +                .containsOnly("lineItemId1");
    +    }
    +
         @Test
         public void shouldPopulateTargetingKeywordsFromMediaTypePriceGranularities() {
             // given
    +        final ExtMediaTypePriceGranularity extMediaTypePriceGranularity = ExtMediaTypePriceGranularity.of(
    +                mapper.valueToTree(ExtPriceGranularity.of(
    +                        3,
    +                        singletonList(ExtGranularityRange.of(
    +                                BigDecimal.valueOf(10), BigDecimal.valueOf(1))))),
    +                null,
    +                null);
    +        final ExtPriceGranularity extPriceGranularity = ExtPriceGranularity.of(2,
    +                singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5))));
    +
    +        final ExtRequestTargeting targeting = ExtRequestTargeting.builder()
    +                .pricegranularity(mapper.valueToTree(extPriceGranularity))
    +                .mediatypepricegranularity(extMediaTypePriceGranularity)
    +                .includewinners(true)
    +                .includebidderkeys(true)
    +                .includeformat(false)
    +                .build();
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(ExtRequestTargeting.builder()
    -                        .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(2,
    -                                singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5))))))
    -                        .mediatypepricegranularity(ExtMediaTypePriceGranularity.of(
    -                                mapper.valueToTree(ExtPriceGranularity.of(
    -                                        3,
    -                                        singletonList(ExtGranularityRange.of(
    -                                                BigDecimal.valueOf(10), BigDecimal.valueOf(1))))),
    -                                null,
    -                                null))
    -                        .includewinners(true)
    -                        .includebidderkeys(true)
    -                        .build())));
    +                extBuilder -> extBuilder.targeting(targeting),
    +                givenImp()));
     
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
                     .flatExtracting(SeatBid::getBid)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getTargeting())
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting())
                     .flatExtracting(Map::entrySet)
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
                     .containsOnly(
    @@ -1026,23 +1686,24 @@ public void shouldPopulateCacheIdHostPathAndUuidTargetingKeywords() {
             // given
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(givenTargeting())));
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp()));
     
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
     
    -        givenCacheServiceResult(singletonMap(bid, CacheIdInfo.of("cacheId", "videoId")));
    +        givenCacheServiceResult(singletonList(CacheInfo.of("cacheId", "videoId", null, null)));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
                     .flatExtracting(SeatBid::getBid).hasSize(1)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getTargeting())
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting())
                     .flatExtracting(Map::entrySet)
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
                     .containsOnly(
    @@ -1070,20 +1731,21 @@ public void shouldPopulateTargetingKeywordsWithAdditionalValuesFromRequest() {
                     extBuilder -> extBuilder
                             .targeting(givenTargeting())
                             .adservertargeting(singletonList(ExtRequestPrebidAdservertargetingRule.of(
    -                                "static_keyword1", xStatic, "static_keyword1")))));
    +                                "static_keyword1", xStatic, "static_keyword1"))),
    +                givenImp()));
     
    -        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of(
                     "bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
                     .flatExtracting(SeatBid::getBid).hasSize(1)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getTargeting())
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting())
                     .flatExtracting(Map::entrySet)
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
                     .contains(tuple("static_keyword1", "static_keyword1"));
    @@ -1094,39 +1756,79 @@ public void shouldPopulateTargetingKeywordsIfBidWasCachedAndAdmWasRemoved() {
             // given
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(givenTargeting())));
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp()));
     
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("impId").adm("adm").build();
    -        final List bidderResponses = singletonList(BidderResponse.of("bidder1",
    -                givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
    +        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).adm(BID_ADM).build();
    +        final List bidderResponses = singletonList(
    +                BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
                     .doCaching(true)
                     .returnCreativeBids(false) // this will cause erasing of bid.adm
                     .build();
     
    -        givenCacheServiceResult(singletonMap(bid, CacheIdInfo.of("cacheId", null)));
    +        givenCacheServiceResult(singletonList(CacheInfo.of("cacheId", null, null, null)));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
    +        // Check if you didn't lost any bids because of bid change in winningBids set
             // then
             assertThat(bidResponse.getSeatbid())
                     .flatExtracting(SeatBid::getBid).hasSize(1)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getTargeting())
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting())
                     .doesNotContainNull();
         }
     
    +    @Test
    +    public void shouldCallEventsServiceWhenEventsDisabledByRequestButBidWithLineItem() {
    +        // given
    +        final Account account = Account.builder()
    +                .id("accountId")
    +                .auction(AccountAuctionConfig.builder()
    +                        .events(AccountEventsConfig.of(true))
    +                        .build())
    +                .build();
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp(Collections.singletonMap("dealId", "lineItemId")));
    +        final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context.account(account));
    +
    +        final Bid bid = Bid.builder()
    +                .id("bidId")
    +                .price(BigDecimal.valueOf(5.67))
    +                .impid(IMP_ID)
    +                .dealid("dealId")
    +                .build();
    +        final List bidderResponses = singletonList(BidderResponse.of("bidder1",
    +                givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
    +
    +        // when
    +        bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
    +        // then
    +        verify(eventsService).createEvent(
    +                anyString(), anyString(), anyString(), eq("lineItemId"), eq(false), any());
    +    }
    +
         @Test
         public void shouldAddExtPrebidEventsIfEventsAreEnabledAndExtRequestPrebidEventPresent() {
             // given
    -        final Account account = Account.builder().id("accountId").eventsEnabled(true).build();
    +        final Account account = Account.builder()
    +                .id("accountId")
    +                .auction(AccountAuctionConfig.builder()
    +                        .events(AccountEventsConfig.of(true))
    +                        .build())
    +                .build();
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
                     extBuilder -> extBuilder
                             .events(mapper.createObjectNode())
    -                        .integration("pbjs"));
    +                        .integration("pbjs"),
    +                givenImp());
    +
             final AuctionContext auctionContext = givenAuctionContext(
                     bidRequest,
                     contextBuilder -> contextBuilder.account(account));
    @@ -1134,26 +1836,34 @@ public void shouldAddExtPrebidEventsIfEventsAreEnabledAndExtRequestPrebidEventPr
             final Bid bid = Bid.builder()
                     .id("bidId1")
                     .price(BigDecimal.valueOf(5.67))
    -                .impid("i1")
    +                .impid(IMP_ID)
                     .build();
             final List bidderResponses = singletonList(
                     BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             final Events events = Events.of("http://event-type-win", "http://event-type-view");
    -        given(eventsService.createEvent(anyString(), anyString(), anyString(), anyLong(), anyString()))
    +        given(eventsService.createEvent(anyString(), anyString(), anyString(), any(), anyBoolean(), any()))
                     .willReturn(events);
     
    +        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
    +        givenCacheServiceResult(emptyList());
    +
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
    +        final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(cacheService).cacheBidsOpenrtb(bidsArgumentCaptor.capture(), any(), any(), any());
    +
    +        assertThat(bidsArgumentCaptor.getValue())
    +                .extracting(bidInfo -> toExtBidPrebid(bidInfo.getBid().getExt()).getEvents())
    +                .containsOnly(events);
    +
             assertThat(bidResponse.getSeatbid()).hasSize(1)
                     .flatExtracting(SeatBid::getBid)
    -                .extracting(responseBid -> toExtPrebid(responseBid.getExt()).getPrebid().getEvents())
    +                .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents())
                     .containsOnly(events);
    -
    -        verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
         }
     
         @Test
    @@ -1161,14 +1871,17 @@ public void shouldAddExtPrebidEventsIfEventsAreEnabledAndAccountSupportEventsFor
             // given
             final Account account = Account.builder()
                     .id("accountId")
    -                .eventsEnabled(true)
    -                .analyticsConfig(AccountAnalyticsConfig.of(singletonMap("web", true)))
    +                .auction(AccountAuctionConfig.builder()
    +                        .events(AccountEventsConfig.of(true))
    +                        .build())
    +                .analytics(AccountAnalyticsConfig.of(singletonMap("web", true), null))
                     .build();
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
                     extBuilder -> extBuilder
                             .channel(ExtRequestPrebidChannel.of("web"))
    -                        .integration("pbjs"));
    +                        .integration("pbjs"),
    +                givenImp());
             final AuctionContext auctionContext = givenAuctionContext(
                     bidRequest,
                     contextBuilder -> contextBuilder.account(account));
    @@ -1176,23 +1889,23 @@ public void shouldAddExtPrebidEventsIfEventsAreEnabledAndAccountSupportEventsFor
             final Bid bid = Bid.builder()
                     .id("bidId1")
                     .price(BigDecimal.valueOf(5.67))
    -                .impid("i1")
    +                .impid(IMP_ID)
                     .build();
             final List bidderResponses = singletonList(
                     BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             final Events events = Events.of("http://event-type-win", "http://event-type-view");
    -        given(eventsService.createEvent(anyString(), anyString(), anyString(), anyLong(), anyString()))
    +        given(eventsService.createEvent(anyString(), anyString(), anyString(), any(), anyBoolean(), any()))
                     .willReturn(events);
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).hasSize(1)
                     .flatExtracting(SeatBid::getBid)
    -                .extracting(responseBid -> toExtPrebid(responseBid.getExt()).getPrebid().getEvents())
    +                .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents())
                     .containsOnly(events);
     
             verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
    @@ -1201,12 +1914,19 @@ public void shouldAddExtPrebidEventsIfEventsAreEnabledAndAccountSupportEventsFor
         @Test
         public void shouldAddExtPrebidEventsIfEventsAreEnabledAndDefaultAccountAnalyticsConfig() {
             // given
    -        final Account account = Account.builder().id("accountId").eventsEnabled(true).build();
    +        final Account account = Account.builder()
    +                .id("accountId")
    +                .auction(AccountAuctionConfig.builder()
    +                        .events(AccountEventsConfig.of(true))
    +                        .build())
    +                .analytics(AccountAnalyticsConfig.of(null, singletonMap("some-analytics", mapper.createObjectNode())))
    +                .build();
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
                     extBuilder -> extBuilder
                             .channel(ExtRequestPrebidChannel.of("amp"))
    -                        .integration("pbjs"));
    +                        .integration("pbjs"),
    +                givenImp());
             final AuctionContext auctionContext = givenAuctionContext(
                     bidRequest,
                     contextBuilder -> contextBuilder.account(account));
    @@ -1214,99 +1934,125 @@ public void shouldAddExtPrebidEventsIfEventsAreEnabledAndDefaultAccountAnalytics
             final Bid bid = Bid.builder()
                     .id("bidId1")
                     .price(BigDecimal.valueOf(5.67))
    -                .impid("i1")
    +                .impid(IMP_ID)
                     .build();
             final List bidderResponses = singletonList(
                     BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             final Events events = Events.of("http://event-type-win", "http://event-type-view");
    -        given(eventsService.createEvent(anyString(), anyString(), anyString(), anyLong(), anyString()))
    +        given(eventsService.createEvent(anyString(), anyString(), anyString(), any(), anyBoolean(), any()))
                     .willReturn(events);
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).hasSize(1)
                     .flatExtracting(SeatBid::getBid)
    -                .extracting(responseBid -> toExtPrebid(responseBid.getExt()).getPrebid().getEvents())
    +                .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents())
                     .containsOnly(events);
     
             verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
         }
     
    +    @SuppressWarnings("unchecked")
         @Test
         public void shouldAddExtPrebidVideo() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp()));
     
    -        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid("i1").impid("i1")
    -                .ext(mapper.createObjectNode().set("prebid", mapper.valueToTree(
    -                        ExtBidPrebid.builder().video(ExtBidPrebidVideo.of(1, "category")).build()))).build();
    +        final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().video(ExtBidPrebidVideo.of(1, "category")).build();
    +        final Bid bid = Bid.builder()
    +                .id("bidId1")
    +                .price(BigDecimal.valueOf(5.67))
    +                .impid(IMP_ID)
    +                .ext(mapper.createObjectNode().set("prebid", mapper.valueToTree(extBidPrebid)))
    +                .build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
    +        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
    +        givenCacheServiceResult(emptyList());
    +
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
    +        final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(cacheService).cacheBidsOpenrtb(bidsArgumentCaptor.capture(), any(), any(), any());
    +
    +        assertThat(bidsArgumentCaptor.getValue())
    +                .extracting(bidInfo -> toExtBidPrebid(bidInfo.getBid().getExt()).getVideo())
    +                .containsOnly(ExtBidPrebidVideo.of(1, "category"));
    +
             assertThat(bidResponse.getSeatbid()).hasSize(1)
                     .flatExtracting(SeatBid::getBid)
    -                .extracting(responseBid -> toExtPrebid(responseBid.getExt()).getPrebid().getVideo())
    +                .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getVideo())
                     .containsOnly(ExtBidPrebidVideo.of(1, "category"));
         }
     
         @Test
         public void shouldNotAddExtPrebidEventsIfEventsAreNotEnabled() {
             // given
    -        final Account account = Account.builder().id("accountId").eventsEnabled(false).build();
    +        final Account account = Account.builder()
    +                .id("accountId")
    +                .auction(AccountAuctionConfig.builder()
    +                        .events(AccountEventsConfig.of(false))
    +                        .build())
    +                .build();
             final AuctionContext auctionContext = givenAuctionContext(
                     givenBidRequest(
                             identity(),
    -                        extBuilder -> extBuilder.events(mapper.createObjectNode())),
    +                        extBuilder -> extBuilder.events(mapper.createObjectNode()),
    +                        givenImp()),
                     contextBuilder -> contextBuilder.account(account));
     
    -        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +        final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).hasSize(1)
                     .flatExtracting(SeatBid::getBid)
    -                .extracting(responseBid -> toExtPrebid(responseBid.getExt()).getPrebid().getEvents())
    +                .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents())
                     .containsNull();
         }
     
         @Test
         public void shouldNotAddExtPrebidEventsIfExtRequestPrebidEventsNull() {
             // given
    -        final Account account = Account.builder().id("accountId").eventsEnabled(true).build();
    +        final Account account = Account.builder()
    +                .id("accountId")
    +                .auction(AccountAuctionConfig.builder()
    +                        .events(AccountEventsConfig.of(true))
    +                        .build())
    +                .build();
             final AuctionContext auctionContext = givenAuctionContext(
    -                givenBidRequest(),
    +                givenBidRequest(givenImp()),
                     contextBuilder -> contextBuilder.account(account));
     
             final Bid bid = Bid.builder()
                     .id("bidId1")
                     .price(BigDecimal.valueOf(5.67))
    -                .impid("i1")
    +                .impid(IMP_ID)
                     .build();
             final List bidderResponses = singletonList(
                     BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).hasSize(1)
                     .flatExtracting(SeatBid::getBid)
    -                .extracting(responseBid -> toExtPrebid(responseBid.getExt()).getPrebid().getEvents())
    +                .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents())
                     .containsNull();
         }
     
    @@ -1315,12 +2061,15 @@ public void shouldNotAddExtPrebidEventsIfAccountDoesNotSupportEventsForChannel()
             // given
             final Account account = Account.builder()
                     .id("accountId")
    -                .eventsEnabled(true)
    -                .analyticsConfig(AccountAnalyticsConfig.of(singletonMap("web", true)))
    +                .auction(AccountAuctionConfig.builder()
    +                        .events(AccountEventsConfig.of(true))
    +                        .build())
    +                .analytics(AccountAnalyticsConfig.of(singletonMap("web", true), null))
                     .build();
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.channel(ExtRequestPrebidChannel.of("amp")));
    +                extBuilder -> extBuilder.channel(ExtRequestPrebidChannel.of("amp")),
    +                givenImp());
             final AuctionContext auctionContext = givenAuctionContext(
                     bidRequest,
                     contextBuilder -> contextBuilder.account(account));
    @@ -1328,19 +2077,19 @@ public void shouldNotAddExtPrebidEventsIfAccountDoesNotSupportEventsForChannel()
             final Bid bid = Bid.builder()
                     .id("bidId1")
                     .price(BigDecimal.valueOf(5.67))
    -                .impid("i1")
    +                .impid(IMP_ID)
                     .build();
             final List bidderResponses = singletonList(
                     BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).hasSize(1)
                     .flatExtracting(SeatBid::getBid)
    -                .extracting(responseBid -> toExtPrebid(responseBid.getExt()).getPrebid().getEvents())
    +                .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents())
                     .containsNull();
         }
     
    @@ -1349,9 +2098,10 @@ public void shouldReturnCacheEntityInExt() {
             // given
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(givenTargeting())));
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp()));
     
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
    @@ -1361,16 +2111,16 @@ public void shouldReturnCacheEntityInExt() {
                     .shouldCacheVideoBids(true)
                     .build();
     
    -        givenCacheServiceResult(singletonMap(bid, CacheIdInfo.of("cacheId", "videoId")));
    +        givenCacheServiceResult(singletonList(CacheInfo.of("cacheId", "videoId", null, null)));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid())
                     .flatExtracting(SeatBid::getBid)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getCache())
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getCache())
                     .extracting(ExtResponseCache::getBids, ExtResponseCache::getVastXml)
                     .containsExactly(tuple(
                             CacheAsset.of("uuid=cacheId", "cacheId"),
    @@ -1390,9 +2140,11 @@ public void shouldNotPopulateWinningBidTargetingIfIncludeWinnersFlagIsFalse() {
                                             BigDecimal.valueOf(0.5))))))
                             .includewinners(false)
                             .includebidderkeys(true)
    -                        .build())));
    +                        .includeformat(false)
    +                        .build()),
    +                givenImp()));
     
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
    @@ -1402,11 +2154,11 @@ public void shouldNotPopulateWinningBidTargetingIfIncludeWinnersFlagIsFalse() {
                     .shouldCacheVideoBids(true)
                     .build();
     
    -        givenCacheServiceResult(singletonMap(bid, CacheIdInfo.of("cacheId", "videoId")));
    +        givenCacheServiceResult(singletonList(CacheInfo.of("cacheId", "videoId", null, null)));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).flatExtracting(SeatBid::getBid)
    @@ -1431,9 +2183,11 @@ public void shouldNotPopulateBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse()
                                             BigDecimal.valueOf(0.5))))))
                             .includewinners(true)
                             .includebidderkeys(false)
    -                        .build())));
    +                        .includeformat(false)
    +                        .build()),
    +                givenImp()));
     
    -        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid("i1").build();
    +        final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
    @@ -1443,11 +2197,11 @@ public void shouldNotPopulateBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse()
                     .shouldCacheVideoBids(true)
                     .build();
     
    -        givenCacheServiceResult(singletonMap(bid, CacheIdInfo.of("cacheId", "videoId")));
    +        givenCacheServiceResult(singletonList(CacheInfo.of("cacheId", "videoId", null, null)));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).flatExtracting(SeatBid::getBid)
    @@ -1466,7 +2220,8 @@ public void shouldNotPopulateCacheIdTargetingKeywordsIfBidCpmIsZero() {
             // given
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(
                     identity(),
    -                extBuilder -> extBuilder.targeting(givenTargeting())));
    +                extBuilder -> extBuilder.targeting(givenTargeting()),
    +                givenImp("impId1"), givenImp("impId2")));
     
             final Bid firstBid = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ZERO).build();
             final Bid secondBid = Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.valueOf(5.67)).build();
    @@ -1477,11 +2232,11 @@ public void shouldNotPopulateCacheIdTargetingKeywordsIfBidCpmIsZero() {
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
     
    -        givenCacheServiceResult(singletonMap(secondBid, CacheIdInfo.of("cacheId2", null)));
    +        givenCacheServiceResult(singletonList(CacheInfo.of("cacheId2", null, null, null)));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getSeatbid()).flatExtracting(SeatBid::getBid).hasSize(2)
    @@ -1496,46 +2251,46 @@ public void shouldNotPopulateCacheIdTargetingKeywordsIfBidCpmIsZero() {
             verify(cacheService).cacheBidsOpenrtb(anyList(), any(), any(), any());
         }
     
    +    @SuppressWarnings("unchecked")
         @Test
         public void shouldNotCacheNonDealBidWithCpmIsZeroAndCacheDealBidWithZeroCpm() {
             // given
    -        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest());
    +        final Imp imp2 = givenImp("impId2");
    +        final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp("impId1"), imp2));
     
    -        final Bid firstBid = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ZERO).build();
    -        final Bid secondBid = Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.ZERO).dealid("dealId2")
    -                .build();
    +        final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ZERO).build();
    +        final Bid bid2 = Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.ZERO).dealid("dealId2").build();
     
             final List bidderResponses = asList(
    -                BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(firstBid, banner, null)), 99),
    -                BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(secondBid, banner, null)), 99));
    +                BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid1, banner, null)), 99),
    +                BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(bid2, banner, null)), 99));
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
    -        givenCacheServiceResult(singletonMap(secondBid, CacheIdInfo.of("cacheId2", null)));
    +        givenCacheServiceResult(singletonList(CacheInfo.of("cacheId2", null, null, null)));
     
             // when
    -        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result();
    +        bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
    -        @SuppressWarnings("unchecked") final ArgumentCaptor> cacheBidArgumentCaptor
    -                = ArgumentCaptor.forClass(List.class);
    -        verify(cacheService).cacheBidsOpenrtb(cacheBidArgumentCaptor.capture(), any(), any(), any());
    -        assertThat(cacheBidArgumentCaptor.getValue())
    -                .extracting(Bid::getId)
    +        final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(cacheService).cacheBidsOpenrtb(bidsArgumentCaptor.capture(), any(), any(), any());
    +
    +        assertThat(bidsArgumentCaptor.getValue()).extracting(bidInfo -> bidInfo.getBid().getId())
                     .containsOnly("bidId2");
         }
     
         @Test
    -    public void shouldPopulateBidResponseExtension() throws JsonProcessingException {
    +    public void shouldPopulateBidResponseExtension() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
                     .cur(singletonList("USD"))
                     .tmax(1000L)
                     .app(App.builder().build())
    -                .imp(emptyList())
    +                .imp(singletonList(givenImp()))
                     .build();
             final AuctionContext auctionContext = givenAuctionContext(bidRequest);
     
    -        final Bid bid = Bid.builder().id("bidId1").impid("impId1").adm("[]").price(BigDecimal.valueOf(5.67)).build();
    +        final Bid bid = Bid.builder().id("bidId1").impid(IMP_ID).adm("[]").price(BigDecimal.valueOf(5.67)).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     BidderSeatBid.of(singletonList(BidderBid.of(bid, xNative, null)), null,
                             singletonList(BidderError.badInput("bad_input"))), 100));
    @@ -1547,12 +2302,13 @@ public void shouldPopulateBidResponseExtension() throws JsonProcessingException
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
    -        final ExtBidResponse responseExt = mapper.treeToValue(bidResponse.getExt(), ExtBidResponse.class);
    +        final ExtBidResponse responseExt = bidResponse.getExt();
     
             assertThat(responseExt.getDebug()).isNull();
    +        assertThat(responseExt.getWarnings()).isNull();
             assertThat(responseExt.getUsersync()).isNull();
             assertThat(responseExt.getTmaxrequest()).isEqualTo(1000L);
     
    @@ -1562,7 +2318,7 @@ public void shouldPopulateBidResponseExtension() throws JsonProcessingException
                             ExtBidderError.of(3, "Failed to decode: Cannot deserialize instance of `com.iab."
                                     + "openrtb.response.Response` out of START_ARRAY token\n at [Source: (String)\"[]\"; "
                                     + "line: 1, column: 1]"))),
    -                entry("prebid", singletonList(ExtBidderError.of(999, "cacheError"))));
    +                entry("cache", singletonList(ExtBidderError.of(999, "cacheError"))));
     
             assertThat(responseExt.getResponsetimemillis()).hasSize(2)
                     .containsOnly(entry("bidder1", 100), entry("cache", 666));
    @@ -1573,7 +2329,7 @@ public void shouldPopulateBidResponseExtension() throws JsonProcessingException
         @Test
         public void impToStoredVideoJsonShouldTolerateWhenStoredVideoFetchIsFailed() {
             // given
    -        final Imp imp = Imp.builder().id("impId1").ext(
    +        final Imp imp = Imp.builder().id(IMP_ID).ext(
                     mapper.valueToTree(
                             ExtImp.of(
                                     ExtImpPrebid.builder()
    @@ -1585,23 +2341,24 @@ public void impToStoredVideoJsonShouldTolerateWhenStoredVideoFetchIsFailed() {
                     .build();
             final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(imp));
     
    -        final Bid bid = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build();
    +        final Bid bid = Bid.builder().id("bidId1").impid(IMP_ID).price(BigDecimal.valueOf(5.67)).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
    -        given(storedRequestProcessor.videoStoredDataResult(any(), any(), any())).willReturn(
    +        given(storedRequestProcessor.videoStoredDataResult(any(), anyList(), anyList(), any())).willReturn(
                     Future.failedFuture("Fetch failed"));
     
             // when
             final Future result =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false);
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS);
     
             // then
    -        verify(storedRequestProcessor).videoStoredDataResult(eq(singletonList(imp)), any(), eq(timeout));
    +        verify(storedRequestProcessor).videoStoredDataResult(any(), eq(singletonList(imp)), anyList(), eq(timeout));
     
             assertThat(result.succeeded()).isTrue();
         }
     
    +    @SuppressWarnings("unchecked")
         @Test
         public void impToStoredVideoJsonShouldInjectStoredVideoWhenExtOptionsIsTrueAndVideoNotEmpty() {
             // given
    @@ -1632,7 +2389,7 @@ public void impToStoredVideoJsonShouldInjectStoredVideoWhenExtOptionsIsTrueAndVi
             final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build();
             final Bid bid2 = Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.valueOf(2)).build();
             final Bid bid3 = Bid.builder().id("bidId3").impid("impId3").price(BigDecimal.valueOf(3)).build();
    -        final List bidderBids = Arrays.asList(
    +        final List bidderBids = mutableList(
                     BidderBid.of(bid1, banner, "USD"),
                     BidderBid.of(bid2, banner, "USD"),
                     BidderBid.of(bid3, banner, "USD"));
    @@ -1640,27 +2397,37 @@ public void impToStoredVideoJsonShouldInjectStoredVideoWhenExtOptionsIsTrueAndVi
                     BidderResponse.of("bidder1", BidderSeatBid.of(bidderBids, emptyList(), emptyList()), 100));
     
             final Video storedVideo = Video.builder().maxduration(100).h(2).w(2).build();
    -        given(storedRequestProcessor.videoStoredDataResult(any(), any(), any()))
    +        given(storedRequestProcessor.videoStoredDataResult(any(), anyList(), anyList(), any()))
                     .willReturn(Future.succeededFuture(
                             VideoStoredDataResult.of(singletonMap("impId1", storedVideo), emptyList())));
    +        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
    +        givenCacheServiceResult(emptyList());
     
             // when
             final Future result =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false);
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS);
     
             // then
    -        verify(storedRequestProcessor).videoStoredDataResult(eq(Arrays.asList(imp1, imp3)), any(), eq(timeout));
    +        verify(storedRequestProcessor).videoStoredDataResult(any(), eq(asList(imp1, imp3)), anyList(),
    +                eq(timeout));
    +
    +        final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(cacheService).cacheBidsOpenrtb(bidsArgumentCaptor.capture(), any(), any(), any());
    +
    +        assertThat(bidsArgumentCaptor.getValue())
    +                .extracting(bidInfo -> toExtBidPrebid(bidInfo.getBid().getExt()).getStoredRequestAttributes())
    +                .containsOnly(storedVideo, null, null);
     
             assertThat(result.result().getSeatbid())
                     .flatExtracting(SeatBid::getBid).hasSize(3)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getStoredRequestAttributes())
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getStoredRequestAttributes())
                     .containsOnly(storedVideo, null, null);
         }
     
         @Test
         public void impToStoredVideoJsonShouldAddErrorsWithPrebidBidderWhenStoredVideoRequestFailed() {
             // given
    -        final Imp imp1 = Imp.builder().id("impId1").ext(
    +        final Imp imp1 = Imp.builder().id(IMP_ID).ext(
                     mapper.valueToTree(
                             ExtImp.of(ExtImpPrebid.builder()
                                             .storedrequest(ExtStoredRequest.of("st1"))
    @@ -1671,27 +2438,29 @@ public void impToStoredVideoJsonShouldAddErrorsWithPrebidBidderWhenStoredVideoRe
             final BidRequest bidRequest = givenBidRequest(imp1);
             final AuctionContext auctionContext = givenAuctionContext(bidRequest);
     
    -        final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build();
    -        final List bidderBids = singletonList(
    -                BidderBid.of(bid1, banner, "USD"));
    +        final Bid bid1 = Bid.builder().id("bidId1").impid(IMP_ID).price(BigDecimal.valueOf(5.67)).build();
    +        final List bidderBids = singletonList(BidderBid.of(bid1, banner, "USD"));
             final List bidderResponses = singletonList(
                     BidderResponse.of("bidder1", BidderSeatBid.of(bidderBids, emptyList(), emptyList()), 100));
     
    -        given(storedRequestProcessor.videoStoredDataResult(any(), any(), any()))
    +        given(storedRequestProcessor.videoStoredDataResult(any(), anyList(), anyList(), any()))
                     .willReturn(Future.failedFuture("Bad timeout"));
     
             // when
             final Future result =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false);
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS);
     
             // then
    -        verify(storedRequestProcessor).videoStoredDataResult(eq(singletonList(imp1)), any(), eq(timeout));
    +        verify(storedRequestProcessor).videoStoredDataResult(any(), eq(singletonList(imp1)), anyList(), eq(timeout));
     
             assertThat(result.result().getExt()).isEqualTo(
    -                mapper.valueToTree(ExtBidResponse.of(null, singletonMap(
    -                        "prebid", singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(),
    -                                "Bad timeout"))), singletonMap("bidder1", 100), 1000L, null,
    -                        ExtBidResponsePrebid.of(1000L))));
    +                ExtBidResponse.builder()
    +                        .errors(singletonMap("prebid",
    +                                singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(), "Bad timeout"))))
    +                        .responsetimemillis(singletonMap("bidder1", 100))
    +                        .tmaxrequest(1000L)
    +                        .prebid(ExtBidResponsePrebid.of(1000L, null))
    +                        .build());
         }
     
         @Test
    @@ -1712,14 +2481,18 @@ public void shouldProcessRequestAndAddErrorAboutDeprecatedBidder() {
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS).result();
     
             // then
             assertThat(bidResponse.getExt()).isEqualTo(
    -                mapper.valueToTree(ExtBidResponse.of(null, singletonMap(
    -                        invalidBidderName, singletonList(ExtBidderError.of(BidderError.Type.bad_input.getCode(),
    -                                "invalid has been deprecated and is no longer available. Use valid instead."))),
    -                        singletonMap("bidder1", 100), 1000L, null, ExtBidResponsePrebid.of(1000L))));
    +                ExtBidResponse.builder()
    +                        .errors(singletonMap(invalidBidderName,
    +                                singletonList(ExtBidderError.of(BidderError.Type.bad_input.getCode(),
    +                                        "invalid has been deprecated and is no longer available. Use valid instead."))))
    +                        .responsetimemillis(singletonMap("bidder1", 100))
    +                        .tmaxrequest(1000L)
    +                        .prebid(ExtBidResponsePrebid.of(1000L, null))
    +                        .build());
     
             verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any());
         }
    @@ -1728,68 +2501,100 @@ invalidBidderName, singletonList(ExtBidderError.of(BidderError.Type.bad_input.ge
         public void shouldProcessRequestAndAddErrorFromAuctionContext() {
             // given
             final AuctionContext auctionContext = givenAuctionContext(
    -                givenBidRequest(),
    +                givenBidRequest(givenImp()),
                     contextBuilder -> contextBuilder.prebidErrors(singletonList("privacy error")));
     
    -        final Bid bid1 = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build();
    -        final List bidderBids = singletonList(
    -                BidderBid.of(bid1, banner, "USD"));
    +        final Bid bid1 = Bid.builder().id("bidId1").impid(IMP_ID).price(BigDecimal.valueOf(5.67)).build();
    +        final List bidderBids = singletonList(BidderBid.of(bid1, banner, "USD"));
             final List bidderResponses = singletonList(
                     BidderResponse.of("bidder1", BidderSeatBid.of(bidderBids, emptyList(), emptyList()), 100));
     
             // when
             final Future result =
    -                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false);
    +                bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS);
     
             // then
             assertThat(result.result().getExt()).isEqualTo(
    -                mapper.valueToTree(ExtBidResponse.of(null, singletonMap(
    -                        "prebid", singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(),
    -                                "privacy error"))), singletonMap("bidder1", 100), 1000L, null,
    -                        ExtBidResponsePrebid.of(1000L))));
    +                ExtBidResponse.builder()
    +                        .errors(singletonMap(
    +                                "prebid",
    +                                singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(), "privacy error"))))
    +                        .responsetimemillis(singletonMap("bidder1", 100))
    +                        .tmaxrequest(1000L)
    +                        .prebid(ExtBidResponsePrebid.of(1000L, null))
    +                        .build());
         }
     
         @Test
    -    public void shouldPopulateBidResponseDebugExtensionIfDebugIsEnabled() throws JsonProcessingException {
    +    public void shouldPopulateResponseDebugExtensionAndWarningsIfDebugIsEnabled() {
             // given
    -        final BidRequest bidRequest = givenBidRequest();
    -        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
    +        final BidRequest bidRequest = givenBidRequest(givenImp());
    +        final List warnings = asList("warning1", "warning2");
    +        final AuctionContext auctionContext = givenAuctionContext(
    +                bidRequest,
    +                builder -> builder
    +                        .debugWarnings(warnings)
    +                        .debugContext(DebugContext.of(true, null)));
             givenCacheServiceResult(CacheServiceResult.of(
    -                DebugHttpCall.builder().endpoint("http://cache-service/cache")
    -                        .requestUri("test.uri").responseStatus(500).build(), null, emptyMap()));
    +                DebugHttpCall.builder()
    +                        .endpoint("http://cache-service/cache")
    +                        .requestUri("test.uri")
    +                        .responseStatus(500)
    +                        .build(),
    +                null,
    +                emptyMap()));
    +        auctionContext.getDebugHttpCalls().put("userservice", singletonList(
    +                DebugHttpCall.builder()
    +                        .requestUri("userservice.uri")
    +                        .responseStatus(500)
    +                        .responseTimeMillis(200)
    +                        .build()));
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
     
    -        final Bid bid = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build();
    +        final Bid bid = Bid.builder().id("bidId1").impid(IMP_ID).price(BigDecimal.valueOf(5.67)).build();
             final List bidderResponses = singletonList(BidderResponse.of("bidder1",
                     BidderSeatBid.of(singletonList(BidderBid.of(bid, banner, null)),
                             singletonList(ExtHttpCall.builder().status(200).build()), null), 100));
     
             // when
             final BidResponse bidResponse =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, true).result();
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS).result();
     
             // then
    -        final ExtBidResponse responseExt = mapper.treeToValue(bidResponse.getExt(), ExtBidResponse.class);
    +        final ExtBidResponse responseExt = bidResponse.getExt();
     
             assertThat(responseExt.getDebug()).isNotNull();
    -        assertThat(responseExt.getDebug().getHttpcalls()).hasSize(2)
    +        assertThat(responseExt.getDebug().getHttpcalls()).hasSize(3)
                     .containsOnly(
                             entry("bidder1", singletonList(ExtHttpCall.builder().status(200).build())),
    -                        entry("cache", singletonList(ExtHttpCall.builder().uri("test.uri").status(500).build())));
    +                        entry("cache", singletonList(ExtHttpCall.builder().uri("test.uri").status(500).build())),
    +                        entry("userservice", singletonList(ExtHttpCall.builder().uri("userservice.uri").status(500)
    +                                .build())));
     
             assertThat(responseExt.getDebug().getResolvedrequest()).isEqualTo(bidRequest);
     
    +        assertThat(responseExt.getWarnings())
    +                .containsOnly(
    +                        entry("prebid", Arrays.asList(
    +                                ExtBidderError.of(BidderError.Type.generic.getCode(), "warning1"),
    +                                ExtBidderError.of(BidderError.Type.generic.getCode(), "warning2"))));
    +
             verify(cacheService).cacheBidsOpenrtb(anyList(), any(), any(), any());
         }
     
         @Test
         public void shouldPassIntegrationToCacheServiceAndBidEvents() {
             // given
    -        final Account account = Account.builder().id("accountId").eventsEnabled(true).build();
    +        final Account account = Account.builder()
    +                .id("accountId")
    +                .auction(AccountAuctionConfig.builder()
    +                        .events(AccountEventsConfig.of(true))
    +                        .build())
    +                .build();
             final BidRequest bidRequest = BidRequest.builder()
                     .cur(singletonList("USD"))
    -                .imp(emptyList())
    +                .imp(singletonList(givenImp()))
                     .ext(ExtRequest.of(ExtRequestPrebid.builder()
                             .events(mapper.createObjectNode())
                             .integration("integration")
    @@ -1799,22 +2604,22 @@ public void shouldPassIntegrationToCacheServiceAndBidEvents() {
                     bidRequest,
                     contextBuilder -> contextBuilder.account(account));
     
    -        final Bid bid = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build();
    +        final Bid bid = Bid.builder().id("bidId1").impid(IMP_ID).price(BigDecimal.valueOf(5.67)).build();
             final List bidderResponses = singletonList(
                     BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "USD")), 100));
     
             final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build();
     
    -        givenCacheServiceResult(singletonMap(bid, CacheIdInfo.of(null, null)));
    +        givenCacheServiceResult(singletonList(CacheInfo.empty()));
     
    -        given(eventsService.createEvent(anyString(), anyString(), anyString(), anyLong(), anyString()))
    +        given(eventsService.createEvent(anyString(), anyString(), anyString(), any(), anyBoolean(), any()))
                     .willReturn(Events.of(
                             "http://win-url?param=value&int=integration",
                             "http://imp-url?param=value&int=integration"));
     
             // when
             final Future result =
    -                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false);
    +                bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS);
     
             // then
             verify(cacheService).cacheBidsOpenrtb(anyList(), any(), any(),
    @@ -1822,8 +2627,9 @@ public void shouldPassIntegrationToCacheServiceAndBidEvents() {
     
             assertThat(result.result().getSeatbid())
                     .flatExtracting(SeatBid::getBid).hasSize(1)
    -                .extracting(extractedBid -> toExtPrebid(extractedBid.getExt()).getPrebid().getEvents())
    -                .containsOnly(Events.of("http://win-url?param=value&int=integration",
    +                .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getEvents())
    +                .containsOnly(Events.of(
    +                        "http://win-url?param=value&int=integration",
                             "http://imp-url?param=value&int=integration"));
         }
     
    @@ -1833,26 +2639,84 @@ private AuctionContext givenAuctionContext(BidRequest bidRequest,
             final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder()
                     .account(Account.empty("accountId"))
                     .bidRequest(bidRequest)
    +                .txnLog(TxnLog.create())
                     .timeout(timeout)
    +                .debugContext(DebugContext.empty())
    +                .deepDebugLog(DeepDebugLog.create(false, clock))
    +                .debugHttpCalls(new HashMap<>())
    +                .debugWarnings(emptyList())
                     .prebidErrors(emptyList());
     
    -        return contextCustomizer.apply(auctionContextBuilder)
    -                .build();
    +        return contextCustomizer.apply(auctionContextBuilder).build();
         }
     
         private AuctionContext givenAuctionContext(BidRequest bidRequest) {
             return givenAuctionContext(bidRequest, identity());
         }
     
    -    private void givenCacheServiceResult(Map cacheBids) {
    -        givenCacheServiceResult(CacheServiceResult.of(null, null, cacheBids));
    +    private void givenCacheServiceResult(List cacheInfos) {
    +        given(cacheService.cacheBidsOpenrtb(anyList(), any(), any(), any()))
    +                .willAnswer(invocation -> Future.succeededFuture(CacheServiceResult.of(
    +                        null,
    +                        null,
    +                        zipBidsWithCacheInfos(invocation.getArgument(0), cacheInfos))));
         }
     
         private void givenCacheServiceResult(CacheServiceResult cacheServiceResult) {
    -        given(cacheService.cacheBidsOpenrtb(any(), any(), any(), any()))
    +        given(cacheService.cacheBidsOpenrtb(anyList(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(cacheServiceResult));
         }
     
    +    private static Map zipBidsWithCacheInfos(List bidInfos, List cacheInfos) {
    +        return IntStream.range(0, Math.min(bidInfos.size(), cacheInfos.size()))
    +                .boxed()
    +                .collect(Collectors.toMap(i -> bidInfos.get(i).getBid(), cacheInfos::get));
    +    }
    +
    +    private static BidInfo toBidInfo(Bid bid,
    +                                     Imp correspondingImp,
    +                                     String bidder,
    +                                     BidType bidType,
    +                                     boolean isWinningBid) {
    +
    +        return BidInfo.builder()
    +                .bid(bid)
    +                .correspondingImp(correspondingImp)
    +                .bidder(bidder)
    +                .bidType(bidType)
    +                .targetingInfo(TargetingInfo.builder()
    +                        .bidderCode(bidder)
    +                        .isTargetingEnabled(true)
    +                        .isWinningBid(isWinningBid)
    +                        .isBidderWinningBid(true)
    +                        .isAddTargetBidderCode(false)
    +                        .build())
    +                .build();
    +    }
    +
    +    private static Imp givenImp() {
    +        return givenImp(IMP_ID);
    +    }
    +
    +    private static Imp givenImp(String impId) {
    +        return Imp.builder().id(impId).build();
    +    }
    +
    +    private static Imp givenImp(Map dealIdToLineItemId) {
    +        Pmp pmp = null;
    +        if (MapUtils.isNotEmpty(dealIdToLineItemId)) {
    +            final List deals = dealIdToLineItemId.entrySet().stream()
    +                    .map(dealIdAndLineId -> Deal.builder()
    +                            .id(dealIdAndLineId.getKey())
    +                            .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of(
    +                                    dealIdAndLineId.getValue(), null, null, null)))).build())
    +                    .collect(Collectors.toList());
    +            pmp = Pmp.builder().deals(deals).build();
    +        }
    +
    +        return Imp.builder().id(IMP_ID).pmp(pmp).build();
    +    }
    +
         private static BidRequest givenBidRequest(
                 UnaryOperator bidRequestCustomizer,
                 UnaryOperator extRequestCustomizer,
    @@ -1882,7 +2746,7 @@ private static BidRequest givenBidRequest(Imp... imps) {
         }
     
         private static BidderSeatBid givenSeatBid(BidderBid... bids) {
    -        return BidderSeatBid.of(new ArrayList<>(asList(bids)), emptyList(), emptyList());
    +        return BidderSeatBid.of(mutableList(bids), emptyList(), emptyList());
         }
     
         private static ExtRequestTargeting givenTargeting() {
    @@ -1892,20 +2756,48 @@ private static ExtRequestTargeting givenTargeting() {
                                     BigDecimal.valueOf(0.5))))))
                     .includewinners(true)
                     .includebidderkeys(true)
    +                .includeformat(false)
                     .build();
         }
     
    -    private static ExtPrebid toExtPrebid(ObjectNode ext) {
    +    private static ExtBidPrebid toExtBidPrebid(ObjectNode ext) {
             try {
    -            return mapper.readValue(mapper.treeAsTokens(ext), new TypeReference>() {
    -            });
    -        } catch (IOException e) {
    +            return mapper.treeToValue(ext.get("prebid"), ExtBidPrebid.class);
    +        } catch (JsonProcessingException e) {
                 return rethrow(e);
             }
         }
     
         private static String toTargetingByKey(Bid bid, String targetingKey) {
    -        final Map targeting = toExtPrebid(bid.getExt()).getPrebid().getTargeting();
    +        final Map targeting = toExtBidPrebid(bid.getExt()).getTargeting();
             return targeting != null ? targeting.get(targetingKey) : null;
         }
    +
    +    private static String getTargetingBidderCode(Bid bid) {
    +        return toExtBidPrebid(bid.getExt()).getTargetBidderCode();
    +    }
    +
    +    private static ObjectNode extWithTargeting(String targetBidderCode, Map targeting) {
    +        final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder()
    +                .type(banner)
    +                .targeting(targeting)
    +                .targetBidderCode(targetBidderCode)
    +                .build();
    +
    +        final ObjectNode ext = mapper.createObjectNode();
    +        ext.set("prebid", mapper.valueToTree(extBidPrebid));
    +        return ext;
    +    }
    +
    +    @SafeVarargs
    +    private static  List mutableList(T... values) {
    +        return Arrays.stream(values).collect(Collectors.toList());
    +    }
    +
    +    @Accessors(fluent = true)
    +    @Value(staticConstructor = "of")
    +    private static class BidderResponsePayloadImpl implements BidderResponsePayload {
    +
    +        List bids;
    +    }
     }
    diff --git a/src/test/java/org/prebid/server/auction/BidderAliasesTest.java b/src/test/java/org/prebid/server/auction/BidderAliasesTest.java
    index 8a56d41945a..7461fff4fe6 100644
    --- a/src/test/java/org/prebid/server/auction/BidderAliasesTest.java
    +++ b/src/test/java/org/prebid/server/auction/BidderAliasesTest.java
    @@ -1,6 +1,5 @@
     package org.prebid.server.auction;
     
    -import org.junit.Before;
     import org.junit.Rule;
     import org.junit.Test;
     import org.mockito.Mock;
    @@ -10,11 +9,6 @@
     
     import static java.util.Collections.singletonMap;
     import static org.assertj.core.api.Assertions.assertThat;
    -import static org.mockito.ArgumentMatchers.any;
    -import static org.mockito.ArgumentMatchers.anyString;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -import static org.mockito.Mockito.verifyZeroInteractions;
     
     public class BidderAliasesTest {
     
    @@ -24,31 +18,22 @@ public class BidderAliasesTest {
         @Mock
         private BidderCatalog bidderCatalog;
     
    -    @Before
    -    public void setUp() {
    -        given(bidderCatalog.isActive(anyString())).willReturn(true);
    -    }
    -
         @Test
    -    public void isAliasDefinedShouldQueryCatalogWhenNoAliasesInRequest() {
    +    public void isAliasDefinedShouldReturnFalseWhenNoAliasesInRequest() {
             // given
    -        given(bidderCatalog.isAlias(anyString())).willReturn(true);
    -
    -        final BidderAliases aliases = BidderAliases.of(bidderCatalog);
    +        final BidderAliases aliases = BidderAliases.of(null, null, bidderCatalog);
     
             // when and then
    -        assertThat(aliases.isAliasDefined("alias")).isTrue();
    +        assertThat(aliases.isAliasDefined("alias")).isFalse();
         }
     
         @Test
    -    public void isAliasDefinedShouldQueryCatalogWhenAliasIsNotDefinedInRequest() {
    +    public void isAliasDefinedShouldReturnFalseWhenAliasIsNotDefinedInRequest() {
             // given
    -        given(bidderCatalog.isAlias(anyString())).willReturn(true);
    -
             final BidderAliases aliases = BidderAliases.of(singletonMap("anotherAlias", "bidder"), null, bidderCatalog);
     
             // when and then
    -        assertThat(aliases.isAliasDefined("alias")).isTrue();
    +        assertThat(aliases.isAliasDefined("alias")).isFalse();
         }
     
         @Test
    @@ -58,53 +43,24 @@ public void isAliasDefinedShouldDetectAliasInRequest() {
     
             // when and then
             assertThat(aliases.isAliasDefined("alias")).isTrue();
    -
    -        verifyZeroInteractions(bidderCatalog);
         }
     
         @Test
    -    public void resolveBidderShouldReturnInputWhenNoAliasesInRequestAndInCatalog() {
    +    public void resolveBidderShouldReturnInputWhenNoAliasesInRequest() {
             // given
    -        given(bidderCatalog.nameByAlias(anyString())).willReturn(null);
    -
    -        final BidderAliases aliases = BidderAliases.of(bidderCatalog);
    +        final BidderAliases aliases = BidderAliases.of(null, null, bidderCatalog);
     
             // when and then
             assertThat(aliases.resolveBidder("alias")).isEqualTo("alias");
         }
     
         @Test
    -    public void resolveBidderShouldReturnInputWhenNoAliasesInRequestAndInactiveInCatalog() {
    +    public void resolveBidderShouldReturnInputWhenAliasIsNotDefinedInRequest() {
             // given
    -        given(bidderCatalog.nameByAlias(anyString())).willReturn("bidder");
    -        given(bidderCatalog.isActive(any())).willReturn(false);
    -
    -        final BidderAliases aliases = BidderAliases.of(bidderCatalog);
    -
    -        // when and then
    -        assertThat(aliases.resolveBidder("alias")).isEqualTo("alias");
    -    }
    -
    -    @Test
    -    public void resolveBidderShouldQueryCatalogWhenNoAliasesInRequest() {
    -        // given
    -        given(bidderCatalog.nameByAlias(anyString())).willReturn("bidder");
    -
    -        final BidderAliases aliases = BidderAliases.of(bidderCatalog);
    -
    -        // when and then
    -        assertThat(aliases.resolveBidder("alias")).isEqualTo("bidder");
    -    }
    -
    -    @Test
    -    public void resolveBidderShouldQueryCatalogWhenAliasIsNotDefinedInRequest() {
    -        // given
    -        given(bidderCatalog.nameByAlias(eq("alias"))).willReturn("bidder");
    -
             final BidderAliases aliases = BidderAliases.of(singletonMap("anotherAlias", "bidder"), null, bidderCatalog);
     
             // when and then
    -        assertThat(aliases.resolveBidder("alias")).isEqualTo("bidder");
    +        assertThat(aliases.resolveBidder("alias")).isEqualTo("alias");
         }
     
         @Test
    @@ -114,54 +70,24 @@ public void resolveBidderShouldDetectAliasInRequest() {
     
             // when and then
             assertThat(aliases.resolveBidder("alias")).isEqualTo("bidder");
    -
    -        verifyZeroInteractions(bidderCatalog);
    -    }
    -
    -    @Test
    -    public void resolveAliasVendorIdShouldReturnNullWhenNoVendorIdsInRequestAndInCatalog() {
    -        // given
    -        given(bidderCatalog.vendorIdByName(anyString())).willReturn(null);
    -
    -        final BidderAliases aliases = BidderAliases.of(bidderCatalog);
    -
    -        // when and then
    -        assertThat(aliases.resolveAliasVendorId("alias")).isNull();
         }
     
         @Test
    -    public void resolveAliasVendorIdShouldReturnNullWhenNoVendorIdsInRequestAndInactiveInCatalog() {
    +    public void resolveAliasVendorIdShouldReturnNullWhenNoVendorIdsInRequest() {
             // given
    -        given(bidderCatalog.vendorIdByName(anyString())).willReturn(1);
    -        given(bidderCatalog.isActive(anyString())).willReturn(false);
    -
    -        final BidderAliases aliases = BidderAliases.of(bidderCatalog);
    +        final BidderAliases aliases = BidderAliases.of(null, null, bidderCatalog);
     
             // when and then
             assertThat(aliases.resolveAliasVendorId("alias")).isNull();
         }
     
         @Test
    -    public void resolveAliasVendorIdShouldQueryCatalogWhenNoVendorIdsInRequest() {
    +    public void resolveAliasVendorIdShouldReturnNullWhenVendorIdIsNotDefinedInRequest() {
             // given
    -        given(bidderCatalog.vendorIdByName(anyString())).willReturn(1);
    -        given(bidderCatalog.isActive(anyString())).willReturn(true);
    -
    -        final BidderAliases aliases = BidderAliases.of(bidderCatalog);
    -
    -        // when and then
    -        assertThat(aliases.resolveAliasVendorId("alias")).isEqualTo(1);
    -    }
    -
    -    @Test
    -    public void resolveAliasVendorIdShouldQueryCatalogWhenVendorIdIsNotDefinedInRequest() {
    -        // given
    -        given(bidderCatalog.vendorIdByName(eq("alias"))).willReturn(1);
    -
             final BidderAliases aliases = BidderAliases.of(null, singletonMap("anotherAlias", 2), bidderCatalog);
     
             // when and then
    -        assertThat(aliases.resolveAliasVendorId("alias")).isEqualTo(1);
    +        assertThat(aliases.resolveAliasVendorId("alias")).isNull();
         }
     
         @Test
    @@ -171,19 +97,5 @@ public void resolveAliasVendorIdShouldDetectVendorIdInRequest() {
     
             // when and then
             assertThat(aliases.resolveAliasVendorId("alias")).isEqualTo(2);
    -
    -        verifyZeroInteractions(bidderCatalog);
    -    }
    -
    -    @Test
    -    public void resolveAliasVendorIdShouldResolveAliasWhenQueryingCatalog() {
    -        // given
    -        given(bidderCatalog.nameByAlias(eq("alias"))).willReturn("bidder");
    -        given(bidderCatalog.vendorIdByName(eq("bidder"))).willReturn(1);
    -
    -        final BidderAliases aliases = BidderAliases.of(bidderCatalog);
    -
    -        // when and then
    -        assertThat(aliases.resolveAliasVendorId("alias")).isEqualTo(1);
         }
     }
    diff --git a/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java b/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java
    index 2fd4e688f21..81297b77f95 100644
    --- a/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java
    +++ b/src/test/java/org/prebid/server/auction/CurrencyConversionServiceTest.java
    @@ -1,6 +1,7 @@
     package org.prebid.server.auction;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.BidRequest;
     import io.vertx.core.Future;
     import io.vertx.core.Handler;
     import io.vertx.core.Vertx;
    @@ -15,13 +16,21 @@
     import org.prebid.server.currency.CurrencyConversionService;
     import org.prebid.server.currency.proto.CurrencyConversionRates;
     import org.prebid.server.exception.PreBidException;
    +import org.prebid.server.metric.Metrics;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
     import org.prebid.server.spring.config.model.ExternalConversionProperties;
     import org.prebid.server.vertx.http.HttpClient;
     import org.prebid.server.vertx.http.model.HttpClientResponse;
     
     import java.math.BigDecimal;
    +import java.time.Clock;
    +import java.time.Instant;
    +import java.time.ZoneOffset;
     import java.util.HashMap;
     import java.util.Map;
    +import java.util.function.BooleanSupplier;
     
     import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonMap;
    @@ -53,6 +62,9 @@ public class CurrencyConversionServiceTest extends VertxTest {
         private HttpClient httpClient;
         @Mock
         private Vertx vertx;
    +    @Mock
    +    private Metrics metrics;
    +    private final Clock clock = Clock.fixed(Instant.now(), ZoneOffset.UTC);
     
         private CurrencyConversionService currencyService;
     
    @@ -64,13 +76,13 @@ public void setUp() throws JsonProcessingException {
             givenHttpClientReturnsResponse(httpClient, 200,
                     mapper.writeValueAsString(CurrencyConversionRates.of(null, currencyRates)));
     
    -        currencyService = setExternalResource(URL, 1L, vertx, httpClient);
    +        currencyService = createInitializedService(URL, 1L, -3600L, httpClient);
         }
     
         @Test
         public void creationShouldFailOnInvalidCurrencyServerUrl() {
             assertThatIllegalArgumentException()
    -                .isThrownBy(() -> setExternalResource("invalid-url", 1L, vertx, httpClient))
    +                .isThrownBy(() -> createInitializedService("invalid-url", 1L, -1L, httpClient))
                     .withMessage("URL supplied is not valid: invalid-url");
         }
     
    @@ -79,13 +91,38 @@ public void initializeShouldSetLastUpdatedDate() {
             assertThat(currencyService.getLastUpdated()).isNotNull();
         }
     
    +    @Test
    +    public void currencyRatesGaugeShouldReportStale() {
    +        // then
    +        final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class);
    +        verify(metrics).createCurrencyRatesGauge(gaugeValueProviderCaptor.capture());
    +        final BooleanSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue();
    +
    +        assertThat(gaugeValueProvider.getAsBoolean()).isTrue();
    +    }
    +
    +    @Test
    +    public void currencyRatesGaugeShouldReportNotStale() {
    +        // when
    +        metrics = mock(Metrics.class); // original mock is already spoiled by service initialization in setUp
    +        currencyService = createInitializedService(URL, 1L, 3600L, httpClient);
    +
    +        // then
    +        final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class);
    +        verify(metrics).createCurrencyRatesGauge(gaugeValueProviderCaptor.capture());
    +        final BooleanSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue();
    +
    +        assertThat(gaugeValueProvider.getAsBoolean()).isFalse();
    +    }
    +
         @Test
         public void convertCurrencyShouldReturnSamePriceIfBidAndServerCurrenciesEquals() {
             // given
             final BigDecimal price = BigDecimal.valueOf(100);
     
             // when
    -        final BigDecimal convertedPrice = currencyService.convertCurrency(price, null, USD, USD, false);
    +        final BigDecimal convertedPrice = currencyService.convertCurrency(price,
    +                givenBidRequestWithCurrencies(null, false), USD, USD);
     
             // then
             assertThat(convertedPrice).isSameAs(price);
    @@ -98,8 +135,8 @@ public void convertCurrencyShouldUseUSDByDefaultIfBidCurrencyIsNull() {
                     singletonMap(GBP, singletonMap(USD, BigDecimal.valueOf(1.4306)));
     
             // when
    -        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, GBP, null,
    -                false);
    +        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE,
    +                givenBidRequestWithCurrencies(requestConversionRates, false), GBP, null);
     
             // then
             assertThat(price.compareTo(BigDecimal.valueOf(0.699))).isEqualTo(0);
    @@ -112,8 +149,8 @@ public void convertCurrencyShouldReturnConvertedByStraightMultiplierPrice() {
                     singletonMap(EUR, BigDecimal.valueOf(1.1565)));
     
             // when
    -        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, GBP, EUR,
    -                false);
    +        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE,
    +                givenBidRequestWithCurrencies(requestConversionRates, false), GBP, EUR);
     
             // then
             assertThat(price.compareTo(BigDecimal.valueOf(0.865))).isEqualTo(0);
    @@ -126,8 +163,8 @@ public void convertCurrencyShouldReturnConvertedByInvertedMultiplierPrice() {
                     BigDecimal.valueOf(1.1565)));
     
             // when
    -        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, GBP,
    -                false);
    +        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE,
    +                givenBidRequestWithCurrencies(requestConversionRates, false), EUR, GBP);
     
             // then
             assertThat(price.compareTo(BigDecimal.valueOf(1.156))).isEqualTo(0);
    @@ -141,8 +178,8 @@ public void convertCurrencyShouldReturnConvertedByIntermediateMultiplierPrice()
             requestConversionRates.put(EUR, singletonMap(USD, BigDecimal.valueOf(1.2304)));
     
             // when
    -        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, GBP,
    -                false);
    +        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE,
    +                givenBidRequestWithCurrencies(requestConversionRates, false), EUR, GBP);
     
             // then
             assertThat(price.compareTo(BigDecimal.valueOf(1.163))).isEqualTo(0);
    @@ -155,8 +192,8 @@ public void convertCurrencyShouldReturnConvertedBySingleDigitMultiplierPrice() {
             requestConversionRates.put(EUR, singletonMap(USD, BigDecimal.valueOf(0.5)));
     
             // when
    -        final BigDecimal price = currencyService.convertCurrency(new BigDecimal("1.23"), requestConversionRates, EUR,
    -                USD, false);
    +        final BigDecimal price = currencyService.convertCurrency(new BigDecimal("1.23"),
    +                givenBidRequestWithCurrencies(requestConversionRates, false), EUR, USD);
     
             // then
             assertThat(price.compareTo(BigDecimal.valueOf(2.460))).isEqualTo(0);
    @@ -165,7 +202,8 @@ public void convertCurrencyShouldReturnConvertedBySingleDigitMultiplierPrice() {
         @Test
         public void convertCurrencyShouldUseLatestRatesIfRequestRatesIsNull() {
             // when
    -        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, null, EUR, GBP, false);
    +        final BigDecimal price = currencyService.convertCurrency(
    +                BigDecimal.ONE, givenBidRequestWithCurrencies(null, false), EUR, GBP);
     
             // then
             assertThat(price.compareTo(BigDecimal.valueOf(1.149))).isEqualTo(0);
    @@ -178,8 +216,8 @@ public void convertCurrencyShouldUseConversionRateFromServerIfusepbsratesIsTrue(
             requestConversionRates.put(EUR, singletonMap(USD, BigDecimal.valueOf(0.6)));
     
             // when
    -        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, GBP,
    -                true);
    +        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE,
    +                givenBidRequestWithCurrencies(requestConversionRates, true), EUR, GBP);
     
             // then
             assertThat(price.compareTo(BigDecimal.valueOf(1.149))).isEqualTo(0);
    @@ -192,8 +230,8 @@ public void convertCurrencyShouldUseConversionRateFromRequestIfusepbsratesIsFals
                     BigDecimal.valueOf(0.6)));
     
             // when
    -        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, USD,
    -                false);
    +        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE,
    +                givenBidRequestWithCurrencies(requestConversionRates, false), EUR, USD);
     
             // then
             assertThat(price.compareTo(BigDecimal.valueOf(1.667))).isEqualTo(0);
    @@ -206,8 +244,8 @@ public void convertCurrencyShouldUseLatestRatesIfMultiplierWasNotFoundInRequestR
                     singletonMap(EUR, BigDecimal.valueOf(0.8434)));
     
             // when
    -        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, UAH,
    -                false);
    +        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE,
    +                givenBidRequestWithCurrencies(requestConversionRates, false), EUR, UAH);
     
             // then
             assertThat(price.compareTo(BigDecimal.valueOf(1.156))).isEqualTo(0);
    @@ -216,7 +254,8 @@ public void convertCurrencyShouldUseLatestRatesIfMultiplierWasNotFoundInRequestR
         @Test
         public void convertCurrencyShouldReturnSamePriceIfBidCurrencyIsNullAndServerCurrencyUSD() {
             // when
    -        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE, emptyMap(), USD, null, false);
    +        final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE,
    +                givenBidRequestWithCurrencies(emptyMap(), false), USD, null);
     
             // then
             assertThat(price.compareTo(BigDecimal.ONE)).isEqualTo(0);
    @@ -229,17 +268,18 @@ public void convertCurrencyShouldFailWhenRequestRatesIsNullAndNoExternalRatesPro
     
             // when and then
             assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> currencyConversionService.convertCurrency(BigDecimal.ONE, null, EUR, GBP,
    -                        false))
    -                .withMessage("no currency conversion available");
    +                .isThrownBy(() -> currencyConversionService.convertCurrency(BigDecimal.ONE,
    +                        givenBidRequestWithCurrencies(null, false), EUR, GBP))
    +                .withMessage("Unable to convert from currency GBP to desired ad server currency EUR");
         }
     
         @Test
         public void convertCurrencyShouldThrowPrebidExceptionIfServerAndRequestRatesAreNull() {
             // when and then
             assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, null, USD, EUR, false))
    -                .withMessage("no currency conversion available");
    +                .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE,
    +                        givenBidRequestWithCurrencies(null, false), USD, EUR))
    +                .withMessage("Unable to convert from currency EUR to desired ad server currency USD");
         }
     
         @Test
    @@ -251,13 +291,13 @@ public void convertCurrencyShouldThrowPrebidExceptionIfMultiplierWasNotFoundFrom
             givenHttpClientReturnsResponse(httpClient, 503, "server unavailable");
     
             // when
    -        currencyService = setExternalResource(URL, 1L, vertx, httpClient);
    +        currencyService = createInitializedService(URL, 1L, -1L, httpClient);
     
             // then
             assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, requestConversionRates, EUR, AUD,
    -                        false))
    -                .withMessage("no currency conversion available");
    +                .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE,
    +                        givenBidRequestWithCurrencies(requestConversionRates, false), EUR, AUD))
    +                .withMessage("Unable to convert from currency AUD to desired ad server currency EUR");
         }
     
         @Test
    @@ -266,12 +306,13 @@ public void convertCurrencyShouldThrowExceptionWhenCurrencyServerResponseStatusN
             givenHttpClientReturnsResponse(httpClient, 503, "server unavailable");
     
             // when
    -        currencyService = setExternalResource(URL, 1L, vertx, httpClient);
    +        currencyService = createInitializedService(URL, 1L, -1L, httpClient);
     
             // then
             assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, null, UAH, AUD, false))
    -                .withMessage("no currency conversion available");
    +                .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE,
    +                        givenBidRequestWithCurrencies(null, false), UAH, AUD))
    +                .withMessage("Unable to convert from currency AUD to desired ad server currency UAH");
         }
     
         @Test
    @@ -280,12 +321,13 @@ public void convertCurrencyShouldThrowExceptionWhenCurrencyServerResponseContain
             givenHttpClientReturnsResponse(httpClient, 200, "{\"foo\": \"bar\"}");
     
             // when
    -        currencyService = setExternalResource(URL, 1L, vertx, httpClient);
    +        currencyService = createInitializedService(URL, 1L, -1L, httpClient);
     
             // then
             assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE, null, UAH, AUD, false))
    -                .withMessage("no currency conversion available");
    +                .isThrownBy(() -> currencyService.convertCurrency(BigDecimal.ONE,
    +                        givenBidRequestWithCurrencies(null, false), UAH, AUD))
    +                .withMessage("Unable to convert from currency AUD to desired ad server currency UAH");
         }
     
         @SuppressWarnings("unchecked")
    @@ -297,7 +339,7 @@ public void initializeShouldMakeOneInitialRequestAndTwoScheduled() {
             givenHttpClientReturnsResponse(httpClient, 200, "{\"foo\": \"bar\"}");
     
             // when and then
    -        currencyService = setExternalResource(URL, 1000, vertx, httpClient);
    +        currencyService = createInitializedService(URL, 1000, -1L, httpClient);
     
             final ArgumentCaptor> handlerCaptor = ArgumentCaptor.forClass(Handler.class);
             verify(vertx).setPeriodic(eq(1000L), handlerCaptor.capture());
    @@ -309,11 +351,26 @@ public void initializeShouldMakeOneInitialRequestAndTwoScheduled() {
             verify(httpClient, times(3)).get(anyString(), anyLong());
         }
     
    -    private static CurrencyConversionService setExternalResource(String url, long refreshPeriod, Vertx vertx,
    -                                                                 HttpClient httpClient) {
    +    private CurrencyConversionService createInitializedService(String url,
    +                                                               long refreshPeriod,
    +                                                               long staleAfter,
    +                                                               HttpClient httpClient) {
    +
             final CurrencyConversionService currencyService = new CurrencyConversionService(
    -                new ExternalConversionProperties(url, 1000L, refreshPeriod, vertx, httpClient, jacksonMapper));
    +                new ExternalConversionProperties(
    +                        url,
    +                        1000L,
    +                        refreshPeriod,
    +                        staleAfter,
    +                        null,
    +                        vertx,
    +                        httpClient,
    +                        metrics,
    +                        clock,
    +                        jacksonMapper));
    +
             currencyService.initialize();
    +
             return currencyService;
         }
     
    @@ -322,4 +379,12 @@ private static void givenHttpClientReturnsResponse(HttpClient httpClient, int st
             given(httpClient.get(anyString(), anyLong()))
                     .willReturn(Future.succeededFuture(httpClientResponse));
         }
    +
    +    private BidRequest givenBidRequestWithCurrencies(Map> requestCurrencies,
    +                                                     Boolean usepbsrates) {
    +        return BidRequest.builder()
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .currency(ExtRequestCurrency.of(requestCurrencies, usepbsrates)).build()))
    +                .build();
    +    }
     }
    diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java
    index 2b90e172417..0b2d928c811 100644
    --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java
    +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java
    @@ -1,22 +1,27 @@
     package org.prebid.server.auction;
     
    -import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.fasterxml.jackson.core.type.TypeReference;
     import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.node.BooleanNode;
     import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.fasterxml.jackson.databind.node.TextNode;
     import com.iab.openrtb.request.App;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.BidRequest.BidRequestBuilder;
    +import com.iab.openrtb.request.Content;
    +import com.iab.openrtb.request.Data;
    +import com.iab.openrtb.request.Deal;
     import com.iab.openrtb.request.Device;
     import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Geo;
     import com.iab.openrtb.request.Imp;
     import com.iab.openrtb.request.Imp.ImpBuilder;
    +import com.iab.openrtb.request.Pmp;
     import com.iab.openrtb.request.Publisher;
     import com.iab.openrtb.request.Regs;
     import com.iab.openrtb.request.Site;
     import com.iab.openrtb.request.User;
    +import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
     import com.iab.openrtb.response.SeatBid;
    @@ -34,7 +39,10 @@
     import org.prebid.server.auction.model.AuctionContext;
     import org.prebid.server.auction.model.BidRequestCacheInfo;
     import org.prebid.server.auction.model.BidderPrivacyResult;
    +import org.prebid.server.auction.model.BidderRequest;
     import org.prebid.server.auction.model.BidderResponse;
    +import org.prebid.server.auction.model.DebugContext;
    +import org.prebid.server.auction.model.MultiBidConfig;
     import org.prebid.server.auction.model.StoredResponseResult;
     import org.prebid.server.bidder.Bidder;
     import org.prebid.server.bidder.BidderCatalog;
    @@ -45,21 +53,49 @@
     import org.prebid.server.bidder.model.BidderSeatBid;
     import org.prebid.server.cookie.UidsCookie;
     import org.prebid.server.currency.CurrencyConversionService;
    +import org.prebid.server.deals.events.ApplicationEventService;
    +import org.prebid.server.deals.model.DeepDebugLog;
    +import org.prebid.server.deals.model.TxnLog;
     import org.prebid.server.exception.InvalidRequestException;
     import org.prebid.server.exception.PreBidException;
     import org.prebid.server.execution.Timeout;
     import org.prebid.server.execution.TimeoutFactory;
    +import org.prebid.server.hooks.execution.HookStageExecutor;
    +import org.prebid.server.hooks.execution.model.ExecutionAction;
    +import org.prebid.server.hooks.execution.model.ExecutionStatus;
    +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
    +import org.prebid.server.hooks.execution.model.HookExecutionContext;
    +import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
    +import org.prebid.server.hooks.execution.model.HookId;
    +import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
    +import org.prebid.server.hooks.execution.model.Stage;
    +import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
    +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl;
    +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
    +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl;
    +import org.prebid.server.hooks.v1.analytics.ActivityImpl;
    +import org.prebid.server.hooks.v1.analytics.AppliedToImpl;
    +import org.prebid.server.hooks.v1.analytics.ResultImpl;
    +import org.prebid.server.hooks.v1.analytics.TagsImpl;
    +import org.prebid.server.log.CriteriaLogManager;
     import org.prebid.server.log.HttpInteractionLogger;
     import org.prebid.server.metric.MetricName;
     import org.prebid.server.metric.Metrics;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
    +import org.prebid.server.model.Endpoint;
    +import org.prebid.server.model.HttpRequestContext;
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.BidAdjustmentMediaType;
     import org.prebid.server.proto.openrtb.ext.request.ExtApp;
     import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfig;
    -import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigFpd;
    +import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigOrtb;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDeal;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine;
     import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange;
     import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity;
     import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidadjustmentfactors;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig;
    @@ -67,22 +103,34 @@
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheBids;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheVastxml;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchainNode;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
     import org.prebid.server.proto.openrtb.ext.request.ExtSite;
    -import org.prebid.server.proto.openrtb.ext.request.ExtSource;
     import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEid;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.TraceLevel;
     import org.prebid.server.proto.openrtb.ext.response.BidType;
     import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
     import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse;
     import org.prebid.server.proto.openrtb.ext.response.ExtBidderError;
    -import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModules;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome;
     import org.prebid.server.settings.model.Account;
    +import org.prebid.server.settings.model.AccountAuctionConfig;
    +import org.prebid.server.settings.model.AccountEventsConfig;
     import org.prebid.server.validation.ResponseBidValidator;
     import org.prebid.server.validation.model.ValidationResult;
     
    @@ -91,6 +139,10 @@
     import java.time.Clock;
     import java.time.Instant;
     import java.time.ZoneId;
    +import java.util.ArrayList;
    +import java.util.Arrays;
    +import java.util.Collections;
    +import java.util.EnumMap;
     import java.util.HashMap;
     import java.util.List;
     import java.util.Map;
    @@ -117,16 +169,21 @@
     import static org.mockito.ArgumentMatchers.isNull;
     import static org.mockito.ArgumentMatchers.same;
     import static org.mockito.BDDMockito.given;
    +import static org.mockito.BDDMockito.willReturn;
     import static org.mockito.Mockito.any;
     import static org.mockito.Mockito.anyInt;
     import static org.mockito.Mockito.anyList;
    +import static org.mockito.Mockito.doAnswer;
     import static org.mockito.Mockito.doReturn;
     import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.never;
     import static org.mockito.Mockito.times;
     import static org.mockito.Mockito.verify;
     import static org.mockito.Mockito.verifyNoMoreInteractions;
     import static org.mockito.Mockito.verifyZeroInteractions;
    +import static org.prebid.server.assertion.FutureAssertion.assertThat;
     import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
     
     public class ExchangeServiceTest extends VertxTest {
     
    @@ -136,14 +193,14 @@ public class ExchangeServiceTest extends VertxTest {
         @Mock
         private BidderCatalog bidderCatalog;
         @Mock
    -    private Usersyncer usersyncer;
    -    @Mock
         private StoredResponseProcessor storedResponseProcessor;
         @Mock
         private PrivacyEnforcementService privacyEnforcementService;
         @Mock
         private FpdResolver fpdResolver;
         @Mock
    +    private SchainResolver schainResolver;
    +    @Mock
         private HttpBidderRequester httpBidderRequester;
         @Mock
         private ResponseBidValidator responseBidValidator;
    @@ -154,13 +211,18 @@ public class ExchangeServiceTest extends VertxTest {
         @Spy
         private BidResponsePostProcessor.NoOpBidResponsePostProcessor bidResponsePostProcessor;
         @Mock
    +    private HookStageExecutor hookStageExecutor;
    +    @Mock
    +    private ApplicationEventService applicationEventService;
    +    @Mock
         private HttpInteractionLogger httpInteractionLogger;
         @Mock
         private Metrics metrics;
         @Mock
         private UidsCookie uidsCookie;
    -
         private Clock clock;
    +    @Mock
    +    private CriteriaLogManager criteriaLogManager;
     
         private ExchangeService exchangeService;
     
    @@ -169,12 +231,12 @@ public class ExchangeServiceTest extends VertxTest {
         @SuppressWarnings("unchecked")
         @Before
         public void setUp() {
    -        given(bidResponseCreator.create(anyList(), any(), any(), anyBoolean()))
    +        given(bidResponseCreator.create(anyList(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(givenBidResponseWithBids(singletonList(givenBid(identity())))));
     
             given(bidderCatalog.isValidName(anyString())).willReturn(true);
             given(bidderCatalog.isActive(anyString())).willReturn(true);
    -        given(bidderCatalog.usersyncerByName(anyString())).willReturn(usersyncer);
    +        given(bidderCatalog.usersyncerByName(anyString())).willReturn(Usersyncer.of("cookieFamily", null, null));
     
             given(privacyEnforcementService.mask(any(), argThat(MapUtils::isNotEmpty), any(), any()))
                     .willAnswer(inv ->
    @@ -191,18 +253,39 @@ public void setUp() {
             given(fpdResolver.resolveUser(any(), any())).willAnswer(invocation -> invocation.getArgument(0));
             given(fpdResolver.resolveSite(any(), any())).willAnswer(invocation -> invocation.getArgument(0));
             given(fpdResolver.resolveApp(any(), any())).willAnswer(invocation -> invocation.getArgument(0));
    -
    -        given(responseBidValidator.validate(any())).willReturn(ValidationResult.success());
    -        given(usersyncer.getCookieFamilyName()).willReturn("cookieFamily");
    -
    -        given(currencyService.convertCurrency(any(), any(), any(), any(), any()))
    +        given(fpdResolver.resolveImpExt(any(), anyBoolean()))
    +                .willAnswer(invocation -> invocation.getArgument(0));
    +
    +        given(schainResolver.resolveForBidder(anyString(), any())).willReturn(null);
    +
    +        given(hookStageExecutor.executeBidderRequestStage(any(), any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                        false,
    +                        BidderRequestPayloadImpl.of(invocation.getArgument(0).getBidRequest()))));
    +        given(hookStageExecutor.executeRawBidderResponseStage(any(), any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                        false,
    +                        BidderResponsePayloadImpl.of(invocation.getArgument(0).getSeatBid()
    +                                .getBids()))));
    +        given(hookStageExecutor.executeAuctionResponseStage(any(), any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                        false,
    +                        AuctionResponsePayloadImpl.of(invocation.getArgument(0)))));
    +
    +        given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success());
    +
    +        given(currencyService.convertCurrency(any(), any(), any(), any()))
                     .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
     
    -        given(storedResponseProcessor.getStoredResponseResult(any(), any(), any()))
    -                .willAnswer(inv -> Future.succeededFuture(StoredResponseResult.of(inv.getArgument(0), emptyList())));
    +        given(storedResponseProcessor.getStoredResponseResult(any(), any()))
    +                .willAnswer(inv -> Future.succeededFuture(StoredResponseResult.of(inv.getArgument(0), emptyList(),
    +                        emptyMap())));
             given(storedResponseProcessor.mergeWithBidderResponses(any(), any(), any())).willAnswer(
                     inv -> inv.getArgument(0));
     
    +        given(criteriaLogManager.traceResponse(any(), any(), any(), anyBoolean()))
    +                .willAnswer(inv -> inv.getArgument(1));
    +
             clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
             timeout = new TimeoutFactory(clock).create(500);
     
    @@ -212,35 +295,44 @@ public void setUp() {
                     storedResponseProcessor,
                     privacyEnforcementService,
                     fpdResolver,
    +                schainResolver,
                     httpBidderRequester,
                     responseBidValidator,
                     currencyService,
                     bidResponseCreator,
                     bidResponsePostProcessor,
    +                hookStageExecutor,
    +                applicationEventService,
                     httpInteractionLogger,
                     metrics,
                     clock,
    -                jacksonMapper);
    +                jacksonMapper,
    +                criteriaLogManager);
         }
     
         @Test
         public void creationShouldFailOnNegativeExpectedCacheTime() {
    -        assertThatIllegalArgumentException().isThrownBy(
    -                () -> new ExchangeService(
    +        assertThatIllegalArgumentException()
    +                .isThrownBy(() -> new ExchangeService(
                             -1,
                             bidderCatalog,
                             storedResponseProcessor,
                             privacyEnforcementService,
                             fpdResolver,
    +                        schainResolver,
                             httpBidderRequester,
                             responseBidValidator,
                             currencyService,
                             bidResponseCreator,
                             bidResponsePostProcessor,
    +                        hookStageExecutor,
    +                        applicationEventService,
                             httpInteractionLogger,
                             metrics,
                             clock,
    -                        jacksonMapper));
    +                        jacksonMapper,
    +                        criteriaLogManager))
    +                .withMessage("Expected cache time should be positive");
         }
     
         @Test
    @@ -296,7 +388,7 @@ public void shouldExtractRequestWithBidderSpecificExtension() {
             givenBidder(givenEmptySeatBid());
     
             final BidRequest bidRequest = givenBidRequest(singletonList(
    -                givenImp(doubleMap("prebid", 0, "someBidder", 1), builder -> builder
    +                givenImp(singletonMap("someBidder", 1), builder -> builder
                             .id("impId")
                             .banner(Banner.builder()
                                     .format(singletonList(Format.builder().w(400).h(300).build()))
    @@ -316,7 +408,7 @@ public void shouldExtractRequestWithBidderSpecificExtension() {
                             .banner(Banner.builder()
                                     .format(singletonList(Format.builder().w(400).h(300).build()))
                                     .build())
    -                        .ext(mapper.valueToTree(ExtPrebid.of(0, 1)))
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, 1)))
                             .build()))
                     .tmax(500L)
                     .build());
    @@ -332,7 +424,7 @@ public void shouldExtractRequestWithCurrencyRatesExtension() {
                     "UAH", singletonMap("EUR", BigDecimal.valueOf(1.1565)));
     
             final BidRequest bidRequest = givenBidRequest(singletonList(
    -                givenImp(doubleMap("prebid", 0, "someBidder", 1), builder -> builder
    +                givenImp(singletonMap("someBidder", 1), builder -> builder
                             .id("impId")
                             .banner(Banner.builder()
                                     .format(singletonList(Format.builder().w(400).h(300).build()))
    @@ -358,7 +450,7 @@ public void shouldExtractRequestWithCurrencyRatesExtension() {
                             .banner(Banner.builder()
                                     .format(singletonList(Format.builder().w(400).h(300).build()))
                                     .build())
    -                        .ext(mapper.valueToTree(ExtPrebid.of(0, 1)))
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, 1)))
                             .build()))
                     .ext(ExtRequest.of(
                             ExtRequestPrebid.builder().currency(ExtRequestCurrency.of(currencyRates, false)).build()))
    @@ -382,55 +474,153 @@ public void shouldExtractMultipleRequests() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequest1Captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(same(bidder1), bidRequest1Captor.capture(), any(), anyBoolean());
    -        final BidRequest capturedBidRequest1 = bidRequest1Captor.getValue();
    -        assertThat(capturedBidRequest1.getImp()).hasSize(2)
    +        final ArgumentCaptor bidRequest1Captor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(same(bidder1), bidRequest1Captor.capture(), any(), any(), anyBoolean());
    +        final BidderRequest capturedBidRequest1 = bidRequest1Captor.getValue();
    +        assertThat(capturedBidRequest1.getBidRequest().getImp()).hasSize(2)
                     .extracting(imp -> imp.getExt().get("bidder").asInt())
                     .containsOnly(1, 3);
     
    -        final ArgumentCaptor bidRequest2Captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(same(bidder2), bidRequest2Captor.capture(), any(), anyBoolean());
    -        final BidRequest capturedBidRequest2 = bidRequest2Captor.getValue();
    -        assertThat(capturedBidRequest2.getImp()).hasSize(1)
    +        final ArgumentCaptor bidRequest2Captor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(same(bidder2), bidRequest2Captor.capture(), any(), any(), anyBoolean());
    +        final BidderRequest capturedBidRequest2 = bidRequest2Captor.getValue();
    +        assertThat(capturedBidRequest2.getBidRequest().getImp()).hasSize(1)
                     .element(0).returns(2, imp -> imp.getExt().get("bidder").asInt());
         }
     
         @Test
    -    public void shouldPassImpWithExtPrebidToDefinedBidder() {
    +    public void shouldSkipBidderWhenRejectedByBidderRequestHooks() {
             // given
    -        final String bidder1Name = "bidder1";
    -        final String bidder2Name = "bidder2";
    -        final Bidder bidder1 = mock(Bidder.class);
    -        final Bidder bidder2 = mock(Bidder.class);
    -        givenBidder(bidder1Name, bidder1, givenEmptySeatBid());
    -        givenBidder(bidder2Name, bidder2, givenEmptySeatBid());
    +        doAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(true, null)))
    +                .when(hookStageExecutor).executeBidderRequestStage(any(), any());
     
    -        final ObjectNode impExt = mapper.createObjectNode()
    -                .put(bidder1Name, "ignored1")
    -                .put(bidder2Name, "ignored2")
    -                .putPOJO("prebid", doubleMap(bidder1Name, mapper.createObjectNode().put("somefield", "bidder1"),
    -                        bidder2Name, mapper.createObjectNode().put("somefield", "bidder2")));
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), identity());
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        verifyZeroInteractions(httpBidderRequester);
    +    }
    +
    +    @Test
    +    public void shouldPassRequestModifiedByBidderRequestHooks() {
    +        // given
    +        givenBidder(givenEmptySeatBid());
    +
    +        doAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                false,
    +                BidderRequestPayloadImpl.of(BidRequest.builder().id("bidderRequestId").build()))))
    +                .when(hookStageExecutor).executeBidderRequestStage(any(), any());
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), identity());
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        final BidRequest capturedBidRequest = captureBidRequest();
    +        assertThat(capturedBidRequest).isEqualTo(BidRequest.builder().id("bidderRequestId").build());
    +    }
    +
    +    @Test
    +    @SuppressWarnings("unchecked")
    +    public void shouldSkipBidderWhenRejectedByRawBidderResponseHooks() {
    +        // given
    +        final String bidder = "someBidder";
    +        givenBidder(bidder, mock(Bidder.class), givenSeatBid(singletonList(
    +                givenBid(Bid.builder().price(BigDecimal.ONE).build()))));
    +
    +        doAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(true, null)))
    +                .when(hookStageExecutor).executeRawBidderResponseStage(any(), any());
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap(bidder, 1)), identity());
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
     
    -        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(impExt, identity())), identity());
    +        // then
    +        final ArgumentCaptor> bidResponseCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(storedResponseProcessor).mergeWithBidderResponses(bidResponseCaptor.capture(), any(), any());
    +
    +        assertThat(bidResponseCaptor.getValue())
    +                .extracting(BidderResponse::getSeatBid)
    +                .containsOnly(BidderSeatBid.empty());
    +    }
    +
    +    @Test
    +    @SuppressWarnings("unchecked")
    +    public void shouldPassRequestModifiedByRawBidderResponseHooks() {
    +        // given
    +        final String bidder = "someBidder";
    +        givenBidder(bidder, mock(Bidder.class), givenSeatBid(singletonList(
    +                givenBid(Bid.builder().build()))));
    +
    +        final BidderBid hookChangedBid = BidderBid.of(Bid.builder().id("newId").build(), video, "USD");
    +        doAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                false,
    +                BidderResponsePayloadImpl.of(singletonList(hookChangedBid)))))
    +                .when(hookStageExecutor).executeRawBidderResponseStage(any(), any());
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap(bidder, 1)), identity());
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequest1Captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(same(bidder1), bidRequest1Captor.capture(), any(), anyBoolean());
    -        assertThat(bidRequest1Captor.getValue().getImp()).hasSize(1)
    -                .extracting(imp -> imp.getExt().get("prebid"))
    -                .containsOnly(mapper.createObjectNode().set("bidder",
    -                        mapper.createObjectNode().put("somefield", "bidder1")));
    +        final ArgumentCaptor> bidResponseCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(storedResponseProcessor).mergeWithBidderResponses(bidResponseCaptor.capture(), any(), any());
    +
    +        assertThat(bidResponseCaptor.getValue())
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getBids)
    +                .containsOnly(hookChangedBid);
    +    }
    +
    +    @Test
    +    public void shouldExtractRequestsWithoutFilteredDealsOnlyBidders() {
    +        // given
    +        exchangeService = new ExchangeService(
    +                100,
    +                bidderCatalog,
    +                storedResponseProcessor,
    +                privacyEnforcementService,
    +                fpdResolver,
    +                schainResolver,
    +                httpBidderRequester,
    +                responseBidValidator,
    +                currencyService,
    +                bidResponseCreator,
    +                bidResponsePostProcessor,
    +                hookStageExecutor,
    +                applicationEventService,
    +                httpInteractionLogger,
    +                metrics,
    +                clock,
    +                jacksonMapper,
    +                criteriaLogManager);
    +
    +        final Bidder bidder1 = mock(Bidder.class);
    +        final Bidder bidder2 = mock(Bidder.class);
    +        givenBidder("bidder1", bidder1, givenEmptySeatBid());
    +        givenBidder("bidder2", bidder2, givenEmptySeatBid());
    +
    +        final BidRequest bidRequest = givenBidRequest(asList(
    +                givenImp(singletonMap("bidder1", 1),
    +                        identity()),
    +                givenImp(singletonMap("bidder2", mapper.createObjectNode().set("dealsonly", BooleanNode.getTrue())),
    +                        identity())));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
     
    -        final ArgumentCaptor bidRequest2Captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(same(bidder2), bidRequest2Captor.capture(), any(), anyBoolean());
    -        assertThat(bidRequest2Captor.getValue().getImp()).hasSize(1)
    -                .extracting(imp -> imp.getExt().get("prebid"))
    -                .containsOnly(mapper.createObjectNode().set("bidder",
    -                        mapper.createObjectNode().put("somefield", "bidder2")));
    +        // then
    +        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final BidRequest capturedBidRequest = bidRequestCaptor.getValue().getBidRequest();
    +        assertThat(capturedBidRequest.getImp()).hasSize(1)
    +                .extracting(imp -> imp.getExt().get("bidder").asInt())
    +                .containsOnly(1);
         }
     
         @Test
    @@ -461,20 +651,20 @@ public void shouldPassRequestWithExtPrebidToDefinedBidder() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequest1Captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(same(bidder1), bidRequest1Captor.capture(), any(), anyBoolean());
    +        final ArgumentCaptor bidRequest1Captor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(same(bidder1), bidRequest1Captor.capture(), any(), any(), anyBoolean());
     
    -        final BidRequest capturedBidRequest1 = bidRequest1Captor.getValue();
    -        final ExtRequestPrebid prebid1 = capturedBidRequest1.getExt().getPrebid();
    +        final BidderRequest capturedBidRequest1 = bidRequest1Captor.getValue();
    +        final ExtRequestPrebid prebid1 = capturedBidRequest1.getBidRequest().getExt().getPrebid();
             assertThat(prebid1).isNotNull();
             final JsonNode bidders1 = prebid1.getBidders();
             assertThat(bidders1).isNotNull();
             assertThat(bidders1.fields()).hasSize(1)
                     .containsOnly(entry("bidder", mapper.createObjectNode().put("test1", "test1")));
     
    -        final ArgumentCaptor bidRequest2Captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(same(bidder2), bidRequest2Captor.capture(), any(), anyBoolean());
    -        final BidRequest capturedBidRequest2 = bidRequest2Captor.getValue();
    +        final ArgumentCaptor bidRequest2Captor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(same(bidder2), bidRequest2Captor.capture(), any(), any(), anyBoolean());
    +        final BidRequest capturedBidRequest2 = bidRequest2Captor.getValue().getBidRequest();
             final ExtRequestPrebid prebid2 = capturedBidRequest2.getExt().getPrebid();
             assertThat(prebid2).isNotNull();
             final JsonNode bidders2 = prebid2.getBidders();
    @@ -496,93 +686,64 @@ public void shouldPassRequestWithInjectedSchainInSourceExt() {
             givenBidder(bidder2Name, bidder2, givenEmptySeatBid());
             givenBidder(bidder3Name, bidder3, givenEmptySeatBid());
     
    -        final ObjectNode schainExtObjectNode = mapper.createObjectNode().put("any", "any");
    -        final ExtRequestPrebidSchainSchainNode specificNodes = ExtRequestPrebidSchainSchainNode.of("asi", "sid", 1,
    -                "rid", "name", "domain", schainExtObjectNode);
    -        final ExtRequestPrebidSchainSchain specificSchain = ExtRequestPrebidSchainSchain.of("ver", 1,
    -                singletonList(specificNodes), schainExtObjectNode);
    +        final ExtRequestPrebidSchainSchainNode specificNodes = ExtRequestPrebidSchainSchainNode.of(
    +                "asi", "sid", 1, "rid", "name", "domain", null);
    +        final ExtRequestPrebidSchainSchain specificSchain = ExtRequestPrebidSchainSchain.of(
    +                "ver", 1, singletonList(specificNodes), null);
             final ExtRequestPrebidSchain schainForBidders = ExtRequestPrebidSchain.of(
    -                asList(bidder1Name, bidder2Name),
    -                specificSchain);
    -        final ExtRequestPrebidSchainSchain allSchainObject = ExtRequestPrebidSchainSchain.of("ver", 1,
    -                singletonList(specificNodes), schainExtObjectNode);
    -        final ExtRequestPrebidSchainSchainNode generalNodes = ExtRequestPrebidSchainSchainNode.of("t", null, 0, "a",
    -                null, "ads", null);
    -        final ExtRequestPrebidSchainSchain generalSchain = ExtRequestPrebidSchainSchain.of("t", 123,
    -                singletonList(generalNodes), null);
    +                asList(bidder1Name, bidder2Name), specificSchain);
    +
    +        final ExtRequestPrebidSchainSchainNode generalNodes = ExtRequestPrebidSchainSchainNode.of(
    +                "t", null, 0, "a", null, "ads", null);
    +        final ExtRequestPrebidSchainSchain generalSchain = ExtRequestPrebidSchainSchain.of(
    +                "t", 123, singletonList(generalNodes), null);
             final ExtRequestPrebidSchain allSchain = ExtRequestPrebidSchain.of(singletonList("*"), generalSchain);
    +
             final ExtRequest extRequest = ExtRequest.of(
                     ExtRequestPrebid.builder()
                             .schains(asList(schainForBidders, allSchain))
                             .auctiontimestamp(1000L)
                             .build());
    -
    -        final BidRequest bidRequest = givenBidRequest(asList(
    -                givenImp(singletonMap(bidder1Name, 1), identity()),
    -                givenImp(singletonMap(bidder2Name, 2), identity()),
    -                givenImp(singletonMap(bidder3Name, 3), identity())),
    +        final BidRequest bidRequest = givenBidRequest(
    +                asList(
    +                        givenImp(singletonMap(bidder1Name, 1), identity()),
    +                        givenImp(singletonMap(bidder2Name, 2), identity()),
    +                        givenImp(singletonMap(bidder3Name, 3), identity())),
                     builder -> builder.ext(extRequest));
     
    +        given(schainResolver.resolveForBidder(eq("bidder1"), same(bidRequest))).willReturn(specificSchain);
    +        given(schainResolver.resolveForBidder(eq("bidder2"), same(bidRequest))).willReturn(specificSchain);
    +        given(schainResolver.resolveForBidder(eq("bidder3"), same(bidRequest))).willReturn(generalSchain);
    +
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequest1Captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(same(bidder1), bidRequest1Captor.capture(), any(), anyBoolean());
    -        final BidRequest capturedBidRequest1 = bidRequest1Captor.getValue();
    -        ExtSource extSource = capturedBidRequest1.getSource().getExt();
    -        ExtRequestPrebidSchainSchain requestSchain1 = extSource.getSchain();
    +        final ArgumentCaptor bidRequest1Captor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(same(bidder1), bidRequest1Captor.capture(), any(), any(), anyBoolean());
    +        final BidRequest capturedBidRequest1 = bidRequest1Captor.getValue().getBidRequest();
    +        final ExtRequestPrebidSchainSchain requestSchain1 = capturedBidRequest1.getSource().getExt().getSchain();
             assertThat(requestSchain1).isNotNull();
             assertThat(requestSchain1).isEqualTo(specificSchain);
             assertThat(capturedBidRequest1.getExt().getPrebid().getSchains()).isNull();
     
    -        final ArgumentCaptor bidRequest2Captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(same(bidder2), bidRequest2Captor.capture(), any(), anyBoolean());
    -        final BidRequest capturedBidRequest2 = bidRequest2Captor.getValue();
    -        ExtRequestPrebidSchainSchain requestSchain2 = extSource.getSchain();
    +        final ArgumentCaptor bidRequest2Captor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(same(bidder2), bidRequest2Captor.capture(), any(), any(), anyBoolean());
    +        final BidRequest capturedBidRequest2 = bidRequest2Captor.getValue().getBidRequest();
    +        final ExtRequestPrebidSchainSchain requestSchain2 = capturedBidRequest2.getSource().getExt().getSchain();
             assertThat(requestSchain2).isNotNull();
             assertThat(requestSchain2).isEqualTo(specificSchain);
             assertThat(capturedBidRequest2.getExt().getPrebid().getSchains()).isNull();
     
    -        final ArgumentCaptor bidRequest3Captor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(same(bidder3), bidRequest3Captor.capture(), any(), anyBoolean());
    -        final BidRequest capturedBidRequest3 = bidRequest3Captor.getValue();
    -        ExtRequestPrebidSchainSchain requestSchain3 = extSource.getSchain();
    +        final ArgumentCaptor bidRequest3Captor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(same(bidder3), bidRequest3Captor.capture(), any(), any(), anyBoolean());
    +        final BidRequest capturedBidRequest3 = bidRequest3Captor.getValue().getBidRequest();
    +        final ExtRequestPrebidSchainSchain requestSchain3 = capturedBidRequest3.getSource().getExt().getSchain();
             assertThat(requestSchain3).isNotNull();
    -        assertThat(requestSchain3).isEqualTo(allSchainObject);
    +        assertThat(requestSchain3).isEqualTo(generalSchain);
             assertThat(capturedBidRequest3.getExt().getPrebid().getSchains()).isNull();
         }
     
    -    @Test
    -    public void shouldRejectDuplicatedSchainBidders() {
    -        // given
    -        final String bidder1 = "bidder";
    -        final String bidder2 = "bidder"; // same name
    -
    -        final ExtRequestPrebidSchain schainForBidder1 = ExtRequestPrebidSchain.of(
    -                singletonList(bidder1), ExtRequestPrebidSchainSchain.of("ver1", null, null, null));
    -        final ExtRequestPrebidSchain schainForBidder2 = ExtRequestPrebidSchain.of(
    -                singletonList(bidder2), ExtRequestPrebidSchainSchain.of("ver2", null, null, null));
    -
    -        final ExtRequest extRequest = ExtRequest.of(
    -                ExtRequestPrebid.builder()
    -                        .schains(asList(schainForBidder1, schainForBidder2))
    -                        .build());
    -
    -        final BidRequest bidRequest = givenBidRequest(asList(
    -                givenImp(singletonMap(bidder1, 1), identity()),
    -                givenImp(singletonMap(bidder2, 2), identity())),
    -                builder -> builder.ext(extRequest));
    -
    -        // when
    -        exchangeService.holdAuction(givenRequestContext(bidRequest));
    -
    -        // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        assertThat(bidRequestCaptor.getValue().getSource()).isNull();
    -    }
    -
         @Test
         public void shouldReturnFailedFutureWithUnchangedMessageWhenPrivacyEnforcementServiceFails() {
             // given
    @@ -647,13 +808,45 @@ public void shouldExtractRequestByAliasForCorrectBidder() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(same(bidder), bidRequestCaptor.capture(), any(), anyBoolean());
    -        assertThat(bidRequestCaptor.getValue().getImp()).hasSize(1)
    +        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(same(bidder), bidRequestCaptor.capture(), any(), any(), anyBoolean());
    +        assertThat(bidRequestCaptor.getValue().getBidRequest().getImp()).hasSize(1)
                     .extracting(imp -> imp.getExt().get("bidder").asInt())
                     .contains(1);
         }
     
    +    @Test
    +    public void shouldExtractRequestByAliasForDeal() {
    +        // given
    +        final Bidder bidder = mock(Bidder.class);
    +        givenBidder("bidder", bidder, givenEmptySeatBid());
    +
    +        final BidRequest bidRequest = givenBidRequest(singletonList(
    +                givenImp(singletonMap("bidderAlias", 1), identity()).toBuilder().pmp(Pmp.builder()
    +                        .deals(singletonList(Deal.builder()
    +                                .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of("lineItemId", "extLineItemId",
    +                                        singletonList(Format.builder().w(100).h(100).build()),
    +                                        "bidderAlias")))).build()))
    +                        .build()).build()),
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .aliases(singletonMap("bidderAlias", "bidder")).build())));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(same(bidder), bidRequestCaptor.capture(), any(), any(), anyBoolean());
    +        assertThat(bidRequestCaptor.getValue().getBidRequest().getImp()).hasSize(1)
    +                .extracting(Imp::getPmp)
    +                .flatExtracting(Pmp::getDeals).hasSize(1)
    +                .extracting(Deal::getExt)
    +                .extracting(ext -> mapper.treeToValue(ext, ExtDeal.class))
    +                .extracting(ExtDeal::getLine)
    +                .containsOnly(ExtDealLine.of("lineItemId", "extLineItemId",
    +                        singletonList(Format.builder().w(100).h(100).build()), null));
    +    }
    +
         @Test
         public void shouldExtractMultipleRequestsForTheSameBidderIfAliasesWereUsed() {
             // given
    @@ -671,12 +864,13 @@ public void shouldExtractMultipleRequestsForTheSameBidderIfAliasesWereUsed() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester, times(2)).requestBids(same(bidder), bidRequestCaptor.capture(), any(),
    +        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester, times(2)).requestBids(same(bidder), bidRequestCaptor.capture(), any(), any(),
                     anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final List capturedBidderRequests = bidRequestCaptor.getAllValues();
     
    -        assertThat(capturedBidRequests).hasSize(2)
    +        assertThat(capturedBidderRequests).hasSize(2)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(capturedBidRequest -> capturedBidRequest.getImp().get(0).getExt().get("bidder").asInt())
                     .containsOnly(2, 1);
         }
    @@ -698,23 +892,33 @@ public void shouldTolerateBidderResultWithoutBids() {
         }
     
         @Test
    -    public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderWereUsedWithingSingleImp() {
    +    public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderWereUsedWithinSingleImp() {
             // given
             given(httpBidderRequester.requestBids(any(),
    -                eq(givenBidRequest(givenSingleImp(mapper.valueToTree(ExtPrebid.of(null, 1))),
    +                eq(BidderRequest.of("bidder", null, givenBidRequest(
    +                        singletonList(givenImp(
    +                                null,
    +                                builder -> builder.ext(mapper.valueToTree(
    +                                        ExtPrebid.of(null, 1))))),
                             builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
                                     .auctiontimestamp(1000L)
    -                                .aliases(singletonMap("bidderAlias", "bidder")).build())))), any(), anyBoolean()))
    +                                .aliases(singletonMap("bidderAlias", "bidder"))
    +                                .build()))))), any(), any(), anyBoolean()))
                     .willReturn(Future.succeededFuture(givenSeatBid(singletonList(
    -                        givenBid(Bid.builder().price(BigDecimal.ONE).build())))));
    +                        givenBid(Bid.builder().impid("impId1").price(BigDecimal.ONE).build())))));
     
             given(httpBidderRequester.requestBids(any(),
    -                eq(givenBidRequest(givenSingleImp(mapper.valueToTree(ExtPrebid.of(null, 2))),
    +                eq(BidderRequest.of("bidderAlias", null, givenBidRequest(
    +                        singletonList(givenImp(
    +                                null,
    +                                builder -> builder.ext(mapper.valueToTree(
    +                                        ExtPrebid.of(null, 2))))),
                             builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
                                     .auctiontimestamp(1000L)
    -                                .aliases(singletonMap("bidderAlias", "bidder")).build())))), any(), anyBoolean()))
    +                                .aliases(singletonMap("bidderAlias", "bidder"))
    +                                .build()))))), any(), any(), anyBoolean()))
                     .willReturn(Future.succeededFuture(givenSeatBid(singletonList(
    -                        givenBid(Bid.builder().price(BigDecimal.ONE).build())))));
    +                        givenBid(Bid.builder().impid("impId2").price(BigDecimal.ONE).build())))));
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(doubleMap("bidder", 1, "bidderAlias", 2)),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    @@ -722,7 +926,7 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW
                             .auctiontimestamp(1000L)
                             .build())));
     
    -        given(bidResponseCreator.create(anyList(), any(), any(), anyBoolean()))
    +        given(bidResponseCreator.create(anyList(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(BidResponse.builder()
                             .seatbid(asList(
                                     givenSeatBid(singletonList(givenBid(identity())), identity()),
    @@ -733,7 +937,7 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW
             final BidResponse bidResponse = exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
    -        verify(httpBidderRequester, times(2)).requestBids(any(), any(), any(), anyBoolean());
    +        verify(httpBidderRequester, times(2)).requestBids(any(), any(), any(), any(), anyBoolean());
             assertThat(bidResponse.getSeatbid()).hasSize(2)
                     .extracting(seatBid -> seatBid.getBid().size())
                     .containsOnly(1, 1);
    @@ -741,26 +945,40 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW
     
         @Test
         @SuppressWarnings("unchecked")
    -    public void shouldCallBidResponseCreatorWithExpectedParams() {
    +    public void shouldCallBidResponseCreatorWithExpectedParamsAndUpdateDebugErrors() {
             // given
             givenBidder("bidder1", mock(Bidder.class), givenEmptySeatBid());
     
    -        final Bid thirdBid = Bid.builder().id("bidId3").impid("impId1").price(BigDecimal.valueOf(7.89)).build();
    +        final Bid thirdBid = Bid.builder().id("bidId3").impid("impId3").price(BigDecimal.valueOf(7.89)).build();
             givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList(givenBid(thirdBid))));
     
    +        final ExtRequestPrebidMultiBid multiBid1 = ExtRequestPrebidMultiBid.of("bidder1", null, 2, "bi1");
    +        final ExtRequestPrebidMultiBid multiBid2 = ExtRequestPrebidMultiBid.of("bidder2", singletonList("invalid"), 4,
    +                "bi2");
    +        final ExtRequestPrebidMultiBid multiBid3 = ExtRequestPrebidMultiBid.of("bidder3", singletonList("invalid"),
    +                null, "bi3");
    +        final ExtRequestPrebidMultiBid duplicateMultiBid1 = ExtRequestPrebidMultiBid.of("bidder1", null, 100, "bi1_2");
    +        final ExtRequestPrebidMultiBid duplicateMultiBids1 = ExtRequestPrebidMultiBid.of(null, singletonList("bidder1"),
    +                100, "bi1_3");
    +        final ExtRequestPrebidMultiBid multiBid4 = ExtRequestPrebidMultiBid.of(null,
    +                Arrays.asList("bidder4", "bidder5"), 3, "ignored");
    +
             final ExtRequestTargeting targeting = givenTargeting(true);
             final ObjectNode events = mapper.createObjectNode();
             final BidRequest bidRequest = givenBidRequest(asList(
                     // imp ids are not really used for matching, included them here for clarity
                     givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")),
    -                givenImp(doubleMap("bidder1", 1, "bidder2", 2), builder -> builder.id("impId1"))),
    +                givenImp(doubleMap("bidder1", 1, "bidder2", 2), builder -> builder.id("impId2"))),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
                             .targeting(targeting)
                             .auctiontimestamp(1000L)
                             .events(events)
    +                        .multibid(Arrays.asList(multiBid1, multiBid2, multiBid3, duplicateMultiBid1,
    +                                duplicateMultiBids1, multiBid4))
                             .cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(53, true),
                                     ExtRequestPrebidCacheVastxml.of(34, true), true))
    -                        .build())));
    +                        .build()))
    +        );
             final AuctionContext auctionContext = givenRequestContext(bidRequest);
     
             // when
    @@ -778,12 +996,47 @@ public void shouldCallBidResponseCreatorWithExpectedParams() {
                     .shouldCacheWinningBidsOnly(false)
                     .build();
     
    -        final ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
    -        verify(bidResponseCreator).create(captor.capture(), eq(auctionContext), eq(expectedCacheInfo), eq(false));
    +        final MultiBidConfig expectedMultiBid1 = MultiBidConfig.of(multiBid1.getBidder(), multiBid1.getMaxBids(),
    +                multiBid1.getTargetBidderCodePrefix());
    +        final MultiBidConfig expectedMultiBid2 = MultiBidConfig.of(multiBid2.getBidder(), multiBid2.getMaxBids(),
    +                multiBid2.getTargetBidderCodePrefix());
    +        final MultiBidConfig expectedFirstMultiBid4 = MultiBidConfig.of("bidder4", multiBid4.getMaxBids(), null);
    +        final MultiBidConfig expectedSecondMultiBid4 = MultiBidConfig.of("bidder5", multiBid4.getMaxBids(), null);
    +
    +        final Map expectedMultiBidMap = new HashMap<>();
    +        expectedMultiBidMap.put(expectedMultiBid1.getBidder(), expectedMultiBid1);
    +        expectedMultiBidMap.put(expectedMultiBid2.getBidder(), expectedMultiBid2);
    +        expectedMultiBidMap.put(expectedFirstMultiBid4.getBidder(), expectedFirstMultiBid4);
    +        expectedMultiBidMap.put(expectedSecondMultiBid4.getBidder(), expectedSecondMultiBid4);
    +
    +        final AuctionContext expectedAuctionContext = auctionContext.toBuilder()
    +                .debugWarnings(asList(
    +                        "Invalid MultiBid: bidder bidder2 and bidders [invalid] specified."
    +                                + " Only bidder bidder2 will be used.",
    +                        "Invalid MultiBid: bidder bidder3 and bidders [invalid] specified."
    +                                + " Only bidder bidder3 will be used.",
    +                        "Invalid MultiBid: MaxBids for bidder bidder3 is not specified and will be skipped.",
    +                        "Invalid MultiBid: Bidder bidder1 specified multiple times.",
    +                        "Invalid MultiBid: CodePrefix bi1_3 that was specified for bidders [bidder1] will be skipped.",
    +                        "Invalid MultiBid: Bidder bidder1 specified multiple times.",
    +                        "Invalid MultiBid: CodePrefix ignored that was specified for bidders [bidder4, bidder5]"
    +                                + " will be skipped."))
    +                .build();
    +
    +        final ArgumentCaptor> bidRequestCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(bidResponseCreator).create(bidRequestCaptor.capture(), eq(expectedAuctionContext), eq(expectedCacheInfo),
    +                eq(expectedMultiBidMap));
     
    -        assertThat(captor.getValue()).containsOnly(
    +        final ObjectNode expectedBidExt = mapper.createObjectNode().put("origbidcpm", new BigDecimal("7.89"));
    +        final Bid expectedThirdBid = Bid.builder()
    +                .id("bidId3")
    +                .impid("impId3")
    +                .price(BigDecimal.valueOf(7.89))
    +                .ext(expectedBidExt)
    +                .build();
    +        assertThat(bidRequestCaptor.getValue()).containsOnly(
                     BidderResponse.of("bidder2", BidderSeatBid.of(singletonList(
    -                        BidderBid.of(thirdBid, banner, null)), emptyList(), emptyList()), 0),
    +                        BidderBid.of(expectedThirdBid, banner, null)), emptyList(), emptyList()), 0),
                     BidderResponse.of("bidder1", BidderSeatBid.of(emptyList(), emptyList(), emptyList()), 0));
         }
     
    @@ -792,7 +1045,7 @@ public void shouldCallBidResponseCreatorWithWinningOnlyTrueWhenIncludeBidderKeys
             // given
             givenBidder("bidder1", mock(Bidder.class), givenEmptySeatBid());
     
    -        final Bid thirdBid = Bid.builder().id("bidId3").impid("impId1").price(BigDecimal.valueOf(7.89)).build();
    +        final Bid thirdBid = Bid.builder().id("bidId3").impid("impId3").price(BigDecimal.valueOf(7.89)).build();
             givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList(givenBid(thirdBid))));
     
             final ExtRequestTargeting targeting = givenTargeting(false);
    @@ -800,7 +1053,7 @@ public void shouldCallBidResponseCreatorWithWinningOnlyTrueWhenIncludeBidderKeys
             final BidRequest bidRequest = givenBidRequest(asList(
                     // imp ids are not really used for matching, included them here for clarity
                     givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")),
    -                givenImp(doubleMap("bidder1", 1, "bidder2", 2), builder -> builder.id("impId1"))),
    +                givenImp(doubleMap("bidder1", 1, "bidder2", 2), builder -> builder.id("impId2"))),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
                             .targeting(targeting)
                             .cache(ExtRequestPrebidCache.of(null, null, true))
    @@ -817,7 +1070,7 @@ public void shouldCallBidResponseCreatorWithWinningOnlyTrueWhenIncludeBidderKeys
                     anyList(),
                     auctionContextArgumentCaptor.capture(),
                     eq(BidRequestCacheInfo.builder().doCaching(true).shouldCacheWinningBidsOnly(true).build()),
    -                eq(false));
    +                eq(emptyMap()));
     
             assertThat(singletonList(auctionContextArgumentCaptor.getValue().getBidRequest()))
                     .extracting(BidRequest::getExt)
    @@ -832,7 +1085,7 @@ public void shouldCallBidResponseCreatorWithWinningOnlyFalseWhenWinningOnlyIsNul
             // given
             givenBidder("bidder1", mock(Bidder.class), givenEmptySeatBid());
     
    -        final Bid thirdBid = Bid.builder().id("bidId3").impid("impId1").price(BigDecimal.valueOf(7.89)).build();
    +        final Bid thirdBid = Bid.builder().id("bidId3").impid("impId3").price(BigDecimal.valueOf(7.89)).build();
             givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList(givenBid(thirdBid))));
     
             final ExtRequestTargeting targeting = givenTargeting(false);
    @@ -840,7 +1093,7 @@ public void shouldCallBidResponseCreatorWithWinningOnlyFalseWhenWinningOnlyIsNul
             final BidRequest bidRequest = givenBidRequest(asList(
                     // imp ids are not really used for matching, included them here for clarity
                     givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")),
    -                givenImp(doubleMap("bidder1", 1, "bidder2", 2), builder -> builder.id("impId1"))),
    +                givenImp(doubleMap("bidder1", 1, "bidder2", 2), builder -> builder.id("impId2"))),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
                             .targeting(targeting)
                             .cache(ExtRequestPrebidCache.of(null, null, null))
    @@ -855,104 +1108,131 @@ public void shouldCallBidResponseCreatorWithWinningOnlyFalseWhenWinningOnlyIsNul
                     anyList(),
                     any(),
                     eq(BidRequestCacheInfo.builder().build()),
    -                anyBoolean());
    +                eq(emptyMap()));
         }
     
         @Test
    -    public void shouldCallBidResponseCreatorWithEnabledDebugTrueIfTestFlagIsTrue() {
    +    public void shouldTolerateNullRequestExtPrebid() {
             // given
    -        givenBidder("bidder1", mock(Bidder.class), BidderSeatBid.of(
    -                singletonList(givenBid(Bid.builder().price(BigDecimal.ONE).build())),
    -                singletonList(ExtHttpCall.builder()
    -                        .uri("bidder1_uri1")
    -                        .requestbody("bidder1_requestBody1")
    -                        .status(200)
    -                        .responsebody("bidder1_responseBody1")
    -                        .build()),
    -                emptyList()));
    +        givenBidder(givenSingleSeatBid(givenBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())));
     
             final BidRequest bidRequest = givenBidRequest(
    -                givenSingleImp(singletonMap("bidder1", 1)),
    -                builder -> builder.test(1));
    +                givenSingleImp(singletonMap("someBidder", 1)),
    +                builder -> builder.ext(jacksonMapper.fillExtension(ExtRequest.empty(), singletonMap("someField", 1))));
     
             // when
    -        exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    +        final BidResponse bidResponse = exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
    -        verify(bidResponseCreator).create(anyList(), any(), any(), eq(true));
    +        assertThat(bidResponse.getSeatbid()).flatExtracting(SeatBid::getBid)
    +                .extracting(bid -> toExtBidPrebid(bid.getExt()).getTargeting())
    +                .allSatisfy(map -> assertThat(map).isNull());
         }
     
         @Test
    -    public void shouldCallBidResponseCreatorWithEnabledDebugTrueIfExtPrebidDebugIsOn() {
    +    public void shouldTolerateNullRequestExtPrebidTargeting() {
             // given
    -        givenBidder("bidder1", mock(Bidder.class), BidderSeatBid.of(
    -                singletonList(givenBid(Bid.builder().price(BigDecimal.ONE).build())),
    -                singletonList(ExtHttpCall.builder()
    -                        .uri("bidder1_uri1")
    -                        .requestbody("bidder1_requestBody1")
    -                        .status(200)
    -                        .responsebody("bidder1_responseBody1")
    -                        .build()),
    -                emptyList()));
    +        givenBidder(givenSingleSeatBid(givenBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())));
     
             final BidRequest bidRequest = givenBidRequest(
    -                givenSingleImp(singletonMap("bidder1", 1)),
    +                givenSingleImp(singletonMap("someBidder", 1)),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .debug(1)
    +                        .data(ExtRequestPrebidData.of(singletonList("someBidder"), null))
                             .auctiontimestamp(1000L)
                             .build())));
     
             // when
    -        exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    +        final BidResponse bidResponse = exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
    -        verify(bidResponseCreator).create(anyList(), any(), any(), eq(true));
    +        assertThat(bidResponse.getSeatbid()).flatExtracting(SeatBid::getBid)
    +                .extracting(bid -> toExtBidPrebid(bid.getExt()).getTargeting())
    +                .allSatisfy(map -> assertThat(map).isNull());
         }
     
    +    @SuppressWarnings("unchecked")
         @Test
    -    public void shouldTolerateNullRequestExtPrebid() {
    +    public void shouldTolerateResponseBidValidationErrors() {
             // given
    -        givenBidder(givenSingleSeatBid(givenBid(Bid.builder().price(BigDecimal.ONE).build())));
    +        givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList(
    +                givenBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build()))));
     
    -        final BidRequest bidRequest = givenBidRequest(
    -                givenSingleImp(singletonMap("someBidder", 1)),
    -                builder -> builder.ext(jacksonMapper.fillExtension(ExtRequest.empty(), singletonMap("someField", 1))));
    +        final BidRequest bidRequest = givenBidRequest(singletonList(
    +                // imp ids are not really used for matching, included them here for clarity
    +                givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))),
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .auctiontimestamp(1000L)
    +                        .build())));
    +
    +        given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.error(
    +                singletonList("bid validation warning"),
    +                "bid validation error"));
    +
    +        givenBidResponseCreator(singletonList(Bid.builder().build()));
     
             // when
    -        final BidResponse bidResponse = exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    +        exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
    -        assertThat(bidResponse.getSeatbid()).flatExtracting(SeatBid::getBid)
    -                .extracting(bid -> toExtPrebid(bid.getExt()).getPrebid().getTargeting())
    -                .allSatisfy(map -> assertThat(map).isNull());
    +        final ArgumentCaptor> bidderResponsesCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(bidResponseCreator).create(bidderResponsesCaptor.capture(), any(), any(), any());
    +        final List bidderResponses = bidderResponsesCaptor.getValue();
    +
    +        assertThat(bidderResponses)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getBids)
    +                .isEmpty();
    +        assertThat(bidderResponses)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getErrors)
    +                .containsOnly(
    +                        BidderError.generic("bid validation warning"),
    +                        BidderError.generic("bid validation error"));
         }
     
    +    @SuppressWarnings("unchecked")
         @Test
    -    public void shouldTolerateNullRequestExtPrebidTargeting() {
    +    public void shouldTolerateResponseBidValidationWarnings() {
             // given
    -        givenBidder(givenSingleSeatBid(givenBid(Bid.builder().price(BigDecimal.ONE).build())));
    +        givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList(
    +                givenBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build()))));
     
    -        final BidRequest bidRequest = givenBidRequest(
    -                givenSingleImp(singletonMap("someBidder", 1)),
    +        final BidRequest bidRequest = givenBidRequest(singletonList(
    +                // imp ids are not really used for matching, included them here for clarity
    +                givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .data(ExtRequestPrebidData.of(singletonList("someBidder")))
                             .auctiontimestamp(1000L)
                             .build())));
     
    +        given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success(
    +                singletonList("bid validation warning")));
    +
    +        givenBidResponseCreator(singletonList(Bid.builder().build()));
    +
             // when
    -        final BidResponse bidResponse = exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    +        exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
    -        assertThat(bidResponse.getSeatbid()).flatExtracting(SeatBid::getBid)
    -                .extracting(bid -> toExtPrebid(bid.getExt()).getPrebid().getTargeting())
    -                .allSatisfy(map -> assertThat(map).isNull());
    +        final ArgumentCaptor> bidderResponsesCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(bidResponseCreator).create(bidderResponsesCaptor.capture(), any(), any(), any());
    +        final List bidderResponses = bidderResponsesCaptor.getValue();
    +
    +        assertThat(bidderResponses)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getBids)
    +                .hasSize(1);
    +        assertThat(bidderResponses)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getErrors)
    +                .containsOnly(BidderError.generic("bid validation warning"));
         }
     
         @Test
    -    public void shouldTolerateResponseBidValidationErrors() throws JsonProcessingException {
    +    public void shouldRejectBidIfCurrencyIsNotValid() {
             // given
             givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList(
    -                givenBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build()))));
    +                givenBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build(),
    +                        "USDD"))));
     
             final BidRequest bidRequest = givenBidRequest(singletonList(
                     // imp ids are not really used for matching, included them here for clarity
    @@ -961,20 +1241,23 @@ public void shouldTolerateResponseBidValidationErrors() throws JsonProcessingExc
                             .auctiontimestamp(1000L)
                             .build())));
     
    -        given(responseBidValidator.validate(any()))
    -                .willReturn(ValidationResult.error("bid validation error"));
    +        given(responseBidValidator.validate(any(), any(), any(), any()))
    +                .willReturn(ValidationResult.error("BidResponse currency is not valid: USDD"));
     
             final List bidderErrors = singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(),
    -                "bid validation error"));
    +                "BidResponse currency is not valid: USDD"));
             givenBidResponseCreator(singletonMap("bidder1", bidderErrors));
     
             // when
             final BidResponse bidResponse = exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
    -        final ExtBidResponse ext = mapper.treeToValue(bidResponse.getExt(), ExtBidResponse.class);
    +        final ExtBidResponse ext = bidResponse.getExt();
             assertThat(ext.getErrors()).hasSize(1)
                     .containsOnly(entry("bidder1", bidderErrors));
    +        assertThat(bidResponse.getSeatbid())
    +                .extracting(SeatBid::getBid)
    +                .isEmpty();
         }
     
         @Test
    @@ -983,25 +1266,25 @@ public void shouldCreateRequestsFromImpsReturnedByStoredResponseProcessor() {
             givenBidder(givenEmptySeatBid());
     
             final BidRequest bidRequest = givenBidRequest(asList(
    -                givenImp(doubleMap("prebid", 0, "someBidder1", 1), builder -> builder
    +                givenImp(singletonMap("someBidder1", 1), builder -> builder
                             .id("impId1")
                             .banner(Banner.builder()
                                     .format(singletonList(Format.builder().w(400).h(300).build()))
                                     .build())),
    -                givenImp(doubleMap("prebid", 0, "someBidder2", 1), builder -> builder
    +                givenImp(singletonMap("someBidder2", 1), builder -> builder
                             .id("impId2")
                             .banner(Banner.builder()
                                     .format(singletonList(Format.builder().w(400).h(300).build()))
                                     .build()))),
                     builder -> builder.id("requestId").tmax(500L));
     
    -        given(storedResponseProcessor.getStoredResponseResult(any(), any(), any()))
    +        given(storedResponseProcessor.getStoredResponseResult(any(), any()))
                     .willReturn(Future.succeededFuture(StoredResponseResult
    -                        .of(singletonList(givenImp(doubleMap("prebid", 0, "someBidder1", 1), builder -> builder
    +                        .of(singletonList(givenImp(singletonMap("someBidder1", 1), builder -> builder
                                     .id("impId1")
                                     .banner(Banner.builder()
                                             .format(singletonList(Format.builder().w(400).h(300).build()))
    -                                        .build()))), emptyList())));
    +                                        .build()))), emptyList(), emptyMap())));
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    @@ -1016,7 +1299,7 @@ public void shouldCreateRequestsFromImpsReturnedByStoredResponseProcessor() {
                             .banner(Banner.builder()
                                     .format(singletonList(Format.builder().w(400).h(300).build()))
                                     .build())
    -                        .ext(mapper.valueToTree(ExtPrebid.of(0, 1)))
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, 1)))
                             .build()))
                     .tmax(500L)
                     .build());
    @@ -1060,7 +1343,7 @@ public void shouldProcessBidderResponseReturnedFromStoredResponseProcessor() {
         @Test
         public void shouldReturnFailedFutureWhenStoredResponseProcessorGetStoredResultReturnsFailedFuture() {
             // given
    -        given(storedResponseProcessor.getStoredResponseResult(any(), any(), any()))
    +        given(storedResponseProcessor.getStoredResponseResult(any(), any()))
                     .willReturn(Future.failedFuture(new InvalidRequestException("Error")));
     
             final BidRequest bidRequest = givenBidRequest(singletonList(
    @@ -1173,44 +1456,76 @@ public void shouldNotChangeGdprFromRequestWhenDeviceLmtIsOne() {
         }
     
         @Test
    -    public void shouldCleanImpExtContextDataWhenFirstPartyDataNotPermittedForBidder() {
    +    public void shouldDeepCopyImpExtContextToEachImpressionAndNotRemoveDataForAllWhenDeprecatedOnlyOneBidder() {
             // given
    -        final ObjectNode impExt = mapper.createObjectNode().put("someBidder", 1).set("context",
    -                mapper.createObjectNode().put("data", "data").put("otherField", "value"));
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("prebid", mapper.createObjectNode()
    +                        .set("bidder", mapper.createObjectNode()
    +                                .put("someBidder", 1)
    +                                .put("deprecatedBidder", 2)))
    +                .set("context", mapper.createObjectNode()
    +                        .put("data", "data")
    +                        .put("otherField", "value"));
             final BidRequest bidRequest = givenBidRequest(singletonList(Imp.builder()
                             .id("impId")
                             .banner(Banner.builder()
                                     .format(singletonList(Format.builder().w(400).h(300).build()))
    -                                .build()).ext(impExt).build()),
    +                                .build())
    +                        .ext(impExt)
    +                        .build()),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .data(ExtRequestPrebidData.of(singletonList("otherBidder")))
    +                        .data(ExtRequestPrebidData.of(singletonList("someBidder"), null))
                             .build())));
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
    +                .willReturn(Future.succeededFuture(givenSeatBid(singletonList(
    +                        givenBid(Bid.builder().price(TEN).build())))));
    +
    +        given(fpdResolver.resolveImpExt(any(), eq(true))).willReturn(mapper.createObjectNode()
    +                .set("context", mapper.createObjectNode()
    +                        .put("data", "data")
    +                        .put("otherField", "value")));
    +        given(fpdResolver.resolveImpExt(any(), eq(false))).willReturn(mapper.createObjectNode()
    +                .set("context", mapper.createObjectNode()
    +                        .put("otherField", "value")));
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final BidRequest capturedRequest = captureBidRequest();
    -        assertThat(capturedRequest.getImp())
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester, times(2))
    +                .requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        assertThat(bidderRequestCaptor.getAllValues())
    +                .extracting(BidderRequest::getBidRequest)
    +                .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getExt)
                     .extracting(impExtNode -> impExtNode.get("context"))
    -                .containsOnly(mapper.createObjectNode().put("otherField", "value"));
    +                .containsOnly(
    +                        // data erased for deprecatedBidder
    +                        mapper.createObjectNode().put("otherField", "value"),
    +                        // data present for someBidder
    +                        mapper.createObjectNode().put("data", "data").put("otherField", "value"));
         }
     
         @Test
    -    public void shouldDeepCopyImpExtContextToEachImpressionAndNotRemoveDataForAllWhenDeprecatedOnlyOneBidder() {
    +    public void shouldPassImpExtFieldsToEachImpression() {
             // given
    -        final ObjectNode impExt = mapper.createObjectNode().put("someBidder", 1).put("deprecatedBidder", 2)
    -                .set("context", mapper.createObjectNode().put("data", "data").put("otherField", "value"));
    -        final BidRequest bidRequest = givenBidRequest(singletonList(Imp.builder()
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("prebid", mapper.createObjectNode()
    +                        .set("bidder", mapper.createObjectNode()
    +                                .put("someBidder", 1)))
    +                .put("all", "allValue");
    +
    +        final BidRequest bidRequest = givenBidRequest(
    +                singletonList(Imp.builder()
                             .id("impId")
                             .banner(Banner.builder()
                                     .format(singletonList(Format.builder().w(400).h(300).build()))
                                     .build()).ext(impExt).build()),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .data(ExtRequestPrebidData.of(singletonList("someBidder")))
    +                        .data(ExtRequestPrebidData.of(singletonList("someBidder"), null))
                             .build())));
    -        given(httpBidderRequester.requestBids(any(), any(), any(), anyBoolean()))
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
                     .willReturn(Future.succeededFuture(givenSeatBid(singletonList(
                             givenBid(Bid.builder().price(TEN).build())))));
     
    @@ -1218,17 +1533,49 @@ public void shouldDeepCopyImpExtContextToEachImpressionAndNotRemoveDataForAllWhe
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester, times(2)).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        assertThat(bidderRequestCaptor.getAllValues())
    +                .extracting(BidderRequest::getBidRequest)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .extracting(impExtNode -> impExtNode.get("all"))
    +                .containsOnly(new TextNode("allValue"));
    +    }
    +
    +    @Test
    +    public void shouldPassImpExtSkadnToEachImpression() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("prebid", mapper.createObjectNode()
    +                        .set("bidder", mapper.createObjectNode()
    +                                .put("someBidder", 1)))
    +                .put("skadn", "skadnValue");
    +        final BidRequest bidRequest = givenBidRequest(
    +                singletonList(Imp.builder()
    +                        .id("impId")
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder().w(400).h(300).build()))
    +                                .build())
    +                        .ext(impExt)
    +                        .build()),
    +                identity());
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
    +                .willReturn(Future.succeededFuture(givenSeatBid(singletonList(
    +                        givenBid(Bid.builder().price(TEN).build())))));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidRequestCaptor.capture(), any(), any(), anyBoolean());
             assertThat(bidRequestCaptor.getAllValues())
    +                .extracting(BidderRequest::getBidRequest)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getExt)
    -                .extracting(impExtNode -> impExtNode.get("context"))
    -                .containsOnly(
    -                        // data erased for deprecatedBidder
    -                        mapper.createObjectNode().put("otherField", "value"),
    -                        // data present for someBidder
    -                        mapper.createObjectNode().put("data", "data").put("otherField", "value"));
    +                .extracting(impExtNode -> impExtNode.get("skadn"))
    +                .containsOnly(new TextNode("skadnValue"));
         }
     
         @Test
    @@ -1246,7 +1593,7 @@ public void shouldSetUserBuyerIdsFromUserExtPrebidAndClearPrebidBuyerIdsAfterwar
                                             .build())
                                     .build())
                             .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                                .data(ExtRequestPrebidData.of(singletonList("someBidder")))
    +                                .data(ExtRequestPrebidData.of(singletonList("someBidder"), null))
                                     .build())));
     
             // when
    @@ -1264,7 +1611,7 @@ public void shouldCleanRequestExtPrebidData() {
             // given
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .data(ExtRequestPrebidData.of(asList("someBidder", "should_be_removed")))
    +                        .data(ExtRequestPrebidData.of(asList("someBidder", "should_be_removed"), null))
                             .aliases(singletonMap("someBidder", "alias_should_stay"))
                             .auctiontimestamp(1000L)
                             .build())));
    @@ -1281,23 +1628,63 @@ public void shouldCleanRequestExtPrebidData() {
         }
     
         @Test
    -    public void shouldPassUserExtDataOnlyForAllowedBidder() {
    +    public void shouldAddMultiBidInfoAboutRequestedBidderIfDataShouldNotBeSuppressed() {
             // given
    -        final Bidder bidder = mock(Bidder.class);
    -        givenBidder("someBidder", bidder, givenEmptySeatBid());
    -        givenBidder("missingBidder", bidder, givenEmptySeatBid());
    -
    -        final ObjectNode dataNode = mapper.createObjectNode().put("data", "value");
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)),
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .multibid(Collections.singletonList(
    +                                ExtRequestPrebidMultiBid.of("someBidder", null, 3, "prefix")))
    +                        .build())));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        final ExtRequest extRequest = captureBidRequest().getExt();
    +        assertThat(extRequest)
    +                .extracting(ExtRequest::getPrebid)
    +                .flatExtracting("multibid")
    +                .containsExactly(ExtRequestPrebidMultiBid.of("someBidder", null, 3, "prefix"));
    +    }
    +
    +    @Test
    +    public void shouldAddMultibidInfoOnlyAboutRequestedBidder() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)),
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .multibid(Collections.singletonList(
    +                                ExtRequestPrebidMultiBid.of(null, asList("someBidder", "anotherBidder"), 3, null)))
    +                        .build())));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        final ExtRequest extRequest = captureBidRequest().getExt();
    +        assertThat(extRequest)
    +                .extracting(ExtRequest::getPrebid)
    +                .flatExtracting("multibid")
    +                .containsExactly(ExtRequestPrebidMultiBid.of("someBidder", null, 3, null));
    +    }
    +
    +    @Test
    +    public void shouldPassUserDataAndExtDataOnlyForAllowedBidder() {
    +        // given
    +        final Bidder bidder = mock(Bidder.class);
    +        givenBidder("someBidder", bidder, givenEmptySeatBid());
    +        givenBidder("missingBidder", bidder, givenEmptySeatBid());
    +
    +        final ObjectNode dataNode = mapper.createObjectNode().put("data", "value");
             final Map bidderToGdpr = doubleMap("someBidder", 1, "missingBidder", 0);
    -        final ExtUserDigiTrust extUserDigiTrust = ExtUserDigiTrust.of("dId", 23, 222);
             final List eids = singletonList(ExtUserEid.of("eId", "id", emptyList(), null));
    -        final ExtUser extUser = ExtUser.builder().data(dataNode).digitrust(extUserDigiTrust).eids(eids).build();
    +        final ExtUser extUser = ExtUser.builder().data(dataNode).eids(eids).build();
    +        final List data = singletonList(Data.builder().build());
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr),
                     builder -> builder
                             .ext(ExtRequest.of(ExtRequestPrebid.builder()
                                     .auctiontimestamp(1000L)
    -                                .data(ExtRequestPrebidData.of(singletonList("someBidder")))
    +                                .data(ExtRequestPrebidData.of(singletonList("someBidder"), null))
                                     .build()))
                             .user(User.builder()
                                     .keywords("keyword")
    @@ -1305,23 +1692,186 @@ public void shouldPassUserExtDataOnlyForAllowedBidder() {
                                     .yob(133)
                                     .geo(Geo.EMPTY)
                                     .ext(extUser)
    +                                .data(data)
                                     .build()));
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester, times(2)).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester, times(2))
    +                .requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
     
    -        final ExtUser maskedExtUser = ExtUser.builder().digitrust(extUserDigiTrust).eids(eids).build();
    +        final ExtUser maskedExtUser = ExtUser.builder().eids(eids).build();
             assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(BidRequest::getUser)
    -                .extracting(User::getKeywords, User::getGender, User::getYob, User::getGeo, User::getExt)
    +                .extracting(User::getKeywords, User::getGender, User::getYob, User::getGeo, User::getExt, User::getData)
                     .containsOnly(
    -                        tuple("keyword", "male", 133, Geo.EMPTY, extUser),
    -                        tuple("keyword", "male", 133, Geo.EMPTY, maskedExtUser));
    +                        tuple("keyword", "male", 133, Geo.EMPTY, extUser, data),
    +                        tuple("keyword", "male", 133, Geo.EMPTY, maskedExtUser, null));
    +    }
    +
    +    @Test
    +    public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForSource() {
    +        testUserEidsPermissionFiltering(
    +                // given
    +                asList(
    +                        ExtUserEid.of("source1", null, null, null),
    +                        ExtUserEid.of("source2", null, null, null)),
    +                singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("otherBidder"))),
    +                emptyMap(),
    +                // expected
    +                singletonList(ExtUserEid.of("source2", null, null, null))
    +        );
    +    }
    +
    +    @Test
    +    public void shouldNotFilterUserExtEidsWhenEidsPermissionDoesNotContainSource() {
    +        testUserEidsPermissionFiltering(
    +                // given
    +                singletonList(ExtUserEid.of("source1", null, null, null)),
    +                singletonList(ExtRequestPrebidDataEidPermissions.of("source2", singletonList("otherBidder"))),
    +                emptyMap(),
    +                // expected
    +                singletonList(ExtUserEid.of("source1", null, null, null))
    +        );
    +    }
    +
    +    @Test
    +    public void shouldNotFilterUserExtEidsWhenSourceAllowedForAllBidders() {
    +        testUserEidsPermissionFiltering(
    +                // given
    +                singletonList(ExtUserEid.of("source1", null, null, null)),
    +                singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("*"))),
    +                emptyMap(),
    +                // expected
    +                singletonList(ExtUserEid.of("source1", null, null, null))
    +        );
    +    }
    +
    +    @Test
    +    public void shouldNotFilterUserExtEidsWhenSourceAllowedForBidder() {
    +        testUserEidsPermissionFiltering(
    +                // given
    +                singletonList(ExtUserEid.of("source1", null, null, null)),
    +                singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("someBidder"))),
    +                emptyMap(),
    +                // expected
    +                singletonList(ExtUserEid.of("source1", null, null, null))
    +        );
    +    }
    +
    +    @Test
    +    public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForSourceAndSetNullIfNoEidsLeft() {
    +        // given
    +        final Bidder bidder = mock(Bidder.class);
    +        givenBidder("someBidder", bidder, givenEmptySeatBid());
    +        final Map bidderToGdpr = singletonMap("someBidder", 1);
    +        final ExtUser extUser = ExtUser.builder().data(mapper.createObjectNode())
    +                .eids(singletonList(ExtUserEid.of("source1", null, null, null))).build();
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr),
    +                builder -> builder
    +                        .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                                .data(ExtRequestPrebidData.of(null, singletonList(
    +                                        ExtRequestPrebidDataEidPermissions.of("source1",
    +                                                singletonList("otherBidder")))))
    +                                .build()))
    +                        .user(User.builder()
    +                                .ext(extUser)
    +                                .build()));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
    +        assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
    +                .extracting(BidRequest::getUser)
    +                .extracting(User::getExt)
    +                .extracting(ExtUser::getEids)
    +                .element(0)
    +                .isNull();
    +    }
    +
    +    @Test
    +    public void shouldFilterUserExtEidsWhenBidderPermissionsGivenToBidderAliasOnly() {
    +        // given
    +        final Bidder bidder = mock(Bidder.class);
    +        givenBidder("someBidder", bidder, givenEmptySeatBid());
    +        final Map bidderToGdpr = singletonMap("someBidder", 1);
    +        final ExtUser extUser = ExtUser.builder().data(mapper.createObjectNode())
    +                .eids(singletonList(ExtUserEid.of("source1", null, null, null))).build();
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr),
    +                builder -> builder
    +                        .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                                .aliases(singletonMap("someBidder", "someBidderAlias"))
    +                                .data(ExtRequestPrebidData.of(null, singletonList(
    +                                        ExtRequestPrebidDataEidPermissions.of("source1",
    +                                                singletonList("someBidderAlias")))))
    +                                .build()))
    +                        .user(User.builder()
    +                                .ext(extUser)
    +                                .build()));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
    +        assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
    +                .extracting(BidRequest::getUser)
    +                .extracting(User::getExt)
    +                .extracting(ExtUser::getEids)
    +                .element(0)
    +                .isNull();
    +    }
    +
    +    @Test
    +    public void shouldFilterUserExtEidsWhenPermissionsGivenToBidderButNotForAlias() {
    +        // given
    +        final Bidder bidder = mock(Bidder.class);
    +        givenBidder("someBidderAlias", bidder, givenEmptySeatBid());
    +        final Map bidderToGdpr = singletonMap("someBidderAlias", 1);
    +        final ExtUser extUser = ExtUser.builder().data(mapper.createObjectNode())
    +                .eids(singletonList(ExtUserEid.of("source1", null, null, null))).build();
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr),
    +                builder -> builder
    +                        .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                                .aliases(singletonMap("someBidder", "someBidderAlias"))
    +                                .data(ExtRequestPrebidData.of(null, singletonList(
    +                                        ExtRequestPrebidDataEidPermissions.of("source1",
    +                                                singletonList("someBidder")))))
    +                                .build()))
    +                        .user(User.builder()
    +                                .ext(extUser)
    +                                .build()));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
    +        assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
    +                .extracting(BidRequest::getUser)
    +                .extracting(User::getExt)
    +                .extracting(ExtUser::getEids)
    +                .element(0)
    +                .isNull();
         }
     
         @Test
    @@ -1338,7 +1888,7 @@ public void shouldNotCleanRequestExtPrebidDataWhenFpdAllowedAndPrebidIsNotNull()
                     builder -> builder
                             .ext(ExtRequest.of(ExtRequestPrebid.builder()
                                     .auctiontimestamp(1000L)
    -                                .data(ExtRequestPrebidData.of(singletonList("someBidder")))
    +                                .data(ExtRequestPrebidData.of(singletonList("someBidder"), null))
                                     .build()))
                             .user(User.builder()
                                     .ext(extUser)
    @@ -1348,10 +1898,11 @@ public void shouldNotCleanRequestExtPrebidDataWhenFpdAllowedAndPrebidIsNotNull()
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
             assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(BidRequest::getUser)
                     .extracting(User::getExt)
                     .containsOnly(ExtUser.builder().data(dataNode).build());
    @@ -1366,14 +1917,13 @@ public void shouldMaskUserExtIfDataBiddersListIsEmpty() {
     
             final ObjectNode dataNode = mapper.createObjectNode().put("data", "value");
             final Map bidderToGdpr = doubleMap("someBidder", 1, "missingBidder", 0);
    -        final ExtUserDigiTrust extUserDigiTrust = ExtUserDigiTrust.of("dId", 23, 222);
             final List eids = singletonList(ExtUserEid.of("eId", "id", emptyList(), null));
    -        final ExtUser extUser = ExtUser.builder().data(dataNode).digitrust(extUserDigiTrust).eids(eids).build();
    +        final ExtUser extUser = ExtUser.builder().data(dataNode).eids(eids).build();
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr),
                     builder -> builder
                             .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                                .data(ExtRequestPrebidData.of(emptyList())).build()))
    +                                .data(ExtRequestPrebidData.of(emptyList(), null)).build()))
                             .user(User.builder()
                                     .keywords("keyword")
                                     .gender("male")
    @@ -1386,12 +1936,14 @@ public void shouldMaskUserExtIfDataBiddersListIsEmpty() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester, times(2)).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester, times(2))
    +                .requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
     
    -        final ExtUser expectedExtUser = ExtUser.builder().digitrust(extUserDigiTrust).eids(eids).build();
    +        final ExtUser expectedExtUser = ExtUser.builder().eids(eids).build();
             assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(BidRequest::getUser)
                     .extracting(User::getKeywords, User::getGender, User::getYob, User::getGeo, User::getExt)
                     .containsOnly(
    @@ -1412,7 +1964,8 @@ public void shouldNoMaskUserExtIfDataBiddersListIsNull() {
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr),
                     builder -> builder
                             .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                                .data(ExtRequestPrebidData.of(null)).build()))
    +                                .auctiontimestamp(1000L)
    +                                .data(ExtRequestPrebidData.of(null, null)).build()))
                             .user(User.builder()
                                     .keywords("keyword")
                                     .gender("male")
    @@ -1425,11 +1978,13 @@ public void shouldNoMaskUserExtIfDataBiddersListIsNull() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester, times(2)).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester, times(2))
    +                .requestBids(any(), bidRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidRequestCaptor.getAllValues();
     
             assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(BidRequest::getUser)
                     .extracting(User::getKeywords, User::getGender, User::getYob, User::getGeo, User::getExt)
                     .containsOnly(
    @@ -1440,7 +1995,7 @@ public void shouldNoMaskUserExtIfDataBiddersListIsNull() {
         }
     
         @Test
    -    public void shouldPassSiteExtDataOnlyForAllowedBidder() {
    +    public void shouldPassSiteContentDataAndExtDataOnlyForAllowedBidder() {
             // given
             final Bidder bidder = mock(Bidder.class);
             givenBidder("someBidder", bidder, givenEmptySeatBid());
    @@ -1448,31 +2003,48 @@ public void shouldPassSiteExtDataOnlyForAllowedBidder() {
     
             final ObjectNode dataNode = mapper.createObjectNode().put("data", "value");
             final Map bidderToGdpr = doubleMap("someBidder", 1, "missingBidder", 0);
    +        final Content content = Content.builder()
    +                .data(singletonList(Data.builder().build()))
    +                .album("album")
    +                .build();
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
                             .auctiontimestamp(1000L)
    -                        .data(ExtRequestPrebidData.of(singletonList("someBidder"))).build()))
    +                        .data(ExtRequestPrebidData.of(singletonList("someBidder"), null)).build()))
                             .site(Site.builder()
                                     .keywords("keyword")
                                     .search("search")
                                     .ext(ExtSite.of(0, dataNode))
    +                                .content(content)
                                     .build()));
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester, times(2)).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester, times(2))
    +                .requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
     
             assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(BidRequest::getSite)
    -                .extracting(Site::getKeywords, Site::getSearch, Site::getExt)
    +                .extracting(Site::getKeywords, Site::getSearch, Site::getExt, Site::getContent)
                     .containsOnly(
    -                        tuple("keyword", "search", ExtSite.of(0, dataNode)),
    -                        tuple("keyword", "search", ExtSite.of(0, null)));
    +                        tuple(
    +                                "keyword",
    +                                "search",
    +                                ExtSite.of(0, dataNode),
    +                                content),
    +                        tuple(
    +                                "keyword",
    +                                "search",
    +                                ExtSite.of(0, null),
    +                                Content.builder()
    +                                        .album("album")
    +                                        .build()));
         }
     
         @Test
    @@ -1487,7 +2059,8 @@ public void shouldNoMaskPassAppExtAndKeywordsWhenDataBiddersListIsNull() {
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                        .data(ExtRequestPrebidData.of(null)).build()))
    +                        .data(ExtRequestPrebidData.of(null, null))
    +                        .auctiontimestamp(1000L).build()))
                             .app(App.builder()
                                     .keywords("keyword")
                                     .ext(ExtApp.of(null, dataNode))
    @@ -1497,11 +2070,13 @@ public void shouldNoMaskPassAppExtAndKeywordsWhenDataBiddersListIsNull() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester, times(2)).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester, times(2))
    +                .requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
     
             assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(BidRequest::getApp)
                     .extracting(App::getExt, App::getKeywords)
                     .containsOnly(
    @@ -1518,57 +2093,37 @@ public void shouldPassAppExtDataOnlyForAllowedBidder() {
     
             final ObjectNode dataNode = mapper.createObjectNode().put("data", "value");
             final Map bidderToGdpr = doubleMap("someBidder", 1, "missingBidder", 0);
    +        final Content content = Content.builder()
    +                .data(singletonList(Data.builder().build()))
    +                .album("album")
    +                .build();
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr),
    -                builder -> builder
    -                        .ext(ExtRequest.of(ExtRequestPrebid.builder()
    -                                .data(ExtRequestPrebidData.of(singletonList("someBidder")))
    -                                .auctiontimestamp(1000L)
    -                                .build()))
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .data(ExtRequestPrebidData.of(singletonList("someBidder"), null))
    +                        .auctiontimestamp(1000L).build()))
                             .app(App.builder()
                                     .keywords("keyword")
                                     .ext(ExtApp.of(null, dataNode))
    +                                .content(content)
                                     .build()));
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester, times(2)).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester, times(2))
    +                .requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
     
             assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(BidRequest::getApp)
    -                .extracting(App::getExt, App::getKeywords)
    +                .extracting(App::getExt, App::getKeywords, App::getContent)
                     .containsOnly(
    -                        tuple(ExtApp.of(null, dataNode), "keyword"),
    -                        tuple(null, "keyword"));
    -    }
    -
    -    @Test
    -    public void shouldRejectRequestWhenAppAndSiteAppearsTogetherAfterFpdMerge() {
    -        // given
    -        final Bidder bidder = mock(Bidder.class);
    -        givenBidder("someBidder", bidder, givenEmptySeatBid());
    -        final ObjectNode bidderConfigApp = mapper.valueToTree(App.builder().id("appFromConfig").build());
    -        final ExtBidderConfig extBidderConfig = ExtBidderConfig.of(
    -                ExtBidderConfigFpd.of(null, bidderConfigApp, null));
    -        final ExtRequestPrebidBidderConfig extRequestPrebidBidderConfig = ExtRequestPrebidBidderConfig.of(
    -                singletonList("someBidder"), extBidderConfig);
    -        final Site requestSite = Site.builder().id("erased").domain("domain").build();
    -        final ExtRequest extRequest = ExtRequest.of(
    -                ExtRequestPrebid.builder()
    -                        .bidderconfig(singletonList(extRequestPrebidBidderConfig))
    -                        .build());
    -        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)),
    -                builder -> builder.site(requestSite).ext(extRequest));
    -
    -        // when
    -        exchangeService.holdAuction(givenRequestContext(bidRequest));
    -
    -        // then
    -        verifyZeroInteractions(httpBidderRequester);
    +                        tuple(ExtApp.of(null, dataNode), "keyword", content),
    +                        tuple(null, "keyword", Content.builder().album("album").build()));
         }
     
         @Test
    @@ -1579,12 +2134,12 @@ public void shouldUseConcreteOverGeneralSiteWithExtPrebidBidderConfig() {
     
             final ObjectNode siteWithPage = mapper.valueToTree(Site.builder().page("testPage").build());
             final ExtBidderConfig extBidderConfig = ExtBidderConfig.of(
    -                ExtBidderConfigFpd.of(siteWithPage, null, null));
    +                null, ExtBidderConfigOrtb.of(siteWithPage, null, null));
             final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of(
                     singletonList("someBidder"), extBidderConfig);
             final ObjectNode siteWithDomain = mapper.valueToTree(Site.builder().domain("notUsed").build());
             final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of(
    -                ExtBidderConfigFpd.of(siteWithDomain, null, null));
    +                null, ExtBidderConfigOrtb.of(siteWithDomain, null, null));
             final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"),
                     allExtBidderConfig);
     
    @@ -1607,11 +2162,12 @@ public void shouldUseConcreteOverGeneralSiteWithExtPrebidBidderConfig() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
     
             assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(BidRequest::getSite)
                     .containsOnly(mergedSite);
         }
    @@ -1625,7 +2181,7 @@ public void shouldUseConcreteOverGeneralAppWithExtPrebidBidderConfig() {
             final Publisher publisherWithId = Publisher.builder().id("testId").build();
             final ObjectNode appWithPublisherId = mapper.valueToTree(App.builder().publisher(publisherWithId).build());
             final ExtBidderConfig extBidderConfig = ExtBidderConfig.of(
    -                ExtBidderConfigFpd.of(null, appWithPublisherId, null));
    +                null, ExtBidderConfigOrtb.of(null, appWithPublisherId, null));
             final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of(
                     singletonList("someBidder"), extBidderConfig);
     
    @@ -1633,7 +2189,7 @@ public void shouldUseConcreteOverGeneralAppWithExtPrebidBidderConfig() {
             final ObjectNode appWithUpdatedPublisher = mapper.valueToTree(
                     App.builder().publisher(publisherWithIdAndDomain).build());
             final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of(
    -                ExtBidderConfigFpd.of(null, appWithUpdatedPublisher, null));
    +                null, ExtBidderConfigOrtb.of(null, appWithUpdatedPublisher, null));
             final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"),
                     allExtBidderConfig);
     
    @@ -1654,11 +2210,12 @@ public void shouldUseConcreteOverGeneralAppWithExtPrebidBidderConfig() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
     
             assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(BidRequest::getApp)
                     .containsOnly(mergedApp);
         }
    @@ -1670,13 +2227,13 @@ public void shouldUseConcreteOverGeneralUserWithExtPrebidBidderConfig() {
             givenBidder("someBidder", bidder, givenEmptySeatBid());
             final ObjectNode bidderConfigUser = mapper.valueToTree(User.builder().id("userFromConfig").build());
             final ExtBidderConfig extBidderConfig = ExtBidderConfig.of(
    -                ExtBidderConfigFpd.of(null, null, bidderConfigUser));
    +                null, ExtBidderConfigOrtb.of(null, null, bidderConfigUser));
             final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of(
                     singletonList("someBidder"), extBidderConfig);
     
             final ObjectNode emptyUser = mapper.valueToTree(User.builder().build());
             final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of(
    -                ExtBidderConfigFpd.of(null, null, emptyUser));
    +                null, ExtBidderConfigOrtb.of(null, null, emptyUser));
             final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"),
                     allExtBidderConfig);
             final User requestUser = User.builder().id("erased").buyeruid("testBuyerId").build();
    @@ -1695,11 +2252,12 @@ public void shouldUseConcreteOverGeneralUserWithExtPrebidBidderConfig() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        final List capturedBidRequests = bidRequestCaptor.getAllValues();
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
     
             assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
                     .extracting(BidRequest::getUser)
                     .containsOnly(mergedUser);
         }
    @@ -1738,6 +2296,28 @@ public void shouldCreateUserIfMissingInRequestAndBuyeridPresentInCookie() {
             assertThat(capturedUser).isEqualTo(User.builder().buyeruid("buyerid").build());
         }
     
    +    @Test
    +    public void shouldRemoveSiteIfBothSiteAndAppPresent() {
    +        // given
    +        givenBidder(givenEmptySeatBid());
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)),
    +                bidRequestBuilder -> bidRequestBuilder
    +                        .site(Site.builder().build())
    +                        .app(App.builder().build()));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        final BidRequest captureBidRequest = captureBidRequest();
    +        assertThat(captureBidRequest)
    +                .extracting(BidRequest::getSite)
    +                .containsNull();
    +        assertThat(captureBidRequest)
    +                .extracting(BidRequest::getApp)
    +                .doesNotContainNull();
    +    }
    +
         @Test
         public void shouldPassGlobalTimeoutToConnectorUnchangedIfCachingIsNotRequested() {
             // given
    @@ -1749,7 +2329,7 @@ public void shouldPassGlobalTimeoutToConnectorUnchangedIfCachingIsNotRequested()
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        verify(httpBidderRequester).requestBids(any(), any(), same(timeout), anyBoolean());
    +        verify(httpBidderRequester).requestBids(any(), any(), same(timeout), any(), anyBoolean());
         }
     
         @Test
    @@ -1761,15 +2341,19 @@ public void shouldPassReducedGlobalTimeoutToConnectorAndOriginalToBidResponseCre
                     storedResponseProcessor,
                     privacyEnforcementService,
                     fpdResolver,
    +                schainResolver,
                     httpBidderRequester,
                     responseBidValidator,
                     currencyService,
                     bidResponseCreator,
                     bidResponsePostProcessor,
    +                hookStageExecutor,
    +                applicationEventService,
                     httpInteractionLogger,
                     metrics,
                     clock,
    -                jacksonMapper);
    +                jacksonMapper,
    +                criteriaLogManager);
     
             final Bid bid = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(5.67)).build();
             givenBidder(givenSeatBid(singletonList(givenBid(bid))));
    @@ -1788,9 +2372,9 @@ public void shouldPassReducedGlobalTimeoutToConnectorAndOriginalToBidResponseCre
     
             // then
             final ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Timeout.class);
    -        verify(httpBidderRequester).requestBids(any(), any(), timeoutCaptor.capture(), anyBoolean());
    +        verify(httpBidderRequester).requestBids(any(), any(), timeoutCaptor.capture(), any(), anyBoolean());
             assertThat(timeoutCaptor.getValue().remaining()).isEqualTo(400L);
    -        verify(bidResponseCreator).create(anyList(), any(), any(), anyBoolean());
    +        verify(bidResponseCreator).create(anyList(), any(), any(), any());
         }
     
         @Test
    @@ -1798,13 +2382,13 @@ public void shouldReturnBidsWithUpdatedPriceCurrencyConversion() {
             // given
             final Bidder bidder = mock(Bidder.class);
             givenBidder("bidder", bidder, givenSeatBid(singletonList(
    -                givenBid(Bid.builder().price(BigDecimal.valueOf(2.0)).build()))));
    +                givenBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()))));
     
             final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
                     identity());
     
             final BigDecimal updatedPrice = BigDecimal.valueOf(5.0);
    -        given(currencyService.convertCurrency(any(), any(), any(), any(), anyBoolean())).willReturn(updatedPrice);
    +        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice);
     
             givenBidResponseCreator(singletonList(Bid.builder().price(updatedPrice).build()));
     
    @@ -1822,13 +2406,13 @@ public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() {
             // given
             final Bidder bidder = mock(Bidder.class);
             givenBidder("bidder", bidder, givenSeatBid(singletonList(
    -                givenBid(Bid.builder().price(BigDecimal.ONE).build()))));
    +                givenBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build()))));
     
             final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
                     identity());
     
             // returns the same price as in argument
    -        given(currencyService.convertCurrency(any(), any(), any(), any(), anyBoolean()))
    +        given(currencyService.convertCurrency(any(), any(), any(), any()))
                     .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
     
             // when
    @@ -1851,20 +2435,20 @@ public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() {
             final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
                     identity());
     
    -        given(currencyService.convertCurrency(any(), any(), any(), any(), any()))
    -                .willThrow(new PreBidException("no currency conversion available"));
    +        given(currencyService.convertCurrency(any(), any(), any(), any()))
    +                .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD"));
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
             final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class);
    -        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean());
    +        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any());
     
             assertThat(argumentCaptor.getValue()).hasSize(1);
     
    -        final BidderError expectedError = BidderError.generic("Unable to covert bid currency CUR to desired ad"
    -                + " server currency USD. no currency conversion available");
    +        final BidderError expectedError =
    +                BidderError.generic("Unable to convert bid currency CUR to desired ad server currency USD");
             final BidderSeatBid firstSeatBid = argumentCaptor.getValue().get(0).getSeatBid();
             assertThat(firstSeatBid.getBids()).isEmpty();
             assertThat(firstSeatBid.getErrors()).containsOnly(expectedError);
    @@ -1876,16 +2460,19 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor()
             // given
             final Bidder bidder = mock(Bidder.class);
             givenBidder("bidder", bidder, givenSeatBid(singletonList(
    -                givenBid(Bid.builder().price(BigDecimal.valueOf(2.0)).build()))));
    +                givenBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()))));
    +
    +        final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder().build();
    +        givenAdjustments.addFactor("bidder", BigDecimal.valueOf(10));
     
             final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
                             .aliases(emptyMap())
    -                        .bidadjustmentfactors(singletonMap("bidder", BigDecimal.valueOf(10)))
    +                        .bidadjustmentfactors(givenAdjustments)
                             .auctiontimestamp(1000L)
                             .build())));
     
    -        given(currencyService.convertCurrency(any(), any(), any(), any(), any()))
    +        given(currencyService.convertCurrency(any(), any(), any(), any()))
                     .willReturn(BigDecimal.valueOf(10));
     
             // when
    @@ -1893,7 +2480,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor()
     
             // then
             final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class);
    -        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean());
    +        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any());
     
             assertThat(argumentCaptor.getValue()).hasSize(1);
     
    @@ -1913,31 +2500,35 @@ public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForS
             final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0);
             final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0);
             givenBidder("bidder", mock(Bidder.class), givenSeatBid(asList(
    -                givenBid(Bid.builder().price(firstBidderPrice).build(), "CUR1"),
    -                givenBid(Bid.builder().price(secondBidderPrice).build(), "CUR2"))));
    +                givenBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"),
    +                givenBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2"))));
     
             final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
                     identity());
     
             final BigDecimal updatedPrice = BigDecimal.valueOf(10.0);
    -        given(currencyService.convertCurrency(any(), any(), any(), any(), any())).willReturn(updatedPrice)
    -                .willThrow(new PreBidException("no currency conversion available"));
    +        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice)
    +                .willThrow(
    +                        new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD"));
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
             final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class);
    -        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean());
    -        verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(null), any(), eq("CUR1"), eq(null));
    -        verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(null), any(), eq("CUR2"), eq(null));
    +        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any());
    +        verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), any(), eq("CUR1"));
    +        verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), any(), eq("CUR2"));
     
             assertThat(argumentCaptor.getValue()).hasSize(1);
     
    -        final Bid expectedBid = Bid.builder().price(updatedPrice).build();
    +        final ObjectNode expectedBidExt = mapper.createObjectNode();
    +        expectedBidExt.put("origbidcpm", new BigDecimal("2.0"));
    +        expectedBidExt.put("origbidcur", "CUR1");
    +        final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build();
             final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR1");
    -        final BidderError expectedError = BidderError.generic("Unable to covert bid currency CUR2 to desired ad"
    -                + " server currency USD. no currency conversion available");
    +        final BidderError expectedError =
    +                BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD");
     
             final BidderSeatBid firstSeatBid = argumentCaptor.getValue().get(0).getSeatBid();
             assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid);
    @@ -1951,39 +2542,42 @@ public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupported
             final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0);
             final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0);
             givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList(
    -                givenBid(Bid.builder().price(firstBidderPrice).build(), "USD"))));
    +                givenBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD"))));
             givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList(
    -                givenBid(Bid.builder().price(BigDecimal.valueOf(10.0)).build(), "CUR"))));
    +                givenBid(Bid.builder().impid("impId2").price(BigDecimal.valueOf(10.0)).build(), "CUR"))));
     
             final BidRequest bidRequest = BidRequest.builder().cur(singletonList("BAD"))
                     .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3),
                             identity()))).build();
     
             final BigDecimal updatedPrice = BigDecimal.valueOf(20);
    -        given(currencyService.convertCurrency(any(), any(), any(), any(), any())).willReturn(updatedPrice);
    -        given(currencyService.convertCurrency(any(), any(), eq("BAD"), eq("CUR"), any()))
    -                .willThrow(new PreBidException("no currency conversion available"));
    +        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice);
    +        given(currencyService.convertCurrency(any(), any(), eq("BAD"), eq("CUR")))
    +                .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency BAD"));
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
             final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class);
    -        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean());
    -        verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(null), eq("BAD"), eq("USD"), eq(null));
    -        verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(null), eq("BAD"), eq("CUR"), eq(null));
    +        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any());
    +        verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("BAD"), eq("USD"));
    +        verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("BAD"), eq("CUR"));
     
             assertThat(argumentCaptor.getValue()).hasSize(2);
     
    -        final Bid expectedBid = Bid.builder().price(updatedPrice).build();
    +        final ObjectNode expectedBidExt = mapper.createObjectNode();
    +        expectedBidExt.put("origbidcpm", new BigDecimal("2.0"));
    +        expectedBidExt.put("origbidcur", "USD");
    +        final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build();
             final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "USD");
             assertThat(argumentCaptor.getValue())
                     .extracting(BidderResponse::getSeatBid)
                     .flatExtracting(BidderSeatBid::getBids)
                     .containsOnly(expectedBidderBid);
     
    -        final BidderError expectedError = BidderError.generic("Unable to covert bid currency CUR to desired ad"
    -                + " server currency BAD. no currency conversion available");
    +        final BidderError expectedError =
    +                BidderError.generic("Unable to convert bid currency CUR to desired ad server currency BAD");
             assertThat(argumentCaptor.getValue())
                     .extracting(BidderResponse::getSeatBid)
                     .flatExtracting(BidderSeatBid::getErrors)
    @@ -1996,22 +2590,22 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCu
             // given
             final BigDecimal bidderPrice = BigDecimal.valueOf(2.0);
             givenBidder("bidder", mock(Bidder.class), givenSeatBid(singletonList(
    -                givenBid(Bid.builder().price(bidderPrice).build(), "USD"))));
    +                givenBid(Bid.builder().impid("impId").price(bidderPrice).build(), "USD"))));
     
             final BidRequest bidRequest = givenBidRequest(
                     singletonList(givenImp(singletonMap("bidder", 2), identity())),
                     builder -> builder.cur(asList("CUR1", "CUR2", "CUR2")));
     
             final BigDecimal updatedPrice = BigDecimal.valueOf(10.0);
    -        given(currencyService.convertCurrency(any(), any(), any(), any(), any())).willReturn(updatedPrice);
    +        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice);
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
             final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class);
    -        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean());
    -        verify(currencyService).convertCurrency(eq(bidderPrice), eq(null), eq("CUR1"), eq("USD"), eq(null));
    +        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any());
    +        verify(currencyService).convertCurrency(eq(bidderPrice), eq(bidRequest), eq("CUR1"), eq("USD"));
     
             assertThat(argumentCaptor.getValue()).hasSize(1);
     
    @@ -2033,11 +2627,11 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() {
             final BigDecimal bidder2Price = BigDecimal.valueOf(2);
             final BigDecimal bidder3Price = BigDecimal.valueOf(3);
             givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList(
    -                givenBid(Bid.builder().price(bidder1Price).build(), "EUR"))));
    +                givenBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR"))));
             givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList(
    -                givenBid(Bid.builder().price(bidder2Price).build(), "GBP"))));
    +                givenBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP"))));
             givenBidder("bidder3", mock(Bidder.class), givenSeatBid(singletonList(
    -                givenBid(Bid.builder().price(bidder3Price).build(), "USD"))));
    +                givenBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD"))));
     
             final Map impBidders = new HashMap<>();
             impBidders.put("bidder1", 1);
    @@ -2047,18 +2641,18 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() {
                     singletonList(givenImp(impBidders, identity())), builder -> builder.cur(singletonList("USD")));
     
             final BigDecimal updatedPrice = BigDecimal.valueOf(10.0);
    -        given(currencyService.convertCurrency(any(), any(), any(), any(), any())).willReturn(updatedPrice);
    -        given(currencyService.convertCurrency(any(), any(), any(), eq("USD"), any())).willReturn(bidder3Price);
    +        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice);
    +        given(currencyService.convertCurrency(any(), any(), any(), eq("USD"))).willReturn(bidder3Price);
     
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
             final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class);
    -        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean());
    -        verify(currencyService).convertCurrency(eq(bidder1Price), eq(null), eq("USD"), eq("EUR"), eq(null));
    -        verify(currencyService).convertCurrency(eq(bidder2Price), eq(null), eq("USD"), eq("GBP"), eq(null));
    -        verify(currencyService).convertCurrency(eq(bidder3Price), eq(null), eq("USD"), eq("USD"), eq(null));
    +        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any());
    +        verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("USD"), eq("EUR"));
    +        verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("USD"), eq("GBP"));
    +        verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD"));
             verifyNoMoreInteractions(currencyService);
     
             assertThat(argumentCaptor.getValue())
    @@ -2076,7 +2670,7 @@ public void shouldNotAddExtPrebidEventsWhenEventsServiceReturnsEmptyEventsServic
             final BigDecimal price = BigDecimal.valueOf(2.0);
             givenBidder(BidderSeatBid.of(
                     singletonList(BidderBid.of(
    -                        Bid.builder().id("bidId").price(price)
    +                        Bid.builder().id("bidId").impid("impId").price(price)
                                     .ext(mapper.valueToTree(singletonMap("bidExt", 1))).build(), banner, null)),
                     emptyList(),
                     emptyList()));
    @@ -2091,16 +2685,16 @@ public void shouldNotAddExtPrebidEventsWhenEventsServiceReturnsEmptyEventsServic
             // then
             assertThat(bidResponse.getSeatbid()).hasSize(1)
                     .flatExtracting(SeatBid::getBid)
    -                .extracting(bid -> toExtPrebid(bid.getExt()).getPrebid().getEvents())
    +                .extracting(bid -> toExtBidPrebid(bid.getExt()).getEvents())
                     .containsNull();
         }
     
         @Test
         public void shouldIncrementCommonMetrics() {
             // given
    -        given(httpBidderRequester.requestBids(any(), any(), any(), anyBoolean()))
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
                     .willReturn(Future.succeededFuture(givenSeatBid(singletonList(
    -                        givenBid(Bid.builder().price(TEN).build())))));
    +                        givenBid(Bid.builder().impid("impId").price(TEN).build())))));
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someAlias", 1)),
                     builder -> builder
    @@ -2113,6 +2707,7 @@ public void shouldIncrementCommonMetrics() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    +        verify(metrics).updateRequestBidderCardinalityMetric(1);
             verify(metrics).updateAccountRequestMetrics(eq("accountId"), eq(MetricName.openrtb2web));
             verify(metrics)
                     .updateAdapterRequestTypeAndNoCookieMetrics(eq("someBidder"), eq(MetricName.openrtb2web), eq(true));
    @@ -2124,11 +2719,9 @@ public void shouldIncrementCommonMetrics() {
         @Test
         public void shouldCallUpdateCookieMetricsWithExpectedValue() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someAlias", 1)),
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)),
                     builder -> builder.app(App.builder().build()));
     
    -        given(bidderCatalog.nameByAlias("someAlias")).willReturn("someBidder");
    -
             // when
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
    @@ -2140,7 +2733,7 @@ public void shouldCallUpdateCookieMetricsWithExpectedValue() {
         @Test
         public void shouldUseEmptyStringIfPublisherIdIsEmpty() {
             // given
    -        given(httpBidderRequester.requestBids(any(), any(), any(), anyBoolean()))
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
                     .willReturn(Future.succeededFuture(givenSeatBid(singletonList(
                             givenBid(Bid.builder().price(TEN).build())))));
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)));
    @@ -2156,7 +2749,7 @@ public void shouldUseEmptyStringIfPublisherIdIsEmpty() {
         @Test
         public void shouldIncrementNoBidRequestsMetric() {
             // given
    -        given(httpBidderRequester.requestBids(any(), any(), any(), anyBoolean()))
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
                     .willReturn(Future.succeededFuture(givenSeatBid(emptyList())));
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)));
    @@ -2171,9 +2764,9 @@ public void shouldIncrementNoBidRequestsMetric() {
         @Test
         public void shouldIncrementGotBidsAndErrorMetricsIfBidderReturnsBidAndDifferentErrors() {
             // given
    -        given(httpBidderRequester.requestBids(any(), any(), any(), anyBoolean()))
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
                     .willReturn(Future.succeededFuture(BidderSeatBid.of(
    -                        singletonList(givenBid(Bid.builder().price(TEN).build())),
    +                        singletonList(givenBid(Bid.builder().impid("impId").price(TEN).build())),
                             emptyList(),
                             asList(
                                     // two identical errors to verify corresponding metric is submitted only once
    @@ -2207,34 +2800,178 @@ public void shouldPassResponseToPostProcessor() {
             exchangeService.holdAuction(givenRequestContext(bidRequest));
     
             // then
    -        verify(bidResponsePostProcessor).postProcess(any(), same(uidsCookie), same(bidRequest), any(),
    -                eq(Account.builder().id("accountId").eventsEnabled(true).build()));
    +        verify(bidResponsePostProcessor).postProcess(
    +                any(),
    +                same(uidsCookie),
    +                same(bidRequest),
    +                any(),
    +                eq(Account.builder()
    +                        .id("accountId")
    +                        .auction(AccountAuctionConfig.builder()
    +                                .events(AccountEventsConfig.of(true))
    +                                .build())
    +                        .build()));
         }
     
         @Test
         public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() {
    +        // given
    +        final Bidder bidder = mock(Bidder.class);
    +        givenBidder("bidder", bidder, givenSeatBid(singletonList(
    +                givenBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).build()))));
    +
    +        final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder().build();
    +        givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468));
    +
    +        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .aliases(emptyMap())
    +                        .bidadjustmentfactors(givenAdjustments)
    +                        .auctiontimestamp(1000L)
    +                        .build())));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    +
    +        // then
    +        final List capturedBidResponses = captureBidResponses();
    +        assertThat(capturedBidResponses)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getBids)
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getPrice)
    +                .containsExactly(BigDecimal.valueOf(4.936));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() {
    +        // given
    +        final Bidder bidder = mock(Bidder.class);
    +        givenBidder("bidder", bidder, givenSeatBid(singletonList(
    +                BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null))));
    +
    +        final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder()
    +                .mediatypes(new EnumMap<>(Collections.singletonMap(BidAdjustmentMediaType.video,
    +                        Collections.singletonMap("bidder", BigDecimal.valueOf(3.456)))))
    +                .build();
    +
    +        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
    +                        impBuilder.id("123").video(Video.builder().placement(1).build()))),
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .aliases(emptyMap())
    +                        .bidadjustmentfactors(givenAdjustments)
    +                        .auctiontimestamp(1000L)
    +                        .build())));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    +
    +        // then
    +        final List capturedBidResponses = captureBidResponses();
    +        assertThat(capturedBidResponses)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getBids)
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getPrice)
    +                .containsExactly(BigDecimal.valueOf(6.912));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementIsMissing() {
    +        // given
    +        final Bidder bidder = mock(Bidder.class);
    +        givenBidder("bidder", bidder, givenSeatBid(singletonList(
    +                BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null))));
    +
    +        final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder()
    +                .mediatypes(new EnumMap<>(Collections.singletonMap(BidAdjustmentMediaType.video,
    +                        Collections.singletonMap("bidder", BigDecimal.valueOf(3.456)))))
    +                .build();
    +
    +        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
    +                        impBuilder.id("123").video(Video.builder().build()))),
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .aliases(emptyMap())
    +                        .bidadjustmentfactors(givenAdjustments)
    +                        .auctiontimestamp(1000L)
    +                        .build())));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    +
    +        // then
    +        final List capturedBidResponses = captureBidResponses();
    +        assertThat(capturedBidResponses)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getBids)
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getPrice)
    +                .containsExactly(BigDecimal.valueOf(6.912));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() {
             // given
             final Bidder bidder = mock(Bidder.class);
             givenBidder("bidder", bidder, givenSeatBid(singletonList(
                     givenBid(Bid.builder().price(BigDecimal.valueOf(2)).build()))));
     
    +        final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder()
    +                .mediatypes(new EnumMap<>(Collections.singletonMap(BidAdjustmentMediaType.banner,
    +                        Collections.singletonMap("bidder", BigDecimal.valueOf(3.456)))))
    +                .build();
    +
             final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
                             .aliases(emptyMap())
    -                        .bidadjustmentfactors(singletonMap("bidder", BigDecimal.valueOf(2.468)))
    +                        .bidadjustmentfactors(givenAdjustments)
                             .auctiontimestamp(1000L)
                             .build())));
     
    -        givenBidResponseCreator(singletonList(Bid.builder().price(BigDecimal.valueOf(4.936)).build()));
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    +
    +        // then
    +        final List capturedBidResponses = captureBidResponses();
    +        assertThat(capturedBidResponses)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getBids)
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getPrice)
    +                .containsExactly(BigDecimal.valueOf(6.912));
    +    }
    +
    +    @Test
    +    public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() {
    +        // given
    +        final Bidder bidder = mock(Bidder.class);
    +        givenBidder("bidder", bidder, givenSeatBid(singletonList(
    +                givenBid(Bid.builder().price(BigDecimal.valueOf(2)).build()))));
    +
    +        final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder()
    +                .mediatypes(new EnumMap<>(Collections.singletonMap(BidAdjustmentMediaType.banner,
    +                        Collections.singletonMap("bidder", BigDecimal.valueOf(3.456)))))
    +                .build();
    +        givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468));
    +
    +        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .aliases(emptyMap())
    +                        .bidadjustmentfactors(givenAdjustments)
    +                        .auctiontimestamp(1000L)
    +                        .build())));
     
             // when
    -        final BidResponse bidResponse = exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    +        exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
     
             // then
    -        assertThat(bidResponse.getSeatbid())
    -                .flatExtracting(SeatBid::getBid)
    +        final List capturedBidResponses = captureBidResponses();
    +        assertThat(capturedBidResponses)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getBids)
    +                .extracting(BidderBid::getBid)
                     .extracting(Bid::getPrice)
    -                .containsExactly(BigDecimal.valueOf(4.936));
    +                .containsExactly(BigDecimal.valueOf(6.912));
         }
     
         @Test
    @@ -2243,14 +2980,17 @@ public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresent
             final Bidder bidder = mock(Bidder.class);
     
             givenBidder("bidder", bidder, givenSeatBid(singletonList(
    -                givenBid(Bid.builder().price(BigDecimal.ONE).build()))));
    +                givenBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build()))));
    +
    +        final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder().build();
    +        givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN);
     
             final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
                     builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
                             .aliases(emptyMap())
                             .auctiontimestamp(1000L)
                             .currency(ExtRequestCurrency.of(null, false))
    -                        .bidadjustmentfactors(singletonMap("some-other-bidder", BigDecimal.TEN))
    +                        .bidadjustmentfactors(givenAdjustments)
                             .build())));
     
             // when
    @@ -2263,24 +3003,661 @@ public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresent
                     .containsExactly(BigDecimal.ONE);
         }
     
    +    @Test
    +    public void shouldReturnBidResponseModifiedByAuctionResponseHooks() {
    +        // given
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
    +                .willReturn(Future.succeededFuture(givenSeatBid(emptyList())));
    +
    +        doAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                false,
    +                AuctionResponsePayloadImpl.of(BidResponse.builder().id("bidResponseId").build()))))
    +                .when(hookStageExecutor).executeAuctionResponseStage(any(), any());
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2)));
    +
    +        // when
    +        final BidResponse bidResponse = exchangeService.holdAuction(givenRequestContext(bidRequest)).result();
    +
    +        // then
    +        assertThat(bidResponse).isEqualTo(BidResponse.builder().id("bidResponseId").build());
    +    }
    +
    +    @Test
    +    public void shouldReturnEmptyBidResponseWhenRequestIsRejected() {
    +        // given
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_auction))
    +                .debugContext(DebugContext.empty())
    +                .requestRejected(true)
    +                .build();
    +
    +        // when
    +        final Future result = exchangeService.holdAuction(auctionContext);
    +
    +        // then
    +        verifyZeroInteractions(storedResponseProcessor, httpBidderRequester, hookStageExecutor, bidResponseCreator);
    +        assertThat(result).succeededWith(BidResponse.builder()
    +                .seatbid(emptyList())
    +                .build());
    +    }
    +
    +    @Test
    +    public void shouldReturnBidResponseWithHooksDebugInfoWhenAuctionHappened() {
    +        // given
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
    +                .willReturn(Future.succeededFuture(givenSeatBid(emptyList())));
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2)));
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder()
    +                .hookExecutionContext(HookExecutionContext.of(
    +                        Endpoint.openrtb2_auction,
    +                        stageOutcomes()))
    +                .debugContext(DebugContext.of(true, null))
    +                .build();
    +
    +        // when
    +        final BidResponse bidResponse = exchangeService.holdAuction(auctionContext).result();
    +
    +        // then
    +        assertThat(bidResponse.getExt()).isNotNull();
    +        assertThat(bidResponse.getExt().getPrebid()).isNotNull();
    +        final ExtModules extModules = bidResponse.getExt().getPrebid().getModules();
    +        assertThat(extModules).isNotNull();
    +
    +        assertThat(extModules.getErrors())
    +                .hasSize(3)
    +                .hasEntrySatisfying("module1", moduleErrors -> assertThat(moduleErrors)
    +                        .hasSize(2)
    +                        .hasEntrySatisfying("hook1", hookErrors -> assertThat(hookErrors)
    +                                .containsOnly("error message 1-1 1", "error message 1-1 2"))
    +                        .hasEntrySatisfying("hook2", hookErrors -> assertThat(hookErrors)
    +                                .containsOnly(
    +                                        "error message 1-2 1",
    +                                        "error message 1-2 2",
    +                                        "error message 1-2 3",
    +                                        "error message 1-2 4")))
    +                .hasEntrySatisfying("module2", moduleErrors -> assertThat(moduleErrors)
    +                        .hasSize(1)
    +                        .hasEntrySatisfying("hook1", hookErrors -> assertThat(hookErrors)
    +                                .containsOnly("error message 2-1 1", "error message 2-1 2")))
    +                .hasEntrySatisfying("module3", moduleErrors -> assertThat(moduleErrors)
    +                        .hasSize(1)
    +                        .hasEntrySatisfying("hook1", hookErrors -> assertThat(hookErrors)
    +                                .containsOnly("error message 3-1 1", "error message 3-1 2")));
    +        assertThat(extModules.getWarnings())
    +                .hasSize(3)
    +                .hasEntrySatisfying("module1", moduleErrors -> assertThat(moduleErrors)
    +                        .hasSize(2)
    +                        .hasEntrySatisfying("hook1", hookErrors -> assertThat(hookErrors)
    +                                .containsOnly("warning message 1-1 1", "warning message 1-1 2"))
    +                        .hasEntrySatisfying("hook2", hookErrors -> assertThat(hookErrors)
    +                                .containsOnly(
    +                                        "warning message 1-2 1",
    +                                        "warning message 1-2 2",
    +                                        "warning message 1-2 3",
    +                                        "warning message 1-2 4")))
    +                .hasEntrySatisfying("module2", moduleErrors -> assertThat(moduleErrors)
    +                        .hasSize(1)
    +                        .hasEntrySatisfying("hook1", hookErrors -> assertThat(hookErrors)
    +                                .containsOnly("warning message 2-1 1", "warning message 2-1 2")))
    +                .hasEntrySatisfying("module3", moduleErrors -> assertThat(moduleErrors)
    +                        .hasSize(1)
    +                        .hasEntrySatisfying("hook1", hookErrors -> assertThat(hookErrors)
    +                                .containsOnly("warning message 3-1 1", "warning message 3-1 2")));
    +
    +        assertThat(extModules.getTrace()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidResponseWithHooksBasicTraceInfoWhenAuctionHappened() {
    +        // given
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
    +                .willReturn(Future.succeededFuture(givenSeatBid(emptyList())));
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2)));
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder()
    +                .hookExecutionContext(HookExecutionContext.of(
    +                        Endpoint.openrtb2_auction,
    +                        stageOutcomes()))
    +                .debugContext(DebugContext.of(false, TraceLevel.basic))
    +                .build();
    +
    +        // when
    +        final BidResponse bidResponse = exchangeService.holdAuction(auctionContext).result();
    +
    +        // then
    +        assertThat(bidResponse.getExt()).isNotNull();
    +        assertThat(bidResponse.getExt().getPrebid()).isNotNull();
    +        final ExtModules extModules = bidResponse.getExt().getPrebid().getModules();
    +        assertThat(extModules).isNotNull();
    +
    +        assertThat(extModules.getErrors()).isNull();
    +        assertThat(extModules.getWarnings()).isNull();
    +
    +        assertThat(extModules.getTrace()).isEqualTo(ExtModulesTrace.of(
    +                16L,
    +                asList(
    +                        ExtModulesTraceStage.of(
    +                                Stage.entrypoint,
    +                                12L,
    +                                singletonList(ExtModulesTraceStageOutcome.of(
    +                                        "http-request",
    +                                        12L,
    +                                        asList(
    +                                                ExtModulesTraceGroup.of(
    +                                                        6L,
    +                                                        asList(
    +                                                                ExtModulesTraceInvocationResult.builder()
    +                                                                        .hookId(HookId.of("module1", "hook1"))
    +                                                                        .executionTime(4L)
    +                                                                        .status(ExecutionStatus.success)
    +                                                                        .message("Message 1-1")
    +                                                                        .action(ExecutionAction.update)
    +                                                                        .build(),
    +                                                                ExtModulesTraceInvocationResult.builder()
    +                                                                        .hookId(HookId.of("module1", "hook2"))
    +                                                                        .executionTime(6L)
    +                                                                        .status(ExecutionStatus.invocation_failure)
    +                                                                        .message("Message 1-2")
    +                                                                        .build())),
    +                                                ExtModulesTraceGroup.of(
    +                                                        6L,
    +                                                        asList(
    +                                                                ExtModulesTraceInvocationResult.builder()
    +                                                                        .hookId(HookId.of("module1", "hook2"))
    +                                                                        .executionTime(4L)
    +                                                                        .status(ExecutionStatus.success)
    +                                                                        .message("Message 1-2")
    +                                                                        .action(ExecutionAction.no_action)
    +                                                                        .build(),
    +                                                                ExtModulesTraceInvocationResult.builder()
    +                                                                        .hookId(HookId.of("module2", "hook1"))
    +                                                                        .executionTime(6L)
    +                                                                        .status(ExecutionStatus.timeout)
    +                                                                        .message("Message 2-1")
    +                                                                        .build())))))),
    +                        ExtModulesTraceStage.of(
    +                                Stage.auction_response,
    +                                4L,
    +                                singletonList(ExtModulesTraceStageOutcome.of(
    +                                        "auction-response",
    +                                        4L,
    +                                        singletonList(
    +                                                ExtModulesTraceGroup.of(
    +                                                        4L,
    +                                                        asList(
    +                                                                ExtModulesTraceInvocationResult.builder()
    +                                                                        .hookId(HookId.of("module3", "hook1"))
    +                                                                        .executionTime(4L)
    +                                                                        .status(ExecutionStatus.success)
    +                                                                        .message("Message 3-1")
    +                                                                        .action(ExecutionAction.update)
    +                                                                        .build(),
    +                                                                ExtModulesTraceInvocationResult.builder()
    +                                                                        .hookId(HookId.of("module3", "hook2"))
    +                                                                        .executionTime(4L)
    +                                                                        .status(ExecutionStatus.success)
    +                                                                        .action(ExecutionAction.no_action)
    +                                                                        .build())))))))));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidResponseWithHooksVerboseTraceInfoWhenAuctionHappened() {
    +        // given
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
    +                .willReturn(Future.succeededFuture(givenSeatBid(emptyList())));
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2)));
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder()
    +                .hookExecutionContext(HookExecutionContext.of(
    +                        Endpoint.openrtb2_auction,
    +                        stageOutcomes()))
    +                .debugContext(DebugContext.of(false, TraceLevel.verbose))
    +                .build();
    +
    +        // when
    +        final BidResponse bidResponse = exchangeService.holdAuction(auctionContext).result();
    +
    +        // then
    +        assertThat(bidResponse.getExt().getPrebid().getModules().getTrace().getStages())
    +                .anySatisfy(stage -> assertThat(stage.getOutcomes())
    +                        .anySatisfy(outcome -> assertThat(outcome.getGroups())
    +                                .anySatisfy(group -> assertThat(group.getInvocationResults())
    +                                        .anySatisfy(hook -> {
    +                                            assertThat(hook.getDebugMessages())
    +                                                    .containsOnly("debug message 1-1 1", "debug message 1-1 2");
    +                                            assertThat(hook.getAnalyticsTags()).isEqualTo(
    +                                                    ExtModulesTraceAnalyticsTags.of(singletonList(
    +                                                            ExtModulesTraceAnalyticsActivity.of(
    +                                                                    "some-activity",
    +                                                                    "success",
    +                                                                    singletonList(ExtModulesTraceAnalyticsResult.of(
    +                                                                            "success",
    +                                                                            mapper.createObjectNode(),
    +                                                                            ExtModulesTraceAnalyticsAppliedTo.builder()
    +                                                                                    .impIds(asList("impId1", "impId2"))
    +                                                                                    .request(true)
    +                                                                                    .build()))))));
    +                                        }))));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidResponseWithHooksDebugAndTraceInfoWhenAuctionHappened() {
    +        // given
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
    +                .willReturn(Future.succeededFuture(givenSeatBid(emptyList())));
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2)));
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder()
    +                .hookExecutionContext(HookExecutionContext.of(
    +                        Endpoint.openrtb2_auction,
    +                        stageOutcomes()))
    +                .debugContext(DebugContext.of(true, TraceLevel.basic))
    +                .build();
    +
    +        // when
    +        final BidResponse bidResponse = exchangeService.holdAuction(auctionContext).result();
    +
    +        // then
    +        final ExtModules extModules = bidResponse.getExt().getPrebid().getModules();
    +
    +        assertThat(extModules.getErrors()).isNotEmpty();
    +        assertThat(extModules.getWarnings()).isNotEmpty();
    +        assertThat(extModules.getTrace()).isNotNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidResponseWithHooksDebugAndTraceInfoWhenRequestIsRejected() {
    +        // given
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .hookExecutionContext(HookExecutionContext.of(
    +                        Endpoint.openrtb2_auction,
    +                        stageOutcomes()))
    +                .debugContext(DebugContext.of(true, TraceLevel.basic))
    +                .requestRejected(true)
    +                .build();
    +
    +        // when
    +        final BidResponse bidResponse = exchangeService.holdAuction(auctionContext).result();
    +
    +        // then
    +        final ExtModules extModules = bidResponse.getExt().getPrebid().getModules();
    +
    +        assertThat(extModules.getErrors()).isNotEmpty();
    +        assertThat(extModules.getWarnings()).isNotEmpty();
    +        assertThat(extModules.getTrace()).isNotNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidResponseWithoutHooksTraceInfoWhenNoHooksExecuted() {
    +        // given
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
    +                .willReturn(Future.succeededFuture(givenSeatBid(emptyList())));
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2)));
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder()
    +                .hookExecutionContext(HookExecutionContext.of(
    +                        Endpoint.openrtb2_auction,
    +                        new EnumMap<>(singletonMap(
    +                                Stage.entrypoint,
    +                                singletonList(StageExecutionOutcome.of("http-request", emptyList()))))))
    +                .debugContext(DebugContext.of(false, TraceLevel.basic))
    +                .build();
    +
    +        // when
    +        final BidResponse bidResponse = exchangeService.holdAuction(auctionContext).result();
    +
    +        // then
    +        assertThat(bidResponse.getExt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldIncrementHooksGlobalMetrics() {
    +        // given
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .hookExecutionContext(HookExecutionContext.of(
    +                        Endpoint.openrtb2_auction,
    +                        stageOutcomes()))
    +                .debugContext(DebugContext.empty())
    +                .requestRejected(true)
    +                .build();
    +
    +        // when
    +        exchangeService.holdAuction(auctionContext);
    +
    +        // then
    +        verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any());
    +        verify(metrics).updateHooksMetrics(
    +                eq("module1"),
    +                eq(Stage.entrypoint),
    +                eq("hook1"),
    +                eq(ExecutionStatus.success),
    +                eq(4L),
    +                eq(ExecutionAction.update));
    +        verify(metrics).updateHooksMetrics(
    +                eq("module1"),
    +                eq(Stage.entrypoint),
    +                eq("hook2"),
    +                eq(ExecutionStatus.invocation_failure),
    +                eq(6L),
    +                isNull());
    +        verify(metrics).updateHooksMetrics(
    +                eq("module1"),
    +                eq(Stage.entrypoint),
    +                eq("hook2"),
    +                eq(ExecutionStatus.success),
    +                eq(4L),
    +                eq(ExecutionAction.no_action));
    +        verify(metrics).updateHooksMetrics(
    +                eq("module2"),
    +                eq(Stage.entrypoint),
    +                eq("hook1"),
    +                eq(ExecutionStatus.timeout),
    +                eq(6L),
    +                isNull());
    +        verify(metrics).updateHooksMetrics(
    +                eq("module3"),
    +                eq(Stage.auction_response),
    +                eq("hook1"),
    +                eq(ExecutionStatus.success),
    +                eq(4L),
    +                eq(ExecutionAction.update));
    +        verify(metrics).updateHooksMetrics(
    +                eq("module3"),
    +                eq(Stage.auction_response),
    +                eq("hook2"),
    +                eq(ExecutionStatus.success),
    +                eq(4L),
    +                eq(ExecutionAction.no_action));
    +        verify(metrics, never()).updateAccountHooksMetrics(any(), any(), any(), any());
    +        verify(metrics, never()).updateAccountModuleDurationMetric(any(), any(), any());
    +    }
    +
    +    @Test
    +    public void shouldIncrementHooksGlobalAndAccountMetrics() {
    +        // given
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
    +                .willReturn(Future.succeededFuture(givenSeatBid(emptyList())));
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2)));
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder()
    +                .hookExecutionContext(HookExecutionContext.of(
    +                        Endpoint.openrtb2_auction,
    +                        stageOutcomes()))
    +                .debugContext(DebugContext.empty())
    +                .build();
    +
    +        // when
    +        exchangeService.holdAuction(auctionContext);
    +
    +        // then
    +        verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any());
    +        verify(metrics, times(6)).updateAccountHooksMetrics(anyString(), any(), any(), any());
    +        verify(metrics).updateAccountHooksMetrics(
    +                eq("accountId"),
    +                eq("module1"),
    +                eq(ExecutionStatus.success),
    +                eq(ExecutionAction.update));
    +        verify(metrics).updateAccountHooksMetrics(
    +                eq("accountId"),
    +                eq("module1"),
    +                eq(ExecutionStatus.invocation_failure),
    +                isNull());
    +        verify(metrics).updateAccountHooksMetrics(
    +                eq("accountId"),
    +                eq("module1"),
    +                eq(ExecutionStatus.success),
    +                eq(ExecutionAction.no_action));
    +        verify(metrics).updateAccountHooksMetrics(
    +                eq("accountId"),
    +                eq("module2"),
    +                eq(ExecutionStatus.timeout),
    +                isNull());
    +        verify(metrics).updateAccountHooksMetrics(
    +                eq("accountId"),
    +                eq("module3"),
    +                eq(ExecutionStatus.success),
    +                eq(ExecutionAction.update));
    +        verify(metrics).updateAccountHooksMetrics(
    +                eq("accountId"),
    +                eq("module3"),
    +                eq(ExecutionStatus.success),
    +                eq(ExecutionAction.no_action));
    +        verify(metrics, times(3)).updateAccountModuleDurationMetric(anyString(), any(), any());
    +        verify(metrics).updateAccountModuleDurationMetric(eq("accountId"), eq("module1"), eq(14L));
    +        verify(metrics).updateAccountModuleDurationMetric(eq("accountId"), eq("module2"), eq(6L));
    +        verify(metrics).updateAccountModuleDurationMetric(eq("accountId"), eq("module3"), eq(8L));
    +    }
    +
    +    @Test
    +    public void shouldTolerateBidWithDealThatHasNoLineItemAssociated() {
    +        // given
    +        givenBidder(givenSingleSeatBid(givenBid(
    +                Bid.builder().impid("impId").dealid("dealId").price(BigDecimal.ONE).build())));
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), identity());
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest);
    +
    +        // when
    +        exchangeService.holdAuction(auctionContext);
    +
    +        // then
    +        final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(AuctionContext.class);
    +        verify(applicationEventService).publishAuctionEvent(argumentCaptor.capture());
    +        final TxnLog txnLog = argumentCaptor.getValue().getTxnLog();
    +
    +        assertThat(txnLog).isEqualTo(TxnLog.create());
    +    }
    +
    +    @Test
    +    public void shouldRecordLineItemMetricsInTransactionLog() {
    +        // given
    +        givenBidder(givenSeatBid(asList(
    +                givenBid(Bid.builder().impid("impId").dealid("dealId1").price(BigDecimal.ONE).build()),
    +                givenBid(Bid.builder().impid("impId").dealid("dealId2").price(BigDecimal.ONE).build()))));
    +
    +        willReturn(ValidationResult.success()).given(responseBidValidator)
    +                .validate(argThat(bid -> bid.getBid().getDealid().equals("dealId1")), any(), any(), any());
    +        willReturn(ValidationResult.error("validation error")).given(responseBidValidator)
    +                .validate(argThat(bid -> bid.getBid().getDealid().equals("dealId2")), any(), any(), any());
    +
    +        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("someBidder", 1),
    +                builder -> builder
    +                        .id("impId")
    +                        .pmp(Pmp.builder()
    +                                .deals(asList(
    +                                        Deal.builder()
    +                                                .id("dealId1")
    +                                                .ext(mapper.valueToTree(ExtDeal.of(
    +                                                        ExtDealLine.of("lineItemId1", null, null, "someBidder"))))
    +                                                .build(),
    +                                        Deal.builder()
    +                                                .id("dealId2")
    +                                                .ext(mapper.valueToTree(ExtDeal.of(
    +                                                        ExtDealLine.of("lineItemId2", null, null, "someBidder"))))
    +                                                .build()))
    +                                .build()))),
    +                identity());
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest);
    +
    +        // when
    +        exchangeService.holdAuction(auctionContext);
    +
    +        // then
    +        final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(AuctionContext.class);
    +        verify(applicationEventService).publishAuctionEvent(argumentCaptor.capture());
    +        final TxnLog txnLog = argumentCaptor.getValue().getTxnLog();
    +
    +        final TxnLog expectedTxnLog = TxnLog.create();
    +        expectedTxnLog.lineItemsReceivedFromBidder().get("someBidder").addAll(asList("lineItemId1", "lineItemId2"));
    +        expectedTxnLog.lineItemsResponseInvalidated().add("lineItemId2");
    +
    +        assertThat(txnLog).isEqualTo(expectedTxnLog);
    +    }
    +
    +    @Test
    +    public void shouldSendOnlyRelevantDealsToBiddersPresentInImp() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("someBidder", 1),
    +                builder -> builder
    +                        .pmp(Pmp.builder()
    +                                .deals(asList(
    +                                        Deal.builder()
    +                                                .id("dealId1")
    +                                                .ext(mapper.valueToTree(ExtDeal.of(
    +                                                        ExtDealLine.of(null, null, null, "someBidder"))))
    +                                                .build(),
    +                                        Deal.builder()
    +                                                .id("dealId2")
    +                                                .ext(mapper.valueToTree(ExtDeal.of(
    +                                                        ExtDealLine.of(null, null, null, "otherBidder"))))
    +                                                .build()))
    +                                .build()))),
    +                identity());
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder()
    +                .build();
    +
    +        // when
    +        exchangeService.holdAuction(auctionContext);
    +
    +        // then
    +        final Deal expectedDeal = Deal.builder()
    +                .id("dealId1")
    +                .bidfloor(0.0f)
    +                .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of(null, null, null, null))))
    +                .build();
    +        final BidRequest capturedBidRequest = captureBidRequest();
    +
    +        assertThat(capturedBidRequest.getImp())
    +                .extracting(Imp::getPmp)
    +                .flatExtracting(Pmp::getDeals)
    +                .containsOnly(expectedDeal);
    +    }
    +
    +    @Test
    +    public void shouldNotModifyOriginalDealsIfDealsFromLineItemServiceAreMissing() {
    +        // given
    +        final Pmp pmp = Pmp.builder()
    +                .deals(singletonList(Deal.builder().id("dealId1").build())) // deal from prebid request (not from PG)
    +                .build();
    +        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("someBidder", 1),
    +                builder -> builder.pmp(pmp))), identity());
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder()
    +                .build();
    +
    +        // when
    +        exchangeService.holdAuction(auctionContext);
    +
    +        // then
    +        final BidRequest capturedBidRequest = captureBidRequest();
    +        assertThat(capturedBidRequest.getImp())
    +                .extracting(Imp::getPmp)
    +                .containsExactly(pmp);
    +    }
    +
    +    @SuppressWarnings("unchecked")
    +    @Test
    +    public void shouldReduceBidsHavingDealIdWithSameImpIdByBidderWithToleratingNotObtainedBidWithTopDeal() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder1", 1),
    +                builder -> builder
    +                        .id("impId1")
    +                        .pmp(Pmp.builder()
    +                                .deals(asList(
    +                                        Deal.builder().id("dealId1").build(), // top deal, but no response bid
    +                                        Deal.builder().id("dealId2").build()))
    +                                .build()))),
    +                identity());
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder().build();
    +
    +        givenBidder(givenSeatBid(singletonList(
    +                givenBid(Bid.builder().id("bidId2").impid("impId1").dealid("dealId2").price(BigDecimal.ONE).build()))));
    +
    +        given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success());
    +
    +        // when
    +        exchangeService.holdAuction(auctionContext);
    +
    +        // then
    +        final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any());
    +        assertThat(argumentCaptor.getValue()).hasSize(1)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getBids).hasSize(1)
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getId)
    +                .containsOnly("bidId2");
    +    }
    +
    +    @SuppressWarnings("unchecked")
    +    @Test
    +    public void shouldReduceBidsHavingDealIdWithSameImpIdByBidderWithToleratingNotObtainedBids() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder1", 1),
    +                builder -> builder
    +                        .id("impId1")
    +                        .pmp(Pmp.builder()
    +                                .deals(asList(
    +                                        Deal.builder().id("dealId1").build(),
    +                                        Deal.builder().id("dealId2").build()))
    +                                .build()))),
    +                identity());
    +        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder().build();
    +
    +        givenBidder(givenSeatBid(emptyList()));
    +
    +        given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success());
    +
    +        // when
    +        exchangeService.holdAuction(auctionContext);
    +
    +        // then
    +        final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any());
    +
    +        assertThat(argumentCaptor.getValue()).hasSize(1)
    +                .extracting(BidderResponse::getSeatBid)
    +                .flatExtracting(BidderSeatBid::getBids).isEmpty();
    +    }
    +
         private AuctionContext givenRequestContext(BidRequest bidRequest) {
    -        return givenRequestContext(bidRequest, Account.builder().id("accountId").eventsEnabled(true).build());
    +        return givenRequestContext(
    +                bidRequest,
    +                Account.builder()
    +                        .id("accountId")
    +                        .auction(AccountAuctionConfig.builder()
    +                                .events(AccountEventsConfig.of(true))
    +                                .build())
    +                        .build());
         }
     
         private AuctionContext givenRequestContext(BidRequest bidRequest, Account account) {
             return AuctionContext.builder()
    +                .httpRequest(HttpRequestContext.builder().headers(CaseInsensitiveMultiMap.empty()).build())
                     .uidsCookie(uidsCookie)
                     .bidRequest(bidRequest)
    +                .debugWarnings(new ArrayList<>())
                     .account(account)
                     .requestTypeMetric(MetricName.openrtb2web)
                     .timeout(timeout)
    +                .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_auction))
    +                .debugContext(DebugContext.empty())
    +                .txnLog(TxnLog.create())
    +                .deepDebugLog(DeepDebugLog.create(false, clock))
                     .build();
         }
     
         private BidRequest captureBidRequest() {
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidRequest.class);
    -        verify(httpBidderRequester).requestBids(any(), bidRequestCaptor.capture(), any(), anyBoolean());
    -        return bidRequestCaptor.getValue();
    +        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidRequestCaptor.capture(), any(), any(), anyBoolean());
    +        return bidRequestCaptor.getValue().getBidRequest();
    +    }
    +
    +    private List captureBidResponses() {
    +        final ArgumentCaptor> bidderResponseCaptor = ArgumentCaptor.forClass(List.class);
    +        verify(bidResponseCreator).create(bidderResponseCaptor.capture(), any(), any(), any());
    +        return bidderResponseCaptor.getValue();
         }
     
         private static BidRequest givenBidRequest(
    @@ -2294,7 +3671,8 @@ private static BidRequest givenBidRequest(List imp) {
     
         private static  Imp givenImp(T ext, Function impBuilderCustomizer) {
             return impBuilderCustomizer.apply(Imp.builder()
    -                .ext(ext != null ? mapper.valueToTree(ext) : null))
    +                .ext(mapper.valueToTree(singletonMap(
    +                        "prebid", ext != null ? singletonMap("bidder", ext) : emptyMap()))))
                     .build();
         }
     
    @@ -2303,13 +3681,13 @@ private static  List givenSingleImp(T ext) {
         }
     
         private void givenBidder(BidderSeatBid response) {
    -        given(httpBidderRequester.requestBids(any(), any(), any(), anyBoolean()))
    +        given(httpBidderRequester.requestBids(any(), any(), any(), any(), anyBoolean()))
                     .willReturn(Future.succeededFuture(response));
         }
     
         private void givenBidder(String bidderName, Bidder bidder, BidderSeatBid response) {
             doReturn(bidder).when(bidderCatalog).bidderByName(eq(bidderName));
    -        given(httpBidderRequester.requestBids(same(bidder), any(), any(), anyBoolean()))
    +        given(httpBidderRequester.requestBids(same(bidder), any(), any(), any(), anyBoolean()))
                     .willReturn(Future.succeededFuture(response));
         }
     
    @@ -2356,10 +3734,9 @@ private static  Map doubleMap(K key1, V value1, K key2, V value2) {
             return map;
         }
     
    -    private static ExtPrebid toExtPrebid(ObjectNode ext) {
    +    private static ExtBidPrebid toExtBidPrebid(ObjectNode ext) {
             try {
    -            return mapper.readValue(mapper.treeAsTokens(ext), new TypeReference>() {
    -            });
    +            return mapper.treeToValue(ext.get("prebid"), ExtBidPrebid.class);
             } catch (IOException e) {
                 return rethrow(e);
             }
    @@ -2375,12 +3752,12 @@ private static ExtRequestTargeting givenTargeting(boolean includebidderkeys) {
         }
     
         private void givenBidResponseCreator(List bids) {
    -        given(bidResponseCreator.create(anyList(), any(), any(), anyBoolean()))
    +        given(bidResponseCreator.create(anyList(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(givenBidResponseWithBids(bids)));
         }
     
         private void givenBidResponseCreator(Map> errors) {
    -        given(bidResponseCreator.create(anyList(), any(), any(), anyBoolean()))
    +        given(bidResponseCreator.create(anyList(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(givenBidResponseWithError(errors)));
         }
     
    @@ -2394,7 +3771,122 @@ private static BidResponse givenBidResponseWithBids(List bids) {
         private static BidResponse givenBidResponseWithError(Map> errors) {
             return BidResponse.builder()
                     .seatbid(emptyList())
    -                .ext(mapper.valueToTree(ExtBidResponse.of(null, errors, null, null, null, null)))
    +                .ext(ExtBidResponse.builder()
    +                        .errors(errors)
    +                        .build())
                     .build();
         }
    +
    +    private void testUserEidsPermissionFiltering(List givenExtUserEids,
    +                                                 List givenEidPermissions,
    +                                                 Map givenAlises,
    +                                                 List expectedExtUserEids) {
    +        // given
    +        final Bidder bidder = mock(Bidder.class);
    +        givenBidder("someBidder", bidder, givenEmptySeatBid());
    +        final Map bidderToGdpr = singletonMap("someBidder", 1);
    +        final ExtUser extUser = ExtUser.builder().eids(givenExtUserEids).build();
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr),
    +                builder -> builder
    +                        .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                                .aliases(givenAlises)
    +                                .data(ExtRequestPrebidData.of(null, givenEidPermissions))
    +                                .build()))
    +                        .user(User.builder()
    +                                .ext(extUser)
    +                                .build()));
    +
    +        // when
    +        exchangeService.holdAuction(givenRequestContext(bidRequest));
    +
    +        // then
    +        final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
    +        verify(httpBidderRequester).requestBids(any(), bidderRequestCaptor.capture(), any(), any(), anyBoolean());
    +        final List capturedBidRequests = bidderRequestCaptor.getAllValues();
    +        assertThat(capturedBidRequests)
    +                .extracting(BidderRequest::getBidRequest)
    +                .extracting(BidRequest::getUser)
    +                .extracting(User::getExt)
    +                .flatExtracting(ExtUser::getEids)
    +                .isEqualTo(expectedExtUserEids);
    +    }
    +
    +    private static EnumMap> stageOutcomes() {
    +        final Map> stageOutcomes = new HashMap<>();
    +
    +        stageOutcomes.put(Stage.entrypoint, singletonList(StageExecutionOutcome.of(
    +                "http-request",
    +                asList(
    +                        GroupExecutionOutcome.of(asList(
    +                                HookExecutionOutcome.builder()
    +                                        .hookId(HookId.of("module1", "hook1"))
    +                                        .executionTime(4L)
    +                                        .status(ExecutionStatus.success)
    +                                        .message("Message 1-1")
    +                                        .action(ExecutionAction.update)
    +                                        .errors(asList("error message 1-1 1", "error message 1-1 2"))
    +                                        .warnings(asList("warning message 1-1 1", "warning message 1-1 2"))
    +                                        .debugMessages(asList("debug message 1-1 1", "debug message 1-1 2"))
    +                                        .analyticsTags(TagsImpl.of(singletonList(
    +                                                ActivityImpl.of(
    +                                                        "some-activity",
    +                                                        "success",
    +                                                        singletonList(ResultImpl.of(
    +                                                                "success",
    +                                                                mapper.createObjectNode(),
    +                                                                AppliedToImpl.builder()
    +                                                                        .impIds(asList("impId1", "impId2"))
    +                                                                        .request(true)
    +                                                                        .build()))))))
    +                                        .build(),
    +                                HookExecutionOutcome.builder()
    +                                        .hookId(HookId.of("module1", "hook2"))
    +                                        .executionTime(6L)
    +                                        .status(ExecutionStatus.invocation_failure)
    +                                        .message("Message 1-2")
    +                                        .errors(asList("error message 1-2 1", "error message 1-2 2"))
    +                                        .warnings(asList("warning message 1-2 1", "warning message 1-2 2"))
    +                                        .build())),
    +                        GroupExecutionOutcome.of(asList(
    +                                HookExecutionOutcome.builder()
    +                                        .hookId(HookId.of("module1", "hook2"))
    +                                        .executionTime(4L)
    +                                        .status(ExecutionStatus.success)
    +                                        .message("Message 1-2")
    +                                        .action(ExecutionAction.no_action)
    +                                        .errors(asList("error message 1-2 3", "error message 1-2 4"))
    +                                        .warnings(asList("warning message 1-2 3", "warning message 1-2 4"))
    +                                        .build(),
    +                                HookExecutionOutcome.builder()
    +                                        .hookId(HookId.of("module2", "hook1"))
    +                                        .executionTime(6L)
    +                                        .status(ExecutionStatus.timeout)
    +                                        .message("Message 2-1")
    +                                        .errors(asList("error message 2-1 1", "error message 2-1 2"))
    +                                        .warnings(asList("warning message 2-1 1", "warning message 2-1 2"))
    +                                        .build()))))));
    +
    +        stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of(
    +                "auction-response",
    +                singletonList(
    +                        GroupExecutionOutcome.of(asList(
    +                                HookExecutionOutcome.builder()
    +                                        .hookId(HookId.of("module3", "hook1"))
    +                                        .executionTime(4L)
    +                                        .status(ExecutionStatus.success)
    +                                        .message("Message 3-1")
    +                                        .action(ExecutionAction.update)
    +                                        .errors(asList("error message 3-1 1", "error message 3-1 2"))
    +                                        .warnings(asList("warning message 3-1 1", "warning message 3-1 2"))
    +                                        .build(),
    +                                HookExecutionOutcome.builder()
    +                                        .hookId(HookId.of("module3", "hook2"))
    +                                        .executionTime(4L)
    +                                        .status(ExecutionStatus.success)
    +                                        .action(ExecutionAction.no_action)
    +                                        .build()))))));
    +
    +        return new EnumMap<>(stageOutcomes);
    +    }
     }
    diff --git a/src/test/java/org/prebid/server/auction/FpdResolverTest.java b/src/test/java/org/prebid/server/auction/FpdResolverTest.java
    index bb0c6def595..f0662a88399 100644
    --- a/src/test/java/org/prebid/server/auction/FpdResolverTest.java
    +++ b/src/test/java/org/prebid/server/auction/FpdResolverTest.java
    @@ -14,10 +14,11 @@
     import org.mockito.junit.MockitoJUnit;
     import org.mockito.junit.MockitoRule;
     import org.prebid.server.VertxTest;
    +import org.prebid.server.json.JsonMerger;
     import org.prebid.server.proto.openrtb.ext.request.ExtApp;
     import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
     import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfig;
    -import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigFpd;
    +import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigOrtb;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig;
    @@ -40,7 +41,7 @@ public class FpdResolverTest extends VertxTest {
     
         @Before
         public void setUp() {
    -        fpdResolver = new FpdResolver(jacksonMapper);
    +        fpdResolver = new FpdResolver(jacksonMapper, new JsonMerger(jacksonMapper));
         }
     
         @Test
    @@ -174,6 +175,27 @@ public void resolveUserShouldNotChangeOriginExtDataIfFPDDoesNotHaveExt() {
                     .build());
         }
     
    +    @Test
    +    public void resolveUserShouldReturnCopyOfUserExtDataIfFPDUserExtDataIsMissing() {
    +        // given
    +        final ObjectNode originExtUserData = mapper.createObjectNode().put("originAttr", "originValue");
    +
    +        final User originUser = User.builder()
    +                .ext(ExtUser.builder().data(originExtUserData).build())
    +                .build();
    +
    +        final User fpdUser = User.builder()
    +                .ext(ExtUser.builder().data(null).build())
    +                .build();
    +
    +        // when
    +        final User resultUser = fpdResolver.resolveUser(originUser, mapper.valueToTree(fpdUser));
    +
    +        // then
    +        assertThat(resultUser.getExt().getData() != originExtUserData).isTrue(); // different by reference
    +        assertThat(resultUser.getExt().getData().equals(originExtUserData)).isTrue(); // but the same by value
    +    }
    +
         @Test
         public void resolveAppShouldOverrideFpdFieldsFromFpdApp() {
             // given
    @@ -408,19 +430,21 @@ public void resolveImpExtShouldNotUpdateExtImpIfTargetingHasNotAttributesToMerge
         @Test
         public void resolveImpExtShouldMergeExtImpContextDataWithFpdPriority() {
             // given
    -        final ObjectNode extImp = mapper.createObjectNode().set("context", mapper.createObjectNode()
    -                .set("data", mapper.createObjectNode().put("replacedAttr", "originValue")
    -                        .put("originAttr", "originValue")));
    +        final ObjectNode extImp = mapper.createObjectNode().set("data", mapper.createObjectNode()
    +                .put("replacedAttr", "originValue")
    +                .put("originAttr", "originValue"));
             final ObjectNode targeting = mapper.createObjectNode()
    -                .put("site", "site").put("replacedAttr", "fpdValue").put("fpdAttr", "fpdValue2");
    +                .put("site", "site")
    +                .put("replacedAttr", "fpdValue")
    +                .put("fpdAttr", "fpdValue2");
     
             // when
             final ObjectNode result = fpdResolver.resolveImpExt(extImp, targeting);
     
             // then
    -        assertThat(result).isEqualTo(mapper.createObjectNode().set("context", mapper.createObjectNode()
    +        assertThat(result).isEqualTo(mapper.createObjectNode()
                     .set("data", mapper.createObjectNode().put("replacedAttr", "fpdValue")
    -                        .put("originAttr", "originValue").put("fpdAttr", "fpdValue2"))));
    +                        .put("originAttr", "originValue").put("fpdAttr", "fpdValue2")));
         }
     
         @Test
    @@ -433,8 +457,8 @@ public void resolveImpExtShouldCreateExtImpContextDataIfExtImpIsNull() {
             final ObjectNode result = fpdResolver.resolveImpExt(null, targeting);
     
             // then
    -        assertThat(result).isEqualTo(mapper.createObjectNode().set("context", mapper.createObjectNode()
    -                .set("data", mapper.createObjectNode().put("replacedAttr", "fpdValue").put("fpdAttr", "fpdValue2"))));
    +        assertThat(result).isEqualTo(mapper.createObjectNode()
    +                .set("data", mapper.createObjectNode().put("replacedAttr", "fpdValue").put("fpdAttr", "fpdValue2")));
         }
     
         @Test
    @@ -449,16 +473,15 @@ public void resolveImpExtShouldCreateExtImpContextDataIfExtImpContextIsNullAndKe
     
             // then
             final ObjectNode expectedResult = mapper.createObjectNode().put("prebid", 1).put("rubicon", 2);
    -        assertThat(result).isEqualTo(expectedResult.set("context", mapper.createObjectNode()
    +        assertThat(result).isEqualTo(expectedResult
                     .set("data", mapper.createObjectNode().put("replacedAttr", "fpdValue")
    -                        .put("fpdAttr", "fpdValue2"))));
    +                .put("fpdAttr", "fpdValue2")));
         }
     
         @Test
    -    public void resolveImpExtShouldCreateExtImpContextDataIfExtImpContextDataIsNullAndKeepOtherFields() {
    +    public void resolveImpExtShouldCreateExtImpDataIfExtImpDataIsNullAndKeepOtherFields() {
             // given
             final ObjectNode extImp = mapper.createObjectNode().set("prebid", mapper.createObjectNode());
    -        extImp.set("context", mapper.createObjectNode().put("keywords", "keywords"));
             final ObjectNode targeting = mapper.createObjectNode()
                     .put("site", "site").put("replacedAttr", "fpdValue").put("fpdAttr", "fpdValue2");
     
    @@ -467,13 +490,227 @@ public void resolveImpExtShouldCreateExtImpContextDataIfExtImpContextDataIsNullA
     
             // then
             final ObjectNode expectedResult = mapper.createObjectNode().set("prebid", mapper.createObjectNode());
    -        final ObjectNode context = mapper.createObjectNode().put("keywords", "keywords");
    -        context.set("data", mapper.createObjectNode().put("replacedAttr", "fpdValue")
    +        expectedResult.set("data", mapper.createObjectNode().put("replacedAttr", "fpdValue")
                     .put("fpdAttr", "fpdValue2"));
    -        expectedResult.set("context", context);
             assertThat(result).isEqualTo(expectedResult);
         }
     
    +    @Test
    +    public void resolveImpExtShouldNotSetContextIfContextIsAbsent() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode();
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, true);
    +
    +        // then
    +        assertThat(result.get("context")).isNull();
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldNotRemoveDataFromContextIfFPDEnabled() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("context", mapper.createObjectNode()
    +                        .set("data", mapper.createObjectNode()
    +                                .put("attr1", "value1")));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, true);
    +
    +        // then
    +        assertThat(result.get("context")).isEqualTo(mapper.createObjectNode()
    +                .set("data", mapper.createObjectNode()
    +                        .put("attr1", "value1")));
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldRemoveDataFromContextIfFPDDisabled() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("context", mapper.createObjectNode()
    +                        .put("keyword", "keyw1")
    +                        .set("data", mapper.createObjectNode()
    +                                .put("attr1", "value1")));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, false);
    +
    +        // then
    +        assertThat(result.get("context")).isEqualTo(mapper.createObjectNode()
    +                .put("keyword", "keyw1"));
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldTolerateNonObjectContextIfFPDDisabled() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("context", mapper.createArrayNode().add("value1"));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, false);
    +
    +        // then
    +        assertThat(result.get("context")).isEqualTo(mapper.createArrayNode().add("value1"));
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldNotSetContextIfEmpty() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("context", mapper.createObjectNode()
    +                        .set("data", mapper.createObjectNode()
    +                                .put("attr1", "value1")));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, false);
    +
    +        // then
    +        assertThat(result.get("context")).isNull();
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldNotSetDataIfDataIsAbsent() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode();
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, true);
    +
    +        // then
    +        assertThat(result.get("data")).isNull();
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldNotSetDataIfFPDDisabled() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("data", mapper.createObjectNode()
    +                        .put("attr1", "value1"));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, false);
    +
    +        // then
    +        assertThat(result.get("data")).isNull();
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldSetDataFromContextDataIfDataIsAbsent() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("context", mapper.createObjectNode()
    +                        .set("data", mapper.createObjectNode()
    +                                .put("attr1", "value1")));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, true);
    +
    +        // then
    +        assertThat(result.get("data")).isEqualTo(mapper.createObjectNode()
    +                .put("attr1", "value1"));
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldMergeDataWithContextDataIfDataIsPresent() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("context", mapper.createObjectNode()
    +                        .set("data", mapper.createObjectNode()
    +                                .put("attr1", "value1")))
    +                .set("data", mapper.createObjectNode()
    +                        .put("attr2", "value2"));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, true);
    +
    +        // then
    +        assertThat(result.get("data")).isEqualTo(mapper.createObjectNode()
    +                .put("attr1", "value1")
    +                .put("attr2", "value2"));
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldNotChangeDataIfContextDataIsAbsent() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("context", mapper.createObjectNode())
    +                .set("data", mapper.createObjectNode()
    +                        .put("attr2", "value2"));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, true);
    +
    +        // then
    +        assertThat(result.get("data")).isEqualTo(mapper.createObjectNode()
    +                .put("attr2", "value2"));
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldTolerateNonObjectData() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("context", mapper.createObjectNode()
    +                        .set("data", mapper.createObjectNode()
    +                                .put("attr1", "value1")))
    +                .set("data", mapper.createArrayNode()
    +                        .add("attr2"));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, true);
    +
    +        // then
    +        assertThat(result.get("data")).isEqualTo(mapper.createArrayNode()
    +                .add("attr2"));
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldNotMergeNonObjectContextData() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("context", mapper.createArrayNode()
    +                        .add("attr1"))
    +                .set("data", mapper.createObjectNode()
    +                        .put("attr2", "value2"));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, true);
    +
    +        // then
    +        assertThat(result.get("data")).isEqualTo(mapper.createObjectNode()
    +                .put("attr2", "value2"));
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldNotRemoveUninvolvedFields() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("all", mapper.createObjectNode()
    +                        .put("attr1", "value1"));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, true);
    +
    +        // then
    +        assertThat(result.get("all")).isEqualTo(mapper.createObjectNode()
    +                .put("attr1", "value1"));
    +    }
    +
    +    @Test
    +    public void resolveImpExtShouldNotRemoveUninvolvedFieldsIfFPDDisabled() {
    +        // given
    +        final ObjectNode impExt = mapper.createObjectNode()
    +                .set("all", mapper.createObjectNode()
    +                        .put("attr1", "value1"));
    +
    +        // when
    +        final ObjectNode result = fpdResolver.resolveImpExt(impExt, false);
    +
    +        // then
    +        assertThat(result.get("all")).isEqualTo(mapper.createObjectNode()
    +                .put("attr1", "value1"));
    +    }
    +
         @Test
         public void resolveBidRequestExtShouldReturnSameExtIfTargetingIsNull() {
             // given
    @@ -486,11 +723,25 @@ public void resolveBidRequestExtShouldReturnSameExtIfTargetingIsNull() {
             assertThat(result).isSameAs(givenExtRequest);
         }
     
    +    @Test
    +    public void resolveBidRequestExtShouldTolerateMissingBidders() {
    +        // given
    +        final ExtRequest givenExtRequest = ExtRequest.of(ExtRequestPrebid.builder()
    +                .data(ExtRequestPrebidData.of(Arrays.asList("rubicon", "appnexus"), null)).build());
    +
    +        // when
    +        final ExtRequest result = fpdResolver.resolveBidRequestExt(givenExtRequest,
    +                Targeting.of(null, null, null)); // no bidders
    +
    +        // then
    +        assertThat(result.getPrebid().getData().getBidders()).contains("rubicon", "appnexus");
    +    }
    +
         @Test
         public void resolveBidRequestExtShouldMergeBidders() {
             // given
             final ExtRequest givenExtRequest = ExtRequest.of(ExtRequestPrebid.builder()
    -                .data(ExtRequestPrebidData.of(Arrays.asList("rubicon", "appnexus"))).build());
    +                .data(ExtRequestPrebidData.of(Arrays.asList("rubicon", "appnexus"), null)).build());
     
             // when
             final ExtRequest result = fpdResolver.resolveBidRequestExt(givenExtRequest,
    @@ -534,14 +785,14 @@ public void resolveBidRequestExtShouldAddBiddersIfExtPrebidDataIsNullKeepingOthe
     
             // then
             assertThat(result).isEqualTo(ExtRequest.of(ExtRequestPrebid.builder().debug(1)
    -                .data(ExtRequestPrebidData.of(Arrays.asList("rubicon", "adform"))).build()));
    +                .data(ExtRequestPrebidData.of(Arrays.asList("rubicon", "adform"), null)).build()));
         }
     
         @Test
         public void resolveBidRequestExtShouldAddBiddersIfExtPrebidDataBiddersIsNull() {
             // given
             final ExtRequest givenExtRequest = ExtRequest.of(ExtRequestPrebid.builder().debug(1)
    -                .data(ExtRequestPrebidData.of(null)).build());
    +                .data(ExtRequestPrebidData.of(null, null)).build());
     
             // when
             final ExtRequest result = fpdResolver.resolveBidRequestExt(givenExtRequest,
    @@ -549,7 +800,7 @@ public void resolveBidRequestExtShouldAddBiddersIfExtPrebidDataBiddersIsNull() {
     
             // then
             assertThat(result).isEqualTo(ExtRequest.of(ExtRequestPrebid.builder().debug(1)
    -                .data(ExtRequestPrebidData.of(Arrays.asList("rubicon", "adform"))).build()));
    +                .data(ExtRequestPrebidData.of(Arrays.asList("rubicon", "adform"), null)).build()));
         }
     
         @Test
    @@ -564,10 +815,13 @@ public void resolveBidRequestExtShouldAddBidderConfig() {
             final ExtRequest result = fpdResolver.resolveBidRequestExt(givenExtRequest, targeting);
     
             // then
    +        final ExtRequestPrebidBidderConfig expectedBidderConfig = ExtRequestPrebidBidderConfig.of(
    +                Collections.singletonList("*"),
    +                ExtBidderConfig.of(null, ExtBidderConfigOrtb.of(siteNode, null, userNode)));
    +
             assertThat(result).isEqualTo(ExtRequest.of(ExtRequestPrebid.builder()
    -                .bidderconfig(Collections.singletonList(ExtRequestPrebidBidderConfig.of(
    -                        Collections.singletonList("*"), ExtBidderConfig.of(ExtBidderConfigFpd.of(
    -                                siteNode, null, userNode))))).build()));
    +                .bidderconfig(Collections.singletonList(expectedBidderConfig))
    +                .build()));
         }
     
         @Test
    @@ -596,9 +850,9 @@ public void resolveBidRequestExtShoulUpdateBidderConfigAndData() {
     
             // then
             assertThat(result).isEqualTo(ExtRequest.of(ExtRequestPrebid.builder()
    -                .data(ExtRequestPrebidData.of(Arrays.asList("rubicon", "adform")))
    +                .data(ExtRequestPrebidData.of(Arrays.asList("rubicon", "adform"), null))
                     .bidderconfig(Collections.singletonList(ExtRequestPrebidBidderConfig.of(
    -                        Collections.singletonList("*"), ExtBidderConfig.of(ExtBidderConfigFpd.of(
    +                        Collections.singletonList("*"), ExtBidderConfig.of(null, ExtBidderConfigOrtb.of(
                                     mapper.valueToTree(Site.builder().id("id").build()), null,
                                     mapper.valueToTree(User.builder().id("id").build())))))).build()));
         }
    diff --git a/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java b/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java
    index 39bf00d299a..b3868d0b96f 100644
    --- a/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java
    +++ b/src/test/java/org/prebid/server/auction/ImplicitParametersExtractorTest.java
    @@ -2,23 +2,18 @@
     
     import de.malkusch.whoisServerList.publicSuffixList.PublicSuffixList;
     import de.malkusch.whoisServerList.publicSuffixList.PublicSuffixListFactory;
    -import io.vertx.core.http.CaseInsensitiveHeaders;
    -import io.vertx.core.http.HttpServerRequest;
    -import io.vertx.core.net.impl.SocketAddressImpl;
     import org.junit.Before;
     import org.junit.Rule;
     import org.junit.Test;
    -import org.mockito.Mock;
     import org.mockito.junit.MockitoJUnit;
     import org.mockito.junit.MockitoRule;
     import org.prebid.server.exception.PreBidException;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
    +import org.prebid.server.model.HttpRequestContext;
     import org.prebid.server.util.HttpUtil;
     
    -import java.net.MalformedURLException;
    -
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatCode;
    -import static org.mockito.BDDMockito.given;
     
     public class ImplicitParametersExtractorTest {
     
    @@ -28,21 +23,21 @@ public class ImplicitParametersExtractorTest {
         private final PublicSuffixList psl = new PublicSuffixListFactory().build();
     
         private ImplicitParametersExtractor extractor;
    -    @Mock
    -    private HttpServerRequest httpRequest;
     
         @Before
         public void setUp() {
    -        // minimal request
    -        given(httpRequest.headers()).willReturn(new CaseInsensitiveHeaders());
    -
             extractor = new ImplicitParametersExtractor(psl);
         }
     
         @Test
         public void refererFromShouldReturnRefererFromRefererHeader() {
             // given
    -        httpRequest.headers().set(HttpUtil.REFERER_HEADER, "http://example.com");
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.builder()
    +                        .add(HttpUtil.REFERER_HEADER, "http://example.com")
    +                        .build())
    +                .queryParams(CaseInsensitiveMultiMap.empty())
    +                .build();
     
             // when and then
             assertThat(extractor.refererFrom(httpRequest)).isEqualTo("http://example.com");
    @@ -51,8 +46,14 @@ public void refererFromShouldReturnRefererFromRefererHeader() {
         @Test
         public void refererFromShouldReturnRefererFromRefererHeaderIfUrlOverrideParamBlank() {
             // given
    -        httpRequest.headers().set(HttpUtil.REFERER_HEADER, "http://example.com");
    -        given(httpRequest.getParam("url_override")).willReturn("");
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.builder()
    +                        .add(HttpUtil.REFERER_HEADER, "http://example.com")
    +                        .build())
    +                .queryParams(CaseInsensitiveMultiMap.builder()
    +                        .add("url_override", "")
    +                        .build())
    +                .build();
     
             // when and then
             assertThat(extractor.refererFrom(httpRequest)).isEqualTo("http://example.com");
    @@ -61,7 +62,11 @@ public void refererFromShouldReturnRefererFromRefererHeaderIfUrlOverrideParamBla
         @Test
         public void refererFromShouldReturnRefererFromRequestParamIfUrlOverrideParamExists() {
             // given
    -        given(httpRequest.getParam("url_override")).willReturn("http://exampleoverrride.com");
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .queryParams(CaseInsensitiveMultiMap.builder()
    +                        .add("url_override", "http://exampleoverrride.com")
    +                        .build())
    +                .build();
     
             // when and then
             assertThat(extractor.refererFrom(httpRequest)).isEqualTo("http://exampleoverrride.com");
    @@ -70,7 +75,11 @@ public void refererFromShouldReturnRefererFromRequestParamIfUrlOverrideParamExis
         @Test
         public void refererFromShouldReturnRefererWithHttpSchemeIfUrlOverrideParamDoesNotContainScheme() {
             // given
    -        given(httpRequest.getParam("url_override")).willReturn("example.com");
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .queryParams(CaseInsensitiveMultiMap.builder()
    +                        .add("url_override", "example.com")
    +                        .build())
    +                .build();
     
             // when and then
             assertThat(extractor.refererFrom(httpRequest)).isEqualTo("http://example.com");
    @@ -79,66 +88,76 @@ public void refererFromShouldReturnRefererWithHttpSchemeIfUrlOverrideParamDoesNo
         @Test
         public void refererFromShouldReturnRefererWithHttpSchemeIfRefererHeaderDoesNotContainScheme() {
             // given
    -        httpRequest.headers().set(HttpUtil.REFERER_HEADER, "example.com");
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.builder()
    +                        .add(HttpUtil.REFERER_HEADER, "example.com")
    +                        .build())
    +                .queryParams(CaseInsensitiveMultiMap.empty())
    +                .build();
     
             // when and then
             assertThat(extractor.refererFrom(httpRequest)).isEqualTo("http://example.com");
         }
     
         @Test
    -    public void domainFromShouldFailIfUrlCouldNotBeParsed() {
    -        assertThatCode(() -> extractor.domainFrom("httpP://non_an_url"))
    +    public void domainFromShouldFailIfHostIsNull() {
    +        assertThatCode(() -> extractor.domainFrom(null))
                     .isInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid URL 'httpP://non_an_url': unknown protocol: httpp")
    -                .hasCauseInstanceOf(MalformedURLException.class);
    +                .hasMessage("Host is not defined or can not be derived from request");
         }
     
         @Test
    -    public void domainFromShouldFailIfUrlDoesNotContainHost() {
    -        assertThatCode(() -> extractor.domainFrom("http:/path"))
    +    public void domainFromShouldFailIfDomainCouldNotBeDerivedFromHost() {
    +        assertThatCode(() -> extractor.domainFrom("domain"))
                     .isInstanceOf(PreBidException.class)
    -                .hasMessage("Host not found from URL 'http:/path'");
    +                .hasMessage("Cannot derive eTLD+1 for host domain");
         }
     
         @Test
    -    public void domainFromShouldFailIfDomainCouldNotBeDerivedFromUrl() {
    -        assertThatCode(() -> extractor.domainFrom("http://domain"))
    -                .isInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid URL 'domain': cannot derive eTLD+1 for domain domain");
    +    public void domainFromShouldDeriveDomainFromHost() {
    +        assertThat(extractor.domainFrom("example.com")).isEqualTo("example.com");
         }
     
         @Test
    -    public void domainFromShouldDeriveDomainFromUrl() {
    -        assertThat(extractor.domainFrom("http://example.com")).isEqualTo("example.com");
    +    public void domainFromShouldDeriveDomainFromHostWithSubdomain() {
    +        assertThat(extractor.domainFrom("subdomain.example.com")).isEqualTo("example.com");
         }
     
         @Test
         public void ipFromShouldReturnIpFromHeadersAndRemoteAddress() {
             // given
    -        httpRequest.headers().set("True-Client-IP", "192.168.144.1 ");
    -        httpRequest.headers().set("X-Forwarded-For", "192.168.144.2 , 192.168.144.3 ");
    -        httpRequest.headers().set("X-Real-IP", "192.168.144.4 ");
    -        given(httpRequest.remoteAddress()).willReturn(new SocketAddressImpl(0, "192.168.144.5"));
    +        final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder()
    +                .add("True-Client-IP", "192.168.144.1 ")
    +                .add("X-Forwarded-For", "192.168.144.2 , 192.168.144.3 ")
    +                .add("X-Real-IP", "192.168.144.4 ")
    +                .build();
    +        final String remoteHost = "192.168.144.5";
     
             // when and then
    -        assertThat(extractor.ipFrom(httpRequest)).containsExactly(
    -                "192.168.144.1", "192.168.144.2", "192.168.144.3", "192.168.144.4", "192.168.144.5");
    +        assertThat(extractor.ipFrom(headers, remoteHost)).containsExactly(
    +                "192.168.144.1", "192.168.144.2", "192.168.144.3", "192.168.144.4", remoteHost);
         }
     
         @Test
         public void ipFromShouldNotReturnNullsAndEmptyValues() {
             // given
    -        httpRequest.headers().set("X-Real-IP", " ");
    -        given(httpRequest.remoteAddress()).willReturn(new SocketAddressImpl(0, "192.168.144.5"));
    +        final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder()
    +                .add("X-Real-IP", " ")
    +                .build();
    +        final String remoteHost = "192.168.144.5";
     
             // when and then
    -        assertThat(extractor.ipFrom(httpRequest)).containsExactly("192.168.144.5");
    +        assertThat(extractor.ipFrom(headers, remoteHost)).containsExactly("192.168.144.5");
         }
     
         @Test
         public void uaFromShouldReturnUaFromUserAgentHeader() {
             // given
    -        httpRequest.headers().set(HttpUtil.USER_AGENT_HEADER, " user agent ");
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.builder()
    +                        .add(HttpUtil.USER_AGENT_HEADER, " user agent ")
    +                        .build())
    +                .build();
     
             // when and then
             assertThat(extractor.uaFrom(httpRequest)).isEqualTo("user agent");
    @@ -147,7 +166,11 @@ public void uaFromShouldReturnUaFromUserAgentHeader() {
         @Test
         public void secureFromShouldReturnOneIfXForwardedProtoIsHttps() {
             // given
    -        httpRequest.headers().set("X-Forwarded-Proto", "https");
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.builder()
    +                        .add("X-Forwarded-Proto", "https")
    +                        .build())
    +                .build();
     
             // when and then
             assertThat(extractor.secureFrom(httpRequest)).isEqualTo(1);
    @@ -156,7 +179,10 @@ public void secureFromShouldReturnOneIfXForwardedProtoIsHttps() {
         @Test
         public void secureFromShouldReturnOneIfConnectedViaSSL() {
             // given
    -        given(httpRequest.scheme()).willReturn("https");
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.empty())
    +                .scheme("https")
    +                .build();
     
             // when and then
             assertThat(extractor.secureFrom(httpRequest)).isEqualTo(1);
    @@ -164,6 +190,9 @@ public void secureFromShouldReturnOneIfConnectedViaSSL() {
     
         @Test
         public void secureFromShouldReturnNull() {
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.empty())
    +                .build();
             assertThat(extractor.secureFrom(httpRequest)).isNull();
         }
     }
    diff --git a/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java b/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java
    index 3c6fbef67f9..9c6f510c751 100644
    --- a/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java
    +++ b/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java
    @@ -29,7 +29,7 @@ public void processShouldReturnBidRequestUpdatedWithImpsInterstitialFormat() {
                             .format(singletonList(Format.builder().w(400).h(600).build())).build()).instl(1)
                             .build()))
                     .device(Device.builder()
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build();
     
    @@ -53,7 +53,7 @@ public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsUsingSize
             final BidRequest bidRequest = BidRequest.builder()
                     .imp(singletonList(Imp.builder().banner(Banner.builder().build()).instl(1).build()))
                     .device(Device.builder().w(400).h(600)
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build();
     
    @@ -78,7 +78,7 @@ public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsUsingSize
                     .imp(singletonList(Imp.builder().banner(Banner.builder().format(singletonList(
                             Format.builder().w(1).h(1).build())).build()).instl(1).build()))
                     .device(Device.builder().w(400).h(600)
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build();
     
    @@ -104,7 +104,7 @@ public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsLimitedBy
                             .format(singletonList(Format.builder().w(400).h(600).build())).build()).instl(1)
                             .build()))
                     .device(Device.builder()
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(1, 1))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(1, 1))))
                             .build())
                     .build();
     
    @@ -133,7 +133,7 @@ public void processShouldNotUpdateBidRequestWhenImpInstlIsEqualToZero() {
             final BidRequest bidRequest = BidRequest.builder()
                     .imp(singletonList(Imp.builder().banner(Banner.builder().build()).instl(0).build()))
                     .device(Device.builder().w(400).h(600)
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build();
     
    @@ -171,7 +171,7 @@ public void processShouldNotUpdateImpWithInstlZeroAndUpdateImpWithIstlOne() {
                                     .format(singletonList(Format.builder().w(400).h(600).build()))
                                     .build()).instl(1).build()))
                     .device(Device.builder()
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build();
     
    @@ -192,7 +192,7 @@ public void processShouldNotUpdateImpWithInstlZeroAndUpdateImpWithIstlOne() {
                                             Format.builder().w(320).h(481).build()))
                                     .build()).instl(1).build()))
                     .device(Device.builder()
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build());
         }
    @@ -203,7 +203,7 @@ public void processShouldNotUpdateImpWithoutBanner() {
             final BidRequest bidRequest = BidRequest.builder()
                     .imp(singletonList(Imp.builder().instl(1).build()))
                     .device(Device.builder()
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build();
     
    @@ -214,7 +214,7 @@ public void processShouldNotUpdateImpWithoutBanner() {
             assertThat(result).isEqualTo(BidRequest.builder()
                     .imp(singletonList(Imp.builder().instl(1).build()))
                     .device(Device.builder()
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build());
         }
    @@ -228,7 +228,7 @@ public void processShouldThrowInvalidRequestExceptionWhenCantFindMaxWidthOrMaxHe
                                     .format(singletonList(Format.builder().w(1).h(1).build()))
                                     .build()).instl(1).build()))
                     .device(Device.builder()
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build();
     
    @@ -248,7 +248,7 @@ public void processShouldNotUpdateImpWhenInterstitialSizesWereNotFound() {
                                     .format(singletonList(Format.builder().w(10).h(10).build()))
                                     .build()).instl(1).build()))
                     .device(Device.builder()
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build();
     
    @@ -262,7 +262,7 @@ public void processShouldNotUpdateImpWhenInterstitialSizesWereNotFound() {
                                     .format(singletonList(Format.builder().w(10).h(10).build()))
                                     .build()).instl(1).build()))
                     .device(Device.builder()
    -                        .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
    +                        .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80))))
                             .build())
                     .build());
         }
    diff --git a/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java b/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java
    index 3071553c82b..1b19fac4946 100644
    --- a/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java
    +++ b/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java
    @@ -1,19 +1,23 @@
     package org.prebid.server.auction;
     
     import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.node.ArrayNode;
     import com.fasterxml.jackson.databind.node.IntNode;
     import com.fasterxml.jackson.databind.node.ObjectNode;
     import org.junit.Test;
     import org.prebid.server.VertxTest;
    +import org.prebid.server.json.JsonMerger;
     
     import java.util.ArrayList;
    +import java.util.Arrays;
     import java.util.List;
     
     import static org.assertj.core.api.Assertions.assertThat;
     
     public class OrtbTypesResolverTest extends VertxTest {
     
    -    private final OrtbTypesResolver ortbTypesResolver = new OrtbTypesResolver(jacksonMapper);
    +    private final OrtbTypesResolver ortbTypesResolver =
    +            new OrtbTypesResolver(jacksonMapper, new JsonMerger(jacksonMapper));
     
         @Test
         public void normalizeTargetingShouldNotChangeNodeIfItsTypeIsNotObject() {
    @@ -31,7 +35,7 @@ public void normalizeTargetingShouldNotChangeNodeIfItsTypeIsNotObject() {
         public void normalizeTargetingShouldConvertArrayToFirstElementFieldForUserAndWriteMessage() {
             // given
             final JsonNode inputParam = mapper.createObjectNode().set("user",
    -                mapper.createObjectNode().set("gender", mapper.createArrayNode().add("male").add("female")));
    +                mapper.createObjectNode().set("gender", array("male", "female")));
             final List errors = new ArrayList<>();
     
             // when
    @@ -49,7 +53,7 @@ public void normalizeTargetingShouldConvertArrayToFirstElementFieldForUserAndWri
         public void normalizeTargetingShouldConvertArrayToCommaSeparatedStringFieldForUserAndWriteMessage() {
             // given
             final JsonNode inputParam = mapper.createObjectNode().set("user",
    -                mapper.createObjectNode().set("keywords", mapper.createArrayNode().add("keyword1").add("keyword2")));
    +                mapper.createObjectNode().set("keywords", array("keyword1", "keyword2")));
             final List errors = new ArrayList<>();
     
             // when
    @@ -110,9 +114,9 @@ public void normalizeTargetingToFirstElementTextNodeShouldWriteMessageAndRemoveF
         @Test
         public void normalizeTargetingShouldNormalizeFieldsForUser() {
             // given
    -        final ObjectNode user = mapper.createObjectNode().set("gender", mapper.createArrayNode().add("gender1")
    -                .add("gender2"));
    -        user.set("keywords", mapper.createArrayNode().add("keyword1").add("keyword2"));
    +        final ObjectNode user = mapper.createObjectNode()
    +                .set("gender", array("gender1", "gender2"));
    +        user.set("keywords", array("keyword1", "keyword2"));
             final ObjectNode containerNode = mapper.createObjectNode().set("user", user);
     
             // when
    @@ -128,12 +132,12 @@ public void normalizeTargetingShouldNormalizeFieldsForUser() {
         public void normalizeTargetingShouldNormalizeFieldsForAppExceptId() {
             // given
             final ObjectNode app = mapper.createObjectNode();
    -        app.set("id", mapper.createArrayNode().add("id1").add("id2"));
    -        app.set("name", mapper.createArrayNode().add("name1").add("name2"));
    -        app.set("bundle", mapper.createArrayNode().add("bundle1").add("bundle2"));
    -        app.set("storeurl", mapper.createArrayNode().add("storeurl1").add("storeurl2"));
    -        app.set("domain", mapper.createArrayNode().add("domain1").add("domain2"));
    -        app.set("keywords", mapper.createArrayNode().add("keyword1").add("keyword2"));
    +        app.set("id", array("id1", "id2"));
    +        app.set("name", array("name1", "name2"));
    +        app.set("bundle", array("bundle1", "bundle2"));
    +        app.set("storeurl", array("storeurl1", "storeurl2"));
    +        app.set("domain", array("domain1", "domain2"));
    +        app.set("keywords", array("keyword1", "keyword2"));
             final ObjectNode containerNode = mapper.createObjectNode().set("app", app);
     
             // when
    @@ -146,20 +150,20 @@ public void normalizeTargetingShouldNormalizeFieldsForAppExceptId() {
                     .put("storeurl", "storeurl1")
                     .put("domain", "domain1")
                     .put("keywords", "keyword1,keyword2")
    -                .set("id", mapper.createArrayNode().add("id1").add("id2"))));
    +                .set("id", array("id1", "id2"))));
         }
     
         @Test
         public void normalizeTargetingShouldNormalizeFieldsForSiteExceptId() {
             // given
             final ObjectNode site = mapper.createObjectNode();
    -        site.set("id", mapper.createArrayNode().add("id1").add("id2"));
    -        site.set("name", mapper.createArrayNode().add("name1").add("name2"));
    -        site.set("domain", mapper.createArrayNode().add("domain1").add("domain2"));
    -        site.set("page", mapper.createArrayNode().add("page1").add("page2"));
    -        site.set("ref", mapper.createArrayNode().add("ref1").add("ref2"));
    -        site.set("search", mapper.createArrayNode().add("search1").add("search2"));
    -        site.set("keywords", mapper.createArrayNode().add("keyword1").add("keyword2"));
    +        site.set("id", array("id1", "id2"));
    +        site.set("name", array("name1", "name2"));
    +        site.set("domain", array("domain1", "domain2"));
    +        site.set("page", array("page1", "page2"));
    +        site.set("ref", array("ref1", "ref2"));
    +        site.set("search", array("search1", "search2"));
    +        site.set("keywords", array("keyword1", "keyword2"));
             final ObjectNode containerNode = mapper.createObjectNode().set("site", site);
     
             // when
    @@ -173,7 +177,7 @@ public void normalizeTargetingShouldNormalizeFieldsForSiteExceptId() {
                     .put("domain", "domain1")
                     .put("search", "search1")
                     .put("keywords", "keyword1,keyword2")
    -                .set("id", mapper.createArrayNode().add("id1").add("id2")))
    +                .set("id", array("id1", "id2")))
             );
         }
     
    @@ -216,45 +220,46 @@ public void normalizeTargetingShouldTolerateIncorrectTypeUserFieldAndRemoveIt()
         @Test
         public void normalizeBidRequestShouldMergeUserDataToUserExtDataAndRemoveData() {
             // given
    -        final ObjectNode containerNode = obj("user", obj("data", obj("dataField", "dataValue"))
    -                .set("ext", obj("data", obj("extDataField", "extDataValue"))));
    +        final ObjectNode containerNode = obj("user", obj("data", obj("dataField", "dataValue1"))
    +                .set("ext", obj("data", obj("extDataField", "extDataValue")
    +                        .put("dataField", "dataValue2"))));
     
             // when
             ortbTypesResolver.normalizeBidRequest(containerNode, new ArrayList<>(), "referer");
     
             // then
    -
             assertThat(containerNode).isEqualTo(obj("user", obj("ext", obj("data", obj("extDataField", "extDataValue")
    -                .put("dataField", "dataValue")))));
    +                .put("dataField", "dataValue1")))));
         }
     
         @Test
         public void normalizeBidRequestShouldMergeSiteDataToSiteExtDataAndRemoveData() {
             // given
    -        final ObjectNode containerNode = obj("site", obj("data", obj("dataField", "dataValue"))
    -                .set("ext", obj("data", obj("extDataField", "extDataValue"))));
    +        final ObjectNode containerNode = obj("site", obj("data", obj("dataField", "dataValue1"))
    +                .set("ext", obj("data", obj("extDataField", "extDataValue")
    +                        .put("dataField", "dataValue2"))));
     
             // when
             ortbTypesResolver.normalizeBidRequest(containerNode, new ArrayList<>(), "referer");
     
             // then
    -
             assertThat(containerNode).isEqualTo(obj("site", obj("ext", obj("data", obj("extDataField", "extDataValue")
    -                .put("dataField", "dataValue")))));
    +                .put("dataField", "dataValue1")))));
         }
     
         @Test
         public void normalizeBidRequestShouldMergeAppDataToAppExtDataAndRemoveData() {
             // given
    -        final ObjectNode containerNode = obj("app", obj("data", obj("dataField", "dataValue"))
    -                .set("ext", obj("data", obj("extDataField", "extDataValue"))));
    +        final ObjectNode containerNode = obj("app", obj("data", obj("dataField", "dataValue1"))
    +                .set("ext", obj("data", obj("extDataField", "extDataValue")
    +                        .put("dataField", "dataValue2"))));
     
             // when
             ortbTypesResolver.normalizeBidRequest(containerNode, new ArrayList<>(), "referer");
     
             // then
             assertThat(containerNode).isEqualTo(obj("app", obj("ext", obj("data", obj("extDataField", "extDataValue")
    -                .put("dataField", "dataValue")))));
    +                .put("dataField", "dataValue1")))));
         }
     
         @Test
    @@ -269,6 +274,21 @@ public void normalizeBidRequestShouldNotChangeUserWhenUserDataNotDefined() {
             assertThat(containerNode).isEqualTo(obj("user", obj("ext", obj("data", obj("extDataField", "extDataValue")))));
         }
     
    +    @Test
    +    public void normalizeBidRequestShouldNotChangeUserWhenUserDataNotObject() {
    +        // given
    +        final ObjectNode containerNode = obj("user", obj("ext", obj("data", obj("extDataField", "extDataValue"))))
    +                .set("data", mapper.createArrayNode().add(obj("id", "123")));
    +
    +        // when
    +        ortbTypesResolver.normalizeBidRequest(containerNode, new ArrayList<>(), "referer");
    +
    +        // then
    +        assertThat(containerNode).isEqualTo(
    +                obj("user", obj("ext", obj("data", obj("extDataField", "extDataValue"))))
    +                        .set("data", array(obj("id", "123"))));
    +    }
    +
         @Test
         public void normalizeBidRequestShouldSetDataToUserIfExtDataNotExist() {
             // given
    @@ -312,85 +332,244 @@ public void normalizeBidRequestShouldSetExtDataToUserIfExtIncorrectType() {
         }
     
         @Test
    -    public void normalizeBidRequestShouldResolveORTBFieldsWithIdForRequestAndExcludedIdForBidderConfig() {
    +    public void normalizeBidRequestShouldResolveEmptyOrtbWithFpdFieldsWithIdForRequestAndExcludedIdForBidderConfig() {
             // given
    +        final ObjectNode ortbSite = mapper.createObjectNode();
    +        ortbSite.set("id", array("id1", "id2"));
    +        ortbSite.set("name", array("name1", "name2"));
    +        ortbSite.set("domain", array("domain1", "domain2"));
    +        ortbSite.set("page", array("page1", "page2"));
    +        ortbSite.set("ref", array("ref1", "ref2"));
    +        ortbSite.set("search", array("search1", "search2"));
    +        ortbSite.set("keywords", array("keyword1", "keyword2"));
    +
    +        final ObjectNode ortbApp = mapper.createObjectNode();
    +        ortbApp.set("id", array("id1", "id2"));
    +        ortbApp.set("name", array("name1", "name2"));
    +        ortbApp.set("bundle", array("bundle1", "bundle2"));
    +        ortbApp.set("storeurl", array("storeurl1", "storeurl2"));
    +        ortbApp.set("domain", array("domain1", "domain2"));
    +        ortbApp.set("keywords", array("keyword1", "keyword2"));
    +
    +        final ObjectNode ortbUser = mapper.createObjectNode();
    +        ortbUser.set("gender", array("gender1", "gender2"));
    +        ortbUser.set("keywords", array("keyword1", "keyword2"));
    +
    +        final ObjectNode bidderConfigContext = ortbSite.deepCopy();
    +        final ObjectNode bidderConfigApp = ortbApp.deepCopy();
    +        final ObjectNode bidderConfigUser = ortbUser.deepCopy();
    +
    +        final ObjectNode ortbConfig = mapper.createObjectNode();
    +        ortbConfig.set("site", bidderConfigContext);
    +        ortbConfig.set("app", bidderConfigApp);
    +        ortbConfig.set("user", bidderConfigUser);
    +
             final ObjectNode requestNode = mapper.createObjectNode();
    +        requestNode.set("site", ortbSite);
    +        requestNode.set("app", ortbApp);
    +        requestNode.set("user", ortbUser);
    +
    +        requestNode.set("ext", obj("prebid", obj("bidderconfig", array(obj("config", obj("ortb2", ortbConfig))))));
     
    -        final ObjectNode requestSite = mapper.createObjectNode();
    -        requestSite.set("id", mapper.createArrayNode().add("id1").add("id2"));
    -        requestSite.set("name", mapper.createArrayNode().add("name1").add("name2"));
    -        requestSite.set("domain", mapper.createArrayNode().add("domain1").add("domain2"));
    -        requestSite.set("page", mapper.createArrayNode().add("page1").add("page2"));
    -        requestSite.set("ref", mapper.createArrayNode().add("ref1").add("ref2"));
    -        requestSite.set("search", mapper.createArrayNode().add("search1").add("search2"));
    -        requestSite.set("keywords", mapper.createArrayNode().add("keyword1").add("keyword2"));
    -
    -        final ObjectNode requestApp = mapper.createObjectNode();
    -        requestApp.set("id", mapper.createArrayNode().add("id1").add("id2"));
    -        requestApp.set("name", mapper.createArrayNode().add("name1").add("name2"));
    -        requestApp.set("bundle", mapper.createArrayNode().add("bundle1").add("bundle2"));
    -        requestApp.set("storeurl", mapper.createArrayNode().add("storeurl1").add("storeurl2"));
    -        requestApp.set("domain", mapper.createArrayNode().add("domain1").add("domain2"));
    -        requestApp.set("keywords", mapper.createArrayNode().add("keyword1").add("keyword2"));
    -
    -        final ObjectNode requestUser = mapper.createObjectNode();
    -        requestUser.set("gender", mapper.createArrayNode().add("gender1").add("gender2"));
    -        requestUser.set("keywords", mapper.createArrayNode().add("keyword1").add("keyword2"));
    -
    -        final ObjectNode bidderConfigSite1 = requestSite.deepCopy();
    -        final ObjectNode bidderConfigApp1 = requestApp.deepCopy();
    -        final ObjectNode bidderConfigUser1 = requestUser.deepCopy();
    -
    -        final ObjectNode bidderConfig1 = mapper.createObjectNode();
    -        bidderConfig1.set("site", bidderConfigSite1);
    -        bidderConfig1.set("app", bidderConfigApp1);
    -        bidderConfig1.set("user", bidderConfigUser1);
    -
    -        requestNode.set("site", requestSite);
    -        requestNode.set("user", requestUser);
    -        requestNode.set("app", requestApp);
    -
    -        requestNode.set("ext", mapper.createObjectNode().set("prebid", mapper.createObjectNode().set("bidderconfig",
    -                mapper.createArrayNode()
    -                        .add(mapper.createObjectNode().set("config", mapper.createObjectNode().set("fpd",
    -                                bidderConfig1))))));
             // when
             ortbTypesResolver.normalizeBidRequest(requestNode, new ArrayList<>(), "referer");
     
             // then
             assertThat(requestNode.get("site"))
    -                .isEqualTo(mapper.createObjectNode().put("id", "id1").put("name", "name1").put("domain", "domain1")
    -                        .put("page", "page1").put("ref", "ref1").put("search", "search1")
    +                .isEqualTo(mapper.createObjectNode()
    +                        .put("id", "id1")
    +                        .put("name", "name1")
    +                        .put("domain", "domain1")
    +                        .put("page", "page1")
    +                        .put("ref", "ref1")
    +                        .put("search", "search1")
                             .put("keywords", "keyword1,keyword2"));
     
             assertThat(requestNode.get("app"))
    -                .isEqualTo(mapper.createObjectNode().put("id", "id1").put("name", "name1").put("bundle", "bundle1")
    -                        .put("storeurl", "storeurl1").put("domain", "domain1")
    +                .isEqualTo(mapper.createObjectNode()
    +                        .put("id", "id1")
    +                        .put("name", "name1")
    +                        .put("bundle", "bundle1")
    +                        .put("storeurl", "storeurl1")
    +                        .put("domain", "domain1")
                             .put("keywords", "keyword1,keyword2"));
     
             assertThat(requestNode.get("user"))
    -                .isEqualTo(mapper.createObjectNode().put("gender", "gender1").put("keywords", "keyword1,keyword2"));
    -
    -        assertThat(requestNode.path("ext").path("prebid").path("bidderconfig").path(0).path("config").path("fpd")
    -                .path("site"))
    -                .isEqualTo(mapper.createObjectNode().put("name", "name1").put("domain", "domain1").put("page", "page1")
    -                        .put("ref", "ref1").put("search", "search1").put("keywords", "keyword1,keyword2")
    -                        .set("id", mapper.createArrayNode().add("id1").add("id2")));
    -
    -        assertThat(requestNode.path("ext").path("prebid").path("bidderconfig").path(0).path("config").path("fpd")
    -                .path("app"))
    -                .isEqualTo(mapper.createObjectNode().put("name", "name1").put("bundle", "bundle1")
    -                        .put("storeurl", "storeurl1").put("domain", "domain1")
    +                .isEqualTo(mapper.createObjectNode()
    +                        .put("gender", "gender1")
    +                        .put("keywords", "keyword1,keyword2"));
    +
    +        final JsonNode ortb2 = requestNode.path("ext").path("prebid").path("bidderconfig").path(0).path("config")
    +                .path("ortb2");
    +
    +        assertThat(ortb2.path("site"))
    +                .isEqualTo(mapper.createObjectNode()
    +                        .put("name", "name1")
    +                        .put("domain", "domain1")
    +                        .put("page", "page1")
    +                        .put("ref", "ref1")
    +                        .put("search", "search1")
    +                        .put("keywords", "keyword1,keyword2")
    +                        .set("id", array("id1", "id2")));
    +
    +        assertThat(ortb2.path("app"))
    +                .isEqualTo(mapper.createObjectNode()
    +                        .put("name", "name1")
    +                        .put("bundle", "bundle1")
    +                        .put("storeurl", "storeurl1")
    +                        .put("domain", "domain1")
                             .put("keywords", "keyword1,keyword2")
    -                        .set("id", mapper.createArrayNode().add("id1").add("id2")));
    +                        .set("id", array("id1", "id2")));
    +
    +        assertThat(ortb2.path("user"))
    +                .isEqualTo(mapper.createObjectNode()
    +                        .put("gender", "gender1")
    +                        .put("keywords", "keyword1,keyword2"));
    +    }
    +
    +    @Test
    +    public void normalizeBidRequestShouldBeMergedWithFpdContextToOrtbSite() {
    +        // given
    +        final ObjectNode fpdContext = mapper.createObjectNode();
    +        fpdContext.set("id", array("id1", "id2"));
    +        fpdContext.set("name", array("name1", "name2"));
    +        fpdContext.set("domain", array("domain1"));
    +        fpdContext.set("page", array("page1", "page2"));
    +        fpdContext.set("data", obj("fpdData", "data_value"));
    +
    +        final ObjectNode ortbSite = mapper.createObjectNode();
    +        ortbSite.put("id", "ortb_id");
    +        // name is absent here
    +        ortbSite.set("domain", array("ortb_domain1"));
    +        ortbSite.set("page", array("ortb_page1", "ortb_page2"));
    +        ortbSite.set("ref", array("ortb_ref1", "ortb_ref2"));
    +        ortbSite.set("keywords", array("ortb_keyword1", "ortb_keyword2"));
    +        ortbSite.set("data", obj("ortbData", "ortb_data_value"));
    +
    +        final ObjectNode configNode = mapper.createObjectNode();
    +        configNode.set("fpd", obj("context", fpdContext));
    +        configNode.set("ortb2", obj("site", ortbSite));
    +
    +        final ObjectNode requestNode = obj("ext", obj("prebid", obj("bidderconfig", array(obj("config", configNode)))));
    +
    +        final ObjectNode requestedFpdContext = fpdContext.deepCopy();
    +
    +        // when
    +        ortbTypesResolver.normalizeBidRequest(requestNode, new ArrayList<>(), "referer");
    +
    +        // then
    +        final JsonNode config = requestNode.path("ext").path("prebid").path("bidderconfig").path(0);
    +        final JsonNode fpd = config.path("config").path("fpd");
    +        final JsonNode ortb2 = config.path("config").path("ortb2");
    +
    +        final ObjectNode expectedOrtbExtData = mapper.createObjectNode()
    +                .put("ortbData", "ortb_data_value")
    +                .put("fpdData", "data_value");
    +
    +        final ObjectNode expectedOrtb = mapper.createObjectNode()
    +                .put("name", "name1")
    +                .put("domain", "domain1")
    +                .put("page", "page1")
    +                .put("ref", "ortb_ref1")
    +                .put("keywords", "ortb_keyword1,ortb_keyword2");
    +        expectedOrtb.set("id", array("id1", "id2"));
    +        expectedOrtb.set("ext", obj("data", expectedOrtbExtData));
    +
    +        assertThat(ortb2.path("site")).isEqualTo(expectedOrtb);
    +        assertThat(fpd.path("context")).isEqualTo(requestedFpdContext);
    +    }
    +
    +    @Test
    +    public void normalizeBidRequestShouldBeMergedWithFpdUserToOrtbUser() {
    +        // given
    +        final ObjectNode fpdUser = mapper.createObjectNode();
    +        fpdUser.set("gender", array("gender1", "gender2"));
    +        fpdUser.set("data", obj("fpdData", "data_value"));
    +
    +        final ObjectNode ortbUser = mapper.createObjectNode();
    +        ortbUser.set("gender", array("ortb_gender1", "ortb_gender2"));
    +        ortbUser.set("keywords", array("ortb_keyword1", "ortb_keyword2"));
    +        ortbUser.set("data", obj("ortbData", "ortb_data_value"));
    +
    +        final ObjectNode configNode = mapper.createObjectNode();
    +        configNode.set("fpd", obj("user", fpdUser));
    +        configNode.set("ortb2", obj("user", ortbUser));
    +
    +        final ObjectNode requestNode = obj("ext", obj("prebid", obj("bidderconfig", array(obj("config", configNode)))));
    +
    +        final ObjectNode requestFpdUser = fpdUser.deepCopy();
    +
    +        // when
    +        ortbTypesResolver.normalizeBidRequest(requestNode, new ArrayList<>(), "referer");
    +
    +        // then
    +        final JsonNode config = requestNode.path("ext").path("prebid").path("bidderconfig").path(0);
    +        final JsonNode fpd = config.path("config").path("fpd");
    +        final JsonNode ortb2 = config.path("config").path("ortb2");
    +
    +        final ObjectNode expectedOrtbExtData = mapper.createObjectNode()
    +                .put("ortbData", "ortb_data_value")
    +                .put("fpdData", "data_value");
    +
    +        assertThat(ortb2.path("user"))
    +                .isEqualTo(mapper.createObjectNode()
    +                        .put("gender", "gender1")
    +                        .put("keywords", "ortb_keyword1,ortb_keyword2")
    +                        .set("ext", obj("data", expectedOrtbExtData)));
    +
    +        assertThat(fpd.path("user")).isEqualTo(requestFpdUser);
    +    }
    +
    +    @Test
    +    public void normalizeBidRequestShouldNotBeMergedWithFpdAppToOrtbApp() {
    +        // given
    +        final ObjectNode fpdApp = mapper.createObjectNode();
    +        fpdApp.set("id", array("id1", "id2"));
    +        fpdApp.set("name", array("name1", "name2"));
    +        fpdApp.set("bundle", array("bundle1", "bundle2"));
    +        fpdApp.set("storeurl", array("storeurl1", "storeurl2"));
    +        fpdApp.set("domain", array("domain1", "domain2"));
    +        fpdApp.set("keywords", array("keyword1", "keyword2"));
    +
    +        final ObjectNode ortbApp = mapper.createObjectNode();
    +        fpdApp.put("id", "ortb_id");
    +        // name is absent here
    +        fpdApp.put("bundle", "ortb_bundle1");
    +        fpdApp.put("storeurl", "ortb_storeurl1");
    +        fpdApp.put("keywords", "ortb_keyword1");
    +
    +        final ObjectNode configNode = mapper.createObjectNode();
    +        configNode.set("fpd", obj("app", fpdApp));
    +        configNode.set("ortb2", obj("app", ortbApp));
    +
    +        final ObjectNode requestNode = obj("ext", obj("prebid", obj("bidderconfig", array(obj("config", configNode)))));
    +
    +        final ObjectNode requestFpdApp = fpdApp.deepCopy();
    +        final ObjectNode requestOrtbApp = ortbApp.deepCopy();
    +
    +        // then
    +        final JsonNode config = requestNode.path("ext").path("prebid").path("bidderconfig").path(0);
    +        final JsonNode fpd = config.path("config").path("fpd");
    +        final JsonNode ortb2 = config.path("config").path("ortb2");
    +
    +        assertThat(ortb2.path("app")).isEqualTo(requestOrtbApp);
    +        assertThat(fpd.path("app")).isEqualTo(requestFpdApp);
    +    }
    +
    +    private static ArrayNode array(String... fields) {
    +        final ArrayNode arrayNode = mapper.createArrayNode();
    +        Arrays.stream(fields).forEach(arrayNode::add);
    +        return arrayNode;
    +    }
     
    -        assertThat(requestNode.path("ext").path("prebid").path("bidderconfig").path(0).path("config").path("fpd")
    -                .path("user"))
    -                .isEqualTo(mapper.createObjectNode().put("gender", "gender1").put("keywords", "keyword1,keyword2"));
    +    private static ArrayNode array(ObjectNode... nodes) {
    +        final ArrayNode arrayNode = mapper.createArrayNode();
    +        Arrays.stream(nodes).forEach(arrayNode::add);
    +        return arrayNode;
         }
     
         private static ObjectNode obj(String fieldName, JsonNode value) {
    -        return (ObjectNode) mapper.createObjectNode().set(fieldName, value);
    +        return mapper.createObjectNode().set(fieldName, value);
         }
     
         private static ObjectNode obj(String fieldName, String value) {
    diff --git a/src/test/java/org/prebid/server/auction/PreBidRequestContextFactoryTest.java b/src/test/java/org/prebid/server/auction/PreBidRequestContextFactoryTest.java
    deleted file mode 100644
    index 61ecba43033..00000000000
    --- a/src/test/java/org/prebid/server/auction/PreBidRequestContextFactoryTest.java
    +++ /dev/null
    @@ -1,543 +0,0 @@
    -package org.prebid.server.auction;
    -
    -import com.fasterxml.jackson.core.JsonParseException;
    -import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.fasterxml.jackson.databind.node.ObjectNode;
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.Format;
    -import io.vertx.core.Future;
    -import io.vertx.core.buffer.Buffer;
    -import io.vertx.core.http.HttpServerRequest;
    -import io.vertx.ext.web.RoutingContext;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.auction.model.AdapterRequest;
    -import org.prebid.server.auction.model.IpAddress;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    -import org.prebid.server.bidder.rubicon.proto.RubiconParams;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.cookie.UidsCookieService;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.execution.TimeoutFactory;
    -import org.prebid.server.proto.request.AdUnit;
    -import org.prebid.server.proto.request.Bid;
    -import org.prebid.server.proto.request.PreBidRequest;
    -import org.prebid.server.proto.request.PreBidRequest.PreBidRequestBuilder;
    -import org.prebid.server.proto.response.MediaType;
    -import org.prebid.server.settings.ApplicationSettings;
    -
    -import java.time.Clock;
    -import java.time.Instant;
    -import java.time.ZoneId;
    -import java.util.Collections;
    -import java.util.EnumSet;
    -import java.util.function.Function;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.singletonList;
    -import static java.util.function.Function.identity;
    -import static org.apache.commons.lang3.math.NumberUtils.isDigits;
    -import static org.apache.commons.lang3.math.NumberUtils.toLong;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.mockito.ArgumentMatchers.any;
    -import static org.mockito.ArgumentMatchers.anyLong;
    -import static org.mockito.ArgumentMatchers.anyString;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.ArgumentMatchers.same;
    -import static org.mockito.BDDMockito.given;
    -import static org.mockito.Mockito.verify;
    -
    -public class PreBidRequestContextFactoryTest extends VertxTest {
    -
    -    private static final long DEFAULT_HTTP_REQUEST_TIMEOUT = 250L;
    -
    -    private static final String RUBICON = "rubicon";
    -    private static final String APPNEXUS = "appnexus";
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private TimeoutResolver timeoutResolver;
    -    @Mock
    -    private ImplicitParametersExtractor paramsExtractor;
    -    @Mock
    -    private IpAddressHelper ipAddressHelper;
    -    @Mock
    -    private ApplicationSettings applicationSettings;
    -    @Mock
    -    private UidsCookieService uidsCookieService;
    -    @Mock
    -    private UidsCookie uidsCookie;
    -
    -    private PreBidRequestContextFactory factory;
    -    @Mock
    -    private RoutingContext routingContext;
    -    @Mock
    -    private HttpServerRequest httpRequest;
    -
    -    @Before
    -    public void setUp() {
    -        given(timeoutResolver.resolve(any())).willReturn(DEFAULT_HTTP_REQUEST_TIMEOUT);
    -        given(timeoutResolver.adjustTimeout(anyLong())).willReturn(DEFAULT_HTTP_REQUEST_TIMEOUT);
    -
    -        // minimal request
    -        given(routingContext.getBody()).willReturn(givenPreBidRequest(identity()));
    -        given(routingContext.request()).willReturn(httpRequest);
    -
    -        given(paramsExtractor.refererFrom(any())).willReturn("http://referer");
    -
    -        // parsed uids cookie
    -        given(uidsCookieService.parseFromRequest(any())).willReturn(uidsCookie);
    -        given(uidsCookie.hasLiveUids()).willReturn(false);
    -
    -        TimeoutFactory timeoutFactory = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault()));
    -        factory = new PreBidRequestContextFactory(
    -                timeoutResolver,
    -                paramsExtractor,
    -                ipAddressHelper,
    -                applicationSettings,
    -                uidsCookieService,
    -                timeoutFactory,
    -                jacksonMapper);
    -    }
    -
    -    @Test
    -    public void shouldReturnPopulatedContext() {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder()
    -                .code("adUnitCode1")
    -                .sizes(singletonList(Format.builder().w(100).h(100).build()))
    -                .bids(singletonList(givenBid(RUBICON)))
    -                .build();
    -
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(singletonList(adUnit))));
    -
    -        given(paramsExtractor.refererFrom(any())).willReturn("http://www.example.com");
    -        given(paramsExtractor.domainFrom(anyString())).willReturn("example.com");
    -        given(paramsExtractor.ipFrom(any())).willReturn(singletonList("192.168.244.1"));
    -        given(ipAddressHelper.toIpAddress(eq("192.168.244.1")))
    -                .willReturn(IpAddress.of("192.168.244.1", IpAddress.IP.v4));
    -        given(paramsExtractor.uaFrom(any())).willReturn("userAgent");
    -        given(paramsExtractor.secureFrom(any())).willReturn(1);
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests()).hasSize(1);
    -        assertThat(preBidRequestContext.getPreBidRequest()).isNotNull();
    -        assertThat(preBidRequestContext.getReferer()).isEqualTo("http://www.example.com");
    -        assertThat(preBidRequestContext.getDomain()).isEqualTo("example.com");
    -        assertThat(preBidRequestContext.getIp()).isEqualTo("192.168.244.1");
    -        assertThat(preBidRequestContext.getUa()).isEqualTo("userAgent");
    -        assertThat(preBidRequestContext.getSecure()).isEqualTo(1);
    -        assertThat(preBidRequestContext.isDebug()).isFalse();
    -        assertThat(preBidRequestContext.getUidsCookie()).isNotNull();
    -        assertThat(preBidRequestContext.isNoLiveUids()).isTrue();
    -    }
    -
    -    @Test
    -    public void shouldFailIfRequestBodyCouldNotBeParsed() {
    -        // given
    -        given(routingContext.getBody()).willReturn(Buffer.buffer("{"));
    -
    -        // when
    -        final Future preBidRequestContextFuture = factory.fromRequest(routingContext);
    -
    -        // then
    -        assertThat(preBidRequestContextFuture.failed()).isTrue();
    -        assertThat(preBidRequestContextFuture.cause())
    -                .isInstanceOf(PreBidException.class)
    -                .hasMessageStartingWith("Failed to decode")
    -                .hasCauseInstanceOf(JsonParseException.class);
    -    }
    -
    -    @Test
    -    public void shouldFailIfRequestBodyIsMissing() {
    -        // given
    -        given(routingContext.getBody()).willReturn(null);
    -
    -        // when
    -        final Future preBidRequestContextFuture = factory.fromRequest(routingContext);
    -
    -        // then
    -        assertThat(preBidRequestContextFuture.failed()).isTrue();
    -        assertThat(preBidRequestContextFuture.cause())
    -                .isInstanceOf(PreBidException.class).hasMessage("Incoming request has no body");
    -    }
    -
    -    @Test
    -    public void shouldPopulateBidder() {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder()
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .topframe(1)
    -                .instl(1)
    -                .code("adUnitCode")
    -                .bids(singletonList(Bid.of("bidId", RUBICON, rubiconParams(1001, 2001, 3001))))
    -                .build();
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(singletonList(adUnit))));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests()).containsOnly(
    -                AdapterRequest.of(RUBICON, singletonList(AdUnitBid.builder()
    -                        .bidderCode(RUBICON)
    -                        .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                        .topframe(1)
    -                        .instl(1)
    -                        .adUnitCode("adUnitCode")
    -                        .bidId("bidId")
    -                        .params(rubiconParams(1001, 2001, 3001))
    -                        .mediaTypes(Collections.singleton(MediaType.banner))
    -                        .build())));
    -    }
    -
    -    @Test
    -    public void shouldPopulateAdUnitBidWithBannerTypeIfMediaTypeIsNull() {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder()
    -                .code("adUnitCode1")
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .bids(singletonList(givenBid(RUBICON)))
    -                .build();
    -
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(singletonList(adUnit))));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests().get(0).getAdUnitBids()).hasSize(1).element(0)
    -                .returns(Collections.singleton(MediaType.banner), AdUnitBid::getMediaTypes);
    -    }
    -
    -    @Test
    -    public void shouldPopulateAdUnitBidWithBannerIfMediaTypeIsUnknown() {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder()
    -                .code("adUnitCode1")
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .bids(singletonList(givenBid(RUBICON)))
    -                .mediaTypes(singletonList("RandomMediaType"))
    -                .build();
    -
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(singletonList(adUnit))));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests().get(0).getAdUnitBids()).hasSize(1).element(0)
    -                .returns(Collections.singleton(MediaType.banner), AdUnitBid::getMediaTypes);
    -    }
    -
    -    @Test
    -    public void shouldPopulateAdUnitWithBannerAndVideoIfBothArePresent() {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder()
    -                .code("adUnitCode1")
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .bids(singletonList(givenBid(RUBICON)))
    -                .mediaTypes(asList("banner", "video"))
    -                .build();
    -
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(singletonList(adUnit))));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests().get(0).getAdUnitBids()).hasSize(1).element(0)
    -                .returns(EnumSet.of(MediaType.banner, MediaType.video), AdUnitBid::getMediaTypes);
    -    }
    -
    -    @Test
    -    public void shouldNotReturnAdUnitsWithNullSizes() {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder()
    -                .bids(singletonList(givenBid(RUBICON)))
    -                .code("AdUnitCode1")
    -                .build();
    -        final AdUnit adUnit2 = AdUnit.builder()
    -                .bids(singletonList(givenBid(RUBICON)))
    -                .code("AdUnitCode2")
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .build();
    -
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(asList(adUnit, adUnit2))));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests().get(0).getAdUnitBids()).hasSize(1).element(0)
    -                .returns("AdUnitCode2", AdUnitBid::getAdUnitCode);
    -    }
    -
    -    @Test
    -    public void shouldNotReturnAdUnitsWithNullAdUnitCode() {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder()
    -                .bids(singletonList(givenBid(RUBICON)))
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .build();
    -        final AdUnit adUnit2 = AdUnit.builder()
    -                .bids(singletonList(givenBid(RUBICON)))
    -                .code("AdUnitCode2")
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .build();
    -
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(asList(adUnit, adUnit2))));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests().get(0).getAdUnitBids()).hasSize(1).element(0)
    -                .returns("AdUnitCode2", AdUnitBid::getAdUnitCode);
    -    }
    -
    -    @Test
    -    public void shouldExtractMultipleBidders() {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder()
    -                .code("adUnitCode1")
    -                .sizes(singletonList(Format.builder().h(100).w(200).build()))
    -                .bids(asList(givenBid(RUBICON), givenBid(APPNEXUS)))
    -                .build();
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(asList(adUnit, adUnit))));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests()).hasSize(2)
    -                .flatExtracting(AdapterRequest::getAdUnitBids).hasSize(4);
    -    }
    -
    -    @Test
    -    public void shouldExpandAdUnitConfig() throws JsonProcessingException {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder()
    -                .configId("configId")
    -                .code("adUnitCode1")
    -                .sizes(singletonList(Format.builder().h(100).w(200).build()))
    -                .build();
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(singletonList(adUnit))));
    -
    -        given(applicationSettings.getAdUnitConfigById(anyString(), any()))
    -                .willReturn(Future.succeededFuture(mapper.writeValueAsString(singletonList(
    -                        Bid.of("bidId", RUBICON, rubiconParams(4001, 5001, 6001))))));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        verify(applicationSettings).getAdUnitConfigById(anyString(), same(preBidRequestContext.getTimeout()));
    -        assertThat(preBidRequestContext.getAdapterRequests()).containsOnly(
    -                AdapterRequest.of(RUBICON, singletonList(AdUnitBid.builder()
    -                        .bidderCode(RUBICON)
    -                        .bidId("bidId")
    -                        .adUnitCode("adUnitCode1")
    -                        .sizes(singletonList(Format.builder().h(100).w(200).build()))
    -                        .params(rubiconParams(4001, 5001, 6001))
    -                        .mediaTypes(Collections.singleton(MediaType.banner))
    -                        .build())));
    -    }
    -
    -    @Test
    -    public void shouldTolerateMissingAdUnitConfig() {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder().configId("configId").build();
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(singletonList(adUnit))));
    -
    -        given(applicationSettings.getAdUnitConfigById(anyString(), any()))
    -                .willReturn(Future.failedFuture(new PreBidException("Not found")));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests()).isEmpty();
    -    }
    -
    -    @Test
    -    public void shouldTolerateInvalidAdUnitConfig() {
    -        // given
    -        final AdUnit adUnit = AdUnit.builder().configId("configId").build();
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(singletonList(adUnit))));
    -
    -        given(applicationSettings.getAdUnitConfigById(anyString(), any()))
    -                .willReturn(Future.succeededFuture("invalid"));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests()).isEmpty();
    -    }
    -
    -    @Test
    -    public void shouldGenerateBidIdIfAbsentInRequest() {
    -        // given
    -        given(routingContext.getBody()).willReturn(givenPreBidRequest(identity()));
    -
    -        final AdUnit adUnit = AdUnit.builder()
    -                .code("adUnitCode1")
    -                .sizes(singletonList(Format.builder().w(100).h(100).build()))
    -                .bids(singletonList(givenBid(RUBICON)))
    -                .build();
    -
    -        given(routingContext.getBody())
    -                .willReturn(givenPreBidRequest(builder -> builder.adUnits(singletonList(adUnit))));
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getAdapterRequests())
    -                .flatExtracting(AdapterRequest::getAdUnitBids)
    -                .extracting(AdUnitBid::getBidId)
    -                .element(0)
    -                .matches(id -> isDigits(id) && toLong(id) >= 0);
    -    }
    -
    -    @Test
    -    public void shouldSetTimeoutFromTimeoutResolver() {
    -        // given
    -        given(routingContext.getBody()).willReturn(givenPreBidRequest(identity()));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getTimeout().remaining()).isEqualTo(DEFAULT_HTTP_REQUEST_TIMEOUT);
    -    }
    -
    -    @Test
    -    public void shouldSetIsDebugToTrueIfTrueInPreBidRequest() {
    -        // given
    -        given(routingContext.getBody()).willReturn(givenPreBidRequest(builder -> builder.isDebug(true)));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.isDebug()).isTrue();
    -    }
    -
    -    @Test
    -    public void shouldSetIsDebugToTrueIfQueryParameterEqualTo1() {
    -        // given
    -        given(routingContext.getBody()).willReturn(givenPreBidRequest(builder -> builder.isDebug(false)));
    -        given(httpRequest.getParam(eq("debug"))).willReturn("1");
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.isDebug()).isTrue();
    -    }
    -
    -    @Test
    -    public void shouldSetIsDebugToFalseIfQueryParameterNotEqualTo1() {
    -        // given
    -        given(httpRequest.getParam(eq("debug"))).willReturn("2");
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.isDebug()).isFalse();
    -    }
    -
    -    @Test
    -    public void shouldNotSetClientDataIfAppPresentInPreBidRequest() {
    -        // given
    -        given(routingContext.getBody()).willReturn(givenPreBidRequest(builder -> builder.app(App.builder().build())));
    -
    -        // when
    -        final PreBidRequestContext preBidRequestContext = factory.fromRequest(routingContext).result();
    -
    -        // then
    -        assertThat(preBidRequestContext.getUidsCookie()).isNull();
    -        assertThat(preBidRequestContext.isNoLiveUids()).isFalse();
    -        assertThat(preBidRequestContext.getUa()).isNull();
    -        assertThat(preBidRequestContext.getReferer()).isNull();
    -        assertThat(preBidRequestContext.getDomain()).isNull();
    -    }
    -
    -    @Test
    -    public void shouldFailIfRefererIsMissing() {
    -        // given
    -        final PreBidException exception = new PreBidException("Couldn't derive referer");
    -        given(paramsExtractor.refererFrom(any())).willThrow(exception);
    -
    -        // when
    -        final Future preBidRequestContextFuture = factory.fromRequest(routingContext);
    -
    -        // then
    -        assertThat(preBidRequestContextFuture.failed()).isTrue();
    -        assertThat(preBidRequestContextFuture.cause()).isSameAs(exception);
    -    }
    -
    -    @Test
    -    public void shouldFailIfDomainCouldNotBeExtracted() {
    -        // given
    -        final PreBidException exception = new PreBidException("Couldn't derive domain");
    -        given(paramsExtractor.domainFrom(any())).willThrow(exception);
    -
    -        // when
    -        final Future preBidRequestContextFuture = factory.fromRequest(routingContext);
    -
    -        // then
    -        assertThat(preBidRequestContextFuture.failed()).isTrue();
    -        assertThat(preBidRequestContextFuture.cause()).isSameAs(exception);
    -    }
    -
    -    private static Bid givenBid(String bidder) {
    -        return Bid.of(null, bidder, null);
    -    }
    -
    -    private static Buffer givenPreBidRequest(Function builderCustomizer) {
    -        try {
    -            return Buffer.buffer(mapper.writeValueAsString(
    -                    builderCustomizer.apply(
    -                            PreBidRequest.builder()
    -                                    .adUnits(singletonList(AdUnit.builder()
    -                                            .bids(singletonList(givenBid(RUBICON)))
    -                                            .build())))
    -                            .build()));
    -        } catch (JsonProcessingException e) {
    -            throw new RuntimeException();
    -        }
    -    }
    -
    -    private static ObjectNode rubiconParams(Integer accountId, Integer siteId, Integer zoneId) {
    -        return mapper.valueToTree(RubiconParams.builder()
    -                .accountId(accountId)
    -                .siteId(siteId)
    -                .zoneId(zoneId)
    -                .build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/auction/PriceGranularityTest.java b/src/test/java/org/prebid/server/auction/PriceGranularityTest.java
    index 768434279ec..a488bb96613 100644
    --- a/src/test/java/org/prebid/server/auction/PriceGranularityTest.java
    +++ b/src/test/java/org/prebid/server/auction/PriceGranularityTest.java
    @@ -15,12 +15,6 @@
     
     public class PriceGranularityTest {
     
    -    @Test
    -    public void createFromExtPriceGranularityShouldThrowIllegalArgumentsExceptionIfRangesListNull() {
    -        assertThatIllegalArgumentException().isThrownBy(() -> PriceGranularity.createFromExtPriceGranularity(
    -                ExtPriceGranularity.of(2, null)));
    -    }
    -
         @Test
         public void createFromExtPriceGranularityShouldThrowIllegalArgumentsExceptionIfRangesListIsEmpty() {
             assertThatIllegalArgumentException().isThrownBy(() -> PriceGranularity.createFromExtPriceGranularity(
    diff --git a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java
    index 4146f18b7f5..c32451f9756 100644
    --- a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java
    +++ b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java
    @@ -5,10 +5,12 @@
     import com.iab.openrtb.request.Geo;
     import com.iab.openrtb.request.Imp;
     import com.iab.openrtb.request.Regs;
    +import com.iab.openrtb.request.Site;
     import com.iab.openrtb.request.User;
     import io.vertx.core.Future;
     import io.vertx.core.MultiMap;
     import io.vertx.core.http.HttpServerRequest;
    +import io.vertx.core.net.impl.SocketAddressImpl;
     import org.junit.Before;
     import org.junit.Rule;
     import org.junit.Test;
    @@ -19,7 +21,7 @@
     import org.prebid.server.assertion.FutureAssertion;
     import org.prebid.server.auction.model.AuctionContext;
     import org.prebid.server.auction.model.BidderPrivacyResult;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    +import org.prebid.server.auction.model.IpAddress;
     import org.prebid.server.bidder.BidderCatalog;
     import org.prebid.server.exception.InvalidRequestException;
     import org.prebid.server.execution.Timeout;
    @@ -27,10 +29,12 @@
     import org.prebid.server.geolocation.model.GeoInfo;
     import org.prebid.server.metric.MetricName;
     import org.prebid.server.metric.Metrics;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
     import org.prebid.server.privacy.PrivacyExtractor;
     import org.prebid.server.privacy.ccpa.Ccpa;
     import org.prebid.server.privacy.gdpr.TcfDefinerService;
     import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction;
    +import org.prebid.server.privacy.gdpr.model.RequestLogInfo;
     import org.prebid.server.privacy.gdpr.model.TCStringEmpty;
     import org.prebid.server.privacy.gdpr.model.TcfContext;
     import org.prebid.server.privacy.gdpr.model.TcfResponse;
    @@ -40,17 +44,19 @@
     import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
     import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEid;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid;
     import org.prebid.server.proto.request.CookieSyncRequest;
    -import org.prebid.server.proto.request.PreBidRequest;
     import org.prebid.server.proto.response.BidderInfo;
     import org.prebid.server.settings.model.Account;
    +import org.prebid.server.settings.model.AccountCcpaConfig;
    +import org.prebid.server.settings.model.AccountPrivacyConfig;
    +import org.prebid.server.settings.model.EnabledForRequestType;
     
     import java.time.Clock;
     import java.time.Instant;
     import java.time.ZoneId;
    +import java.util.ArrayList;
     import java.util.HashMap;
     import java.util.HashSet;
     import java.util.List;
    @@ -64,9 +70,11 @@
     import static java.util.Collections.singleton;
     import static java.util.Collections.singletonList;
     import static java.util.Collections.singletonMap;
    +import static java.util.function.UnaryOperator.identity;
     import static org.apache.commons.lang3.StringUtils.EMPTY;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyBoolean;
     import static org.mockito.ArgumentMatchers.anySet;
     import static org.mockito.ArgumentMatchers.anyString;
     import static org.mockito.ArgumentMatchers.eq;
    @@ -92,6 +100,8 @@ public class PrivacyEnforcementServiceTest extends VertxTest {
         @Mock
         private TcfDefinerService tcfDefinerService;
         @Mock
    +    private ImplicitParametersExtractor implicitParametersExtractor;
    +    @Mock
         private IpAddressHelper ipAddressHelper;
         @Mock
         private Metrics metrics;
    @@ -117,28 +127,43 @@ public void setUp() {
             privacyExtractor = new PrivacyExtractor();
     
             privacyEnforcementService = new PrivacyEnforcementService(
    -                bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, false);
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, false, false);
         }
     
         @Test
    -    public void contextFromBidRequestShouldReturnCoppaContext() {
    +    public void contextFromBidRequestShouldReturnTcfContextForCoppa() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
                     .regs(Regs.of(1, null))
                     .build();
     
    +        final TcfContext tcfContext = TcfContext.builder()
    +                .gdpr("1")
    +                .consentString("consent")
    +                .consent(TCStringEmpty.create())
    +                .build();
    +        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any(), any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(tcfContext));
    +
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .bidRequest(bidRequest)
    +                .account(Account.empty("account"))
    +                .prebidErrors(new ArrayList<>())
    +                .build();
    +
             // when
    -        final Future privacyContext = privacyEnforcementService.contextFromBidRequest(
    -                bidRequest, Account.empty("account"), null, null);
    +        final Future privacyContext = privacyEnforcementService.contextFromBidRequest(auctionContext);
     
             // then
             FutureAssertion.assertThat(privacyContext).succeededWith(
    -                PrivacyContext.of(Privacy.of(EMPTY, EMPTY, Ccpa.EMPTY, 1), TcfContext.empty()));
    +                PrivacyContext.of(Privacy.of(EMPTY, EMPTY, Ccpa.EMPTY, 1), tcfContext));
         }
     
         @Test
         public void contextFromBidRequestShouldReturnTcfContext() {
             // given
    +        final String referer = "Referer";
             final BidRequest bidRequest = BidRequest.builder()
                     .regs(Regs.of(null, ExtRegs.of(1, "1YYY")))
                     .user(User.builder()
    @@ -146,6 +171,7 @@ public void contextFromBidRequestShouldReturnTcfContext() {
                                     .consent("consent")
                                     .build())
                             .build())
    +                .site(Site.builder().ref(referer).build())
                     .build();
     
             final TcfContext tcfContext = TcfContext.builder()
    @@ -153,19 +179,30 @@ public void contextFromBidRequestShouldReturnTcfContext() {
                     .consentString("consent")
                     .consent(TCStringEmpty.create())
                     .build();
    -        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any(), any(), any()))
    +        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any(), any(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(tcfContext));
     
    +        final String accountId = "account";
    +        final MetricName requestType = MetricName.openrtb2web;
    +
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .bidRequest(bidRequest)
    +                .account(Account.empty(accountId))
    +                .requestTypeMetric(requestType)
    +                .prebidErrors(new ArrayList<>())
    +                .build();
    +
             // when
    -        final Future privacyContext = privacyEnforcementService.contextFromBidRequest(
    -                bidRequest, Account.empty("account"), MetricName.openrtb2web, null);
    +        final Future privacyContext = privacyEnforcementService.contextFromBidRequest(auctionContext);
     
             // then
             final Privacy privacy = Privacy.of("1", "consent", Ccpa.of("1YYY"), 0);
             FutureAssertion.assertThat(privacyContext).succeededWith(PrivacyContext.of(privacy, tcfContext));
     
    +        final RequestLogInfo expectedRequestLogInfo = RequestLogInfo.of(requestType, referer, accountId);
             verify(tcfDefinerService).resolveTcfContext(
    -                eq(privacy), isNull(), isNull(), isNull(), same(MetricName.openrtb2web), isNull());
    +                eq(privacy), isNull(), isNull(), isNull(), same(requestType),
    +                eq(expectedRequestLogInfo), isNull(), any());
         }
     
         @Test
    @@ -194,88 +231,123 @@ public void contextFromBidRequestShouldReturnTcfContextWithIpMasked() {
                     .inEea(false)
                     .geoInfo(GeoInfo.builder().vendor("v").country("ua").build())
                     .build();
    -        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any(), any(), any()))
    +        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any(), any(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(tcfContext));
     
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .bidRequest(bidRequest)
    +                .account(Account.empty("account"))
    +                .requestTypeMetric(MetricName.openrtb2web)
    +                .prebidErrors(new ArrayList<>())
    +                .build();
    +
             // when
    -        final Future privacyContext = privacyEnforcementService.contextFromBidRequest(
    -                bidRequest, Account.empty("account"), MetricName.openrtb2web, null);
    +        final Future privacyContext = privacyEnforcementService.contextFromBidRequest(auctionContext);
     
             // then
             final Privacy privacy = Privacy.of("1", "consent", Ccpa.of("1YYY"), 0);
             FutureAssertion.assertThat(privacyContext).succeededWith(PrivacyContext.of(privacy, tcfContext, "ip-masked"));
     
             verify(tcfDefinerService).resolveTcfContext(
    -                eq(privacy), isNull(), eq("ip-masked"), isNull(), same(MetricName.openrtb2web), isNull());
    +                eq(privacy), isNull(), eq("ip-masked"), isNull(), same(MetricName.openrtb2web), any(), isNull(), any());
         }
     
         @Test
    -    public void contextFromLegacyRequestShouldReturnContext() {
    +    public void contextFromBidRequestShouldCallResolveTcfContextWithIpv6AnonymizedWhenIpNotPresentAndLmtIsOne() {
             // given
    -        final PreBidRequestContext preBidRequestContext = PreBidRequestContext.builder()
    -                .preBidRequest(PreBidRequest.builder()
    -                        .regs(Regs.of(null, ExtRegs.of(1, "1YYY")))
    -                        .user(User.builder()
    -                                .ext(ExtUser.builder()
    -                                        .consent("consent")
    -                                        .build())
    -                                .build())
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder()
    +                        .lmt(1)
    +                        .ipv6("ipv6")
                             .build())
    -                .ip("ip")
                     .build();
    -
    -        final TcfContext tcfContext = TcfContext.builder()
    -                .gdpr("1")
    -                .consentString("consent")
    -                .consent(TCStringEmpty.create())
    +        given(ipAddressHelper.anonymizeIpv6(any())).willReturn("ip-masked");
    +        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any(), any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(TcfContext.builder().build()));
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .bidRequest(bidRequest)
    +                .account(Account.empty("account"))
    +                .prebidErrors(new ArrayList<>())
                     .build();
    -        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any()))
    -                .willReturn(Future.succeededFuture(tcfContext));
     
             // when
    -        final Future privacyContext = privacyEnforcementService.contextFromLegacyRequest(
    -                preBidRequestContext, Account.empty("account"));
    +        privacyEnforcementService.contextFromBidRequest(auctionContext);
     
             // then
    -        final Privacy privacy = Privacy.of("1", "consent", Ccpa.of("1YYY"), 0);
    -        FutureAssertion.assertThat(privacyContext).succeededWith(PrivacyContext.of(privacy, tcfContext));
    +        verify(tcfDefinerService).resolveTcfContext(any(), any(), eq("ip-masked"), any(), any(), any(), any(), any());
    +    }
     
    -        verify(tcfDefinerService).resolveTcfContext(eq(privacy), eq("ip"), isNull(), isNull());
    +    @Test
    +    public void contextFromBidRequestShouldCallResolveTcfContextWithIpv6WhenIpv4NotPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder()
    +                        .ipv6("ipv6")
    +                        .build())
    +                .build();
    +        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any(), any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(TcfContext.builder().build()));
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .bidRequest(bidRequest)
    +                .account(Account.empty("account"))
    +                .prebidErrors(new ArrayList<>())
    +                .build();
    +
    +        // when
    +        privacyEnforcementService.contextFromBidRequest(auctionContext);
    +
    +        // then
    +        verify(tcfDefinerService).resolveTcfContext(any(), any(), eq("ipv6"), any(), any(), any(), any(), any());
         }
     
         @Test
         public void contextFromSetuidRequestShouldReturnContext() {
             // given
    -        final HttpServerRequest request = mock(HttpServerRequest.class);
    -        given(request.getParam("gdpr")).willReturn("1");
    -        given(request.getParam("gdpr_consent")).willReturn("consent");
    -        given(request.headers()).willReturn(MultiMap.caseInsensitiveMultiMap().add("X-Forwarded-For", "ip"));
    +        final HttpServerRequest httpRequest = mock(HttpServerRequest.class);
    +        given(httpRequest.getParam("gdpr")).willReturn("1");
    +        given(httpRequest.getParam("gdpr_consent")).willReturn("consent");
    +        final MultiMap headers = MultiMap.caseInsensitiveMultiMap();
    +        given(httpRequest.headers()).willReturn(headers);
    +        given(httpRequest.remoteAddress()).willReturn(new SocketAddressImpl(1234, "host"));
    +
    +        given(implicitParametersExtractor.ipFrom(eq(headers), eq("host"))).willReturn(singletonList("ip"));
    +        given(implicitParametersExtractor
    +                .ipFrom(any(CaseInsensitiveMultiMap.class), any())).willReturn(singletonList("ip"));
    +        given(ipAddressHelper.toIpAddress(anyString())).willReturn(IpAddress.of("ip", IpAddress.IP.v4));
     
             final TcfContext tcfContext = TcfContext.builder()
                     .gdpr("1")
                     .consentString("consent")
                     .consent(TCStringEmpty.create())
                     .build();
    -        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any()))
    +        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(tcfContext));
     
    +        final String accountId = "account";
    +
             // when
             final Future privacyContext = privacyEnforcementService.contextFromSetuidRequest(
    -                request, Account.empty("account"), null);
    +                httpRequest, Account.empty(accountId), null);
     
             // then
             final Privacy privacy = Privacy.of("1", "consent", Ccpa.EMPTY, 0);
             FutureAssertion.assertThat(privacyContext).succeededWith(PrivacyContext.of(privacy, tcfContext));
     
    -        verify(tcfDefinerService).resolveTcfContext(eq(privacy), eq("ip"), isNull(), isNull());
    +        final RequestLogInfo expectedRequestLogInfo = RequestLogInfo.of(MetricName.setuid, null, accountId);
    +        verify(tcfDefinerService).resolveTcfContext(
    +                eq(privacy), eq("ip"), isNull(), eq(MetricName.setuid), eq(expectedRequestLogInfo), isNull());
         }
     
         @Test
         public void contextFromCookieSyncRequestShouldReturnContext() {
             // given
    -        final HttpServerRequest httpServerRequest = mock(HttpServerRequest.class);
    -        given(httpServerRequest.headers())
    -                .willReturn(MultiMap.caseInsensitiveMultiMap().add("X-Forwarded-For", "ip"));
    +        final HttpServerRequest httpRequest = mock(HttpServerRequest.class);
    +        final MultiMap headers = MultiMap.caseInsensitiveMultiMap();
    +        given(httpRequest.headers()).willReturn(headers);
    +        given(httpRequest.remoteAddress()).willReturn(new SocketAddressImpl(1234, "host"));
    +
    +        given(implicitParametersExtractor.ipFrom(eq(headers), eq("host"))).willReturn(singletonList("ip"));
    +        given(ipAddressHelper.toIpAddress(anyString())).willReturn(IpAddress.of("ip", IpAddress.IP.v4));
     
             final CookieSyncRequest cookieSyncRequest = CookieSyncRequest.builder()
                     .gdpr(1)
    @@ -288,29 +360,33 @@ public void contextFromCookieSyncRequestShouldReturnContext() {
                     .consentString("consent")
                     .consent(TCStringEmpty.create())
                     .build();
    -        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any()))
    +        given(tcfDefinerService.resolveTcfContext(any(), any(), any(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(tcfContext));
     
    +        final String accountId = "account";
    +
             // when
             final Future privacyContext = privacyEnforcementService.contextFromCookieSyncRequest(
    -                cookieSyncRequest, httpServerRequest, Account.empty("account"), null);
    +                cookieSyncRequest, httpRequest, Account.empty(accountId), null);
     
             // then
             final Privacy privacy = Privacy.of("1", "consent", Ccpa.of("1YYY"), 0);
             FutureAssertion.assertThat(privacyContext).succeededWith(PrivacyContext.of(privacy, tcfContext));
     
    -        verify(tcfDefinerService).resolveTcfContext(eq(privacy), eq("ip"), isNull(), isNull());
    +        final RequestLogInfo expectedRequestLogInfo = RequestLogInfo.of(MetricName.cookiesync, null, accountId);
    +        verify(tcfDefinerService).resolveTcfContext(
    +                eq(privacy), eq("ip"), isNull(), eq(MetricName.cookiesync), eq(expectedRequestLogInfo), isNull());
         }
     
         @Test
    -    public void shouldMaskForCoppaWhenDeviceLmtIsOneAndRegsCoppaIsOneAndDoesNotCallTcfServices() {
    +    public void shouldMaskForCoppaWhenDeviceLmtIsEnforceAndOneAndRegsCoppaIsOneAndDoesNotCallTcfServices() {
             // given
             final User user = notMaskedUser(notMaskedExtUser());
             final Device device = givenNotMaskedDevice(deviceBuilder -> deviceBuilder.lmt(1));
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -338,7 +414,8 @@ public void shouldMaskForCoppaWhenDeviceLmtIsOneAndRegsCoppaIsOneAndDoesNotCallT
         public void shouldMaskForCcpaWhenUsPolicyIsValidAndCoppaIsZero() {
             // given
             privacyEnforcementService = new PrivacyEnforcementService(
    -                bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true);
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, true, false);
     
             given(tcfDefinerService.resultForBidderNames(anySet(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(TcfResponse.of(true, emptyMap(), null)));
    @@ -350,7 +427,7 @@ public void shouldMaskForCcpaWhenUsPolicyIsValidAndCoppaIsZero() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -373,6 +450,158 @@ public void shouldMaskForCcpaWhenUsPolicyIsValidAndCoppaIsZero() {
             assertThat(result).isEqualTo(singletonList(expected));
         }
     
    +    @Test
    +    public void shouldMaskForCcpaWhenAccountHasCppaConfigEnabledForRequestType() {
    +        // given
    +        privacyEnforcementService = new PrivacyEnforcementService(
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, false, true);
    +
    +        given(tcfDefinerService.resultForBidderNames(anySet(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(TcfResponse.of(true, emptyMap(), null)));
    +
    +        given(bidderCatalog.bidderInfoByName(BIDDER_NAME)).willReturn(givenBidderInfo(1, true));
    +
    +        final User user = notMaskedUser(notMaskedExtUser());
    +        final Device device = notMaskedDevice();
    +        final Map bidderToUser = singletonMap(BIDDER_NAME, user);
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    +                        singletonMap(BIDDER_NAME, 1)),
    +                bidRequestBuilder -> bidRequestBuilder
    +                        .user(user)
    +                        .device(device));
    +
    +        final PrivacyContext privacyContext = givenPrivacyContext("1", Ccpa.of("1YYY"), 0);
    +
    +        final AuctionContext context = AuctionContext.builder()
    +                .account(Account.builder()
    +                        .privacy(AccountPrivacyConfig.of(
    +                                null,
    +                                null,
    +                                AccountCcpaConfig.builder()
    +                                        .enabledForRequestType(EnabledForRequestType.of(false, false, true, false))
    +                                        .build()))
    +                        .build())
    +                .requestTypeMetric(MetricName.openrtb2app)
    +                .bidRequest(bidRequest)
    +                .timeout(timeout)
    +                .privacyContext(privacyContext)
    +                .build();
    +
    +        // when
    +        final List result = privacyEnforcementService
    +                .mask(context, bidderToUser, singletonList(BIDDER_NAME), aliases)
    +                .result();
    +
    +        // then
    +        final BidderPrivacyResult expected = BidderPrivacyResult.builder()
    +                .requestBidder(BIDDER_NAME)
    +                .user(userTcfMasked(extUserIdsMasked()))
    +                .device(deviceTcfMasked())
    +                .build();
    +        assertThat(result).isEqualTo(singletonList(expected));
    +    }
    +
    +    @Test
    +    public void shouldMaskForCcpaWhenAccountHasCppaEnforcedTrue() {
    +        // given
    +        privacyEnforcementService = new PrivacyEnforcementService(
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, false, true);
    +
    +        given(tcfDefinerService.resultForBidderNames(anySet(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(TcfResponse.of(true, emptyMap(), null)));
    +
    +        given(bidderCatalog.bidderInfoByName(BIDDER_NAME)).willReturn(givenBidderInfo(1, true));
    +
    +        final User user = notMaskedUser(notMaskedExtUser());
    +        final Device device = notMaskedDevice();
    +        final Map bidderToUser = singletonMap(BIDDER_NAME, user);
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    +                        singletonMap(BIDDER_NAME, 1)),
    +                bidRequestBuilder -> bidRequestBuilder
    +                        .user(user)
    +                        .device(device));
    +
    +        final PrivacyContext privacyContext = givenPrivacyContext("1", Ccpa.of("1YYY"), 0);
    +
    +        final AuctionContext context = AuctionContext.builder()
    +                .account(Account.builder()
    +                        .privacy(AccountPrivacyConfig.of(true, null, null))
    +                        .build())
    +                .requestTypeMetric(MetricName.openrtb2app)
    +                .bidRequest(bidRequest)
    +                .timeout(timeout)
    +                .privacyContext(privacyContext)
    +                .build();
    +
    +        // when
    +        final List result = privacyEnforcementService
    +                .mask(context, bidderToUser, singletonList(BIDDER_NAME), aliases)
    +                .result();
    +
    +        // then
    +        final BidderPrivacyResult expected = BidderPrivacyResult.builder()
    +                .requestBidder(BIDDER_NAME)
    +                .user(userTcfMasked(extUserIdsMasked()))
    +                .device(deviceTcfMasked())
    +                .build();
    +        assertThat(result).isEqualTo(singletonList(expected));
    +    }
    +
    +    @Test
    +    public void shouldMaskForCcpaWhenAccountHasCcpaConfigEnabled() {
    +        // given
    +        privacyEnforcementService = new PrivacyEnforcementService(
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, false, true);
    +
    +        given(tcfDefinerService.resultForBidderNames(anySet(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(TcfResponse.of(true, emptyMap(), null)));
    +
    +        given(bidderCatalog.bidderInfoByName(BIDDER_NAME)).willReturn(givenBidderInfo(1, true));
    +
    +        final User user = notMaskedUser(notMaskedExtUser());
    +        final Device device = notMaskedDevice();
    +        final Map bidderToUser = singletonMap(BIDDER_NAME, user);
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    +                        singletonMap(BIDDER_NAME, 1)),
    +                bidRequestBuilder -> bidRequestBuilder
    +                        .user(user)
    +                        .device(device));
    +
    +        final PrivacyContext privacyContext = givenPrivacyContext("1", Ccpa.of("1YYY"), 0);
    +
    +        final AuctionContext context = AuctionContext.builder()
    +                .account(Account.builder()
    +                        .privacy(AccountPrivacyConfig.of(
    +                                null,
    +                                null,
    +                                AccountCcpaConfig.builder().enabled(true).build()))
    +                        .build())
    +                .requestTypeMetric(null)
    +                .bidRequest(bidRequest)
    +                .timeout(timeout)
    +                .privacyContext(privacyContext)
    +                .build();
    +
    +        // when
    +        final List result = privacyEnforcementService
    +                .mask(context, bidderToUser, singletonList(BIDDER_NAME), aliases)
    +                .result();
    +
    +        // then
    +        final BidderPrivacyResult expected = BidderPrivacyResult.builder()
    +                .requestBidder(BIDDER_NAME)
    +                .user(userTcfMasked(extUserIdsMasked()))
    +                .device(deviceTcfMasked())
    +                .build();
    +        assertThat(result).isEqualTo(singletonList(expected));
    +    }
    +
         @Test
         public void shouldTolerateEmptyBidderToBidderPrivacyResultList() {
             // given
    @@ -446,7 +675,7 @@ public void shouldNotMaskWhenDeviceLmtIsZeroAndCoppaIsZeroAndGdprIsZeroAndTcfDef
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -472,8 +701,12 @@ public void shouldNotMaskWhenDeviceLmtIsZeroAndCoppaIsZeroAndGdprIsZeroAndTcfDef
         }
     
         @Test
    -    public void shouldMaskForTcfWhenTcfServiceAllowAllAndDeviceLmtIsOne() {
    +    public void shouldMaskForTcfWhenTcfServiceAllowAllAndDeviceLmtIsOneAndLmtIsEnforced() {
             // given
    +        privacyEnforcementService = new PrivacyEnforcementService(
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, false, true);
    +
             given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(
                             TcfResponse.of(true, singletonMap(BIDDER_NAME, PrivacyEnforcementAction.allowAll()), null)));
    @@ -483,7 +716,7 @@ public void shouldMaskForTcfWhenTcfServiceAllowAllAndDeviceLmtIsOne() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -508,6 +741,46 @@ public void shouldMaskForTcfWhenTcfServiceAllowAllAndDeviceLmtIsOne() {
             verify(tcfDefinerService).resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), any(), any());
         }
     
    +    @Test
    +    public void shouldNotMaskForTcfWhenTcfServiceAllowAllAndDeviceLmtIsOneAndLmtIsNotEnforced() {
    +        // given
    +        given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(
    +                        TcfResponse.of(true, singletonMap(BIDDER_NAME, PrivacyEnforcementAction.allowAll()), null)));
    +
    +        final User user = notMaskedUser();
    +        final Device device = givenNotMaskedDevice(deviceBuilder -> deviceBuilder.lmt(1));
    +        final Regs regs = Regs.of(0, null);
    +        final Map bidderToUser = singletonMap(BIDDER_NAME, user);
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    +                        singletonMap(BIDDER_NAME, 1)),
    +                bidRequestBuilder -> bidRequestBuilder
    +                        .user(user)
    +                        .device(device)
    +                        .regs(regs));
    +
    +        final PrivacyContext privacyContext = givenPrivacyContext("0", Ccpa.EMPTY, 0);
    +
    +        final AuctionContext context = auctionContext(bidRequest, privacyContext);
    +
    +        // when
    +        final List result = privacyEnforcementService
    +                .mask(context, bidderToUser, singletonList(BIDDER_NAME), BidderAliases.of(null, null, bidderCatalog))
    +                .result();
    +
    +        // then
    +        final BidderPrivacyResult expectedBidderPrivacy = BidderPrivacyResult.builder()
    +                .user(user)
    +                .device(device)
    +                .requestBidder(BIDDER_NAME)
    +                .build();
    +        assertThat(result).containsOnly(expectedBidderPrivacy);
    +
    +        verify(tcfDefinerService)
    +                .resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), any(), any());
    +    }
    +
         @Test
         public void shouldMaskForTcfWhenTcfDefinerServiceRestrictDeviceAndUser() {
             // given
    @@ -516,7 +789,7 @@ public void shouldMaskForTcfWhenTcfDefinerServiceRestrictDeviceAndUser() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -556,7 +829,7 @@ public void shouldMaskUserIdsWhenTcfDefinerServiceRestrictUserIds() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -582,6 +855,9 @@ public void shouldMaskUserIdsWhenTcfDefinerServiceRestrictUserIds() {
             assertThat(result).containsOnly(expectedBidderPrivacy);
     
             verify(tcfDefinerService).resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), any(), any());
    +
    +        verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), eq(true), anyBoolean(), anyBoolean(),
    +                anyBoolean());
         }
     
         @Test
    @@ -596,7 +872,6 @@ public void shouldMaskUserIdsWhenTcfDefinerServiceRestrictUserIdsAndReturnNullWh
     
             final ExtUser extUser = ExtUser.builder()
                     .eids(singletonList(ExtUserEid.of("Test", "id", emptyList(), null)))
    -                .digitrust(ExtUserDigiTrust.of("idDigit", 12, 23))
                     .build();
             final User user = User.builder()
                     .buyeruid(BUYER_UID)
    @@ -606,7 +881,7 @@ public void shouldMaskUserIdsWhenTcfDefinerServiceRestrictUserIdsAndReturnNullWh
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user));
     
    @@ -643,7 +918,7 @@ public void shouldMaskGeoWhenTcfDefinerServiceRestrictGeo() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -666,6 +941,9 @@ public void shouldMaskGeoWhenTcfDefinerServiceRestrictGeo() {
             assertThat(result).containsOnly(expectedBidderPrivacy);
     
             verify(tcfDefinerService).resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), any(), any());
    +
    +        verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), anyBoolean(), eq(true), anyBoolean(),
    +                anyBoolean());
         }
     
         @Test
    @@ -683,7 +961,7 @@ public void shouldMaskDeviceIpWhenTcfDefinerServiceRestrictDeviceIp() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -725,7 +1003,7 @@ public void shouldMaskDeviceInfoWhenTcfDefinerServiceRestrictDeviceInfo() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -755,6 +1033,118 @@ public void shouldMaskDeviceInfoWhenTcfDefinerServiceRestrictDeviceInfo() {
             verify(tcfDefinerService).resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), any(), any());
         }
     
    +    @Test
    +    public void shouldSendAnalyticsBlockedMetricIfRestrictedByPrivacyEnforcement() {
    +        // given
    +        final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.allowAll();
    +        privacyEnforcementAction.setBlockAnalyticsReport(true);
    +
    +        given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(
    +                        TcfResponse.of(true, singletonMap(BIDDER_NAME, privacyEnforcementAction), null)));
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap(BIDDER_NAME, 1)), identity());
    +        final PrivacyContext privacyContext = givenPrivacyContext("0", Ccpa.EMPTY, 0);
    +        final AuctionContext context = auctionContext(bidRequest, privacyContext);
    +
    +        // when
    +        privacyEnforcementService.mask(context, emptyMap(), singletonList(BIDDER_NAME), aliases);
    +
    +        // then
    +        verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), anyBoolean(), anyBoolean(), eq(true),
    +                anyBoolean());
    +    }
    +
    +    @Test
    +    public void shouldNotSendRelatedMetricsIfBlockBidderRequestEnforcementIsPresent() {
    +        // given
    +        final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.allowAll();
    +        privacyEnforcementAction.setBlockBidderRequest(true); // has highest priority
    +        privacyEnforcementAction.setRemoveUserIds(true);
    +        privacyEnforcementAction.setMaskGeo(true);
    +        privacyEnforcementAction.setBlockAnalyticsReport(true);
    +
    +        given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(
    +                        TcfResponse.of(true, singletonMap(BIDDER_NAME, privacyEnforcementAction), null)));
    +
    +        final ExtUser extUser = notMaskedExtUser();
    +        final User user = notMaskedUser(extUser);
    +        final Map bidderToUser = singletonMap(BIDDER_NAME, user);
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    +                        singletonMap(BIDDER_NAME, 1)),
    +                bidRequestBuilder -> bidRequestBuilder
    +                        .user(user)
    +                        .device(notMaskedDevice()));
    +        final PrivacyContext privacyContext = givenPrivacyContext("0", Ccpa.EMPTY, 0);
    +
    +        final AuctionContext context = auctionContext(bidRequest, privacyContext);
    +
    +        // when
    +        privacyEnforcementService.mask(context, bidderToUser, singletonList(BIDDER_NAME), aliases);
    +
    +        // then
    +        verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), eq(false), eq(false), eq(false),
    +                eq(true));
    +    }
    +
    +    @Test
    +    public void shouldNotSendUserIdRemovedMetricIfNoPrivateUserInformation() {
    +        // given
    +        final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.allowAll();
    +        privacyEnforcementAction.setRemoveUserIds(true);
    +
    +        given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(
    +                        TcfResponse.of(true, singletonMap(BIDDER_NAME, privacyEnforcementAction), null)));
    +
    +        final ExtUser extUser = ExtUser.builder().consent("consent").build();
    +        final User user = User.builder().gender("gender").ext(extUser).build();
    +        final Map bidderToUser = singletonMap(BIDDER_NAME, user);
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    +                        singletonMap(BIDDER_NAME, 1)),
    +                bidRequestBuilder -> bidRequestBuilder
    +                        .user(user));
    +        final PrivacyContext privacyContext = givenPrivacyContext("0", Ccpa.EMPTY, 0);
    +
    +        final AuctionContext context = auctionContext(bidRequest, privacyContext);
    +
    +        // when
    +        privacyEnforcementService.mask(context, bidderToUser, singletonList(BIDDER_NAME), aliases);
    +
    +        // then
    +        verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), eq(false), anyBoolean(), anyBoolean(),
    +                anyBoolean());
    +    }
    +
    +    @Test
    +    public void shouldNotSendGeoMaskedMetricIfNoPrivateGeoInformation() {
    +        // given
    +        final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.allowAll();
    +        privacyEnforcementAction.setMaskGeo(true);
    +
    +        given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(
    +                        TcfResponse.of(true, singletonMap(BIDDER_NAME, privacyEnforcementAction), null)));
    +
    +        final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    +                        singletonMap(BIDDER_NAME, 1)),
    +                bidRequestBuilder -> bidRequestBuilder
    +                        .device(Device.builder().model("blackberry").build()));
    +        final PrivacyContext privacyContext = givenPrivacyContext("0", Ccpa.EMPTY, 0);
    +
    +        final AuctionContext context = auctionContext(bidRequest, privacyContext);
    +
    +        // when
    +        privacyEnforcementService.mask(context, emptyMap(), singletonList(BIDDER_NAME), aliases);
    +
    +        // then
    +        verify(metrics).updateAuctionTcfMetrics(eq(BIDDER_NAME), any(), eq(false), anyBoolean(), anyBoolean(),
    +                anyBoolean());
    +    }
    +
         @Test
         public void shouldRerunEmptyResultWhenTcfDefinerServiceRestrictRequest() {
             // given
    @@ -767,7 +1157,7 @@ public void shouldRerunEmptyResultWhenTcfDefinerServiceRestrictRequest() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -782,7 +1172,6 @@ public void shouldRerunEmptyResultWhenTcfDefinerServiceRestrictRequest() {
                     .result();
     
             // then
    -
             final BidderPrivacyResult expectedBidderPrivacy = BidderPrivacyResult.builder()
                     .requestBidder(BIDDER_NAME)
                     .blockedRequestByTcf(true)
    @@ -880,7 +1269,6 @@ public void shouldNotReturnUserIfMaskingAppliedAndUserBecameEmptyObject() {
             // given
             final ExtUser extUser = ExtUser.builder()
                     .eids(singletonList(ExtUserEid.of("Test", "id", emptyList(), null)))
    -                .digitrust(ExtUserDigiTrust.of("idDigit", 12, 23))
                     .build();
             final User user = User.builder()
                     .buyeruid("buyeruid")
    @@ -889,7 +1277,7 @@ public void shouldNotReturnUserIfMaskingAppliedAndUserBecameEmptyObject() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user));
     
    @@ -921,7 +1309,7 @@ public void shouldReturnFailedFutureWhenTcfServiceIsReturnFailedFuture() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -947,7 +1335,8 @@ public void shouldReturnFailedFutureWhenTcfServiceIsReturnFailedFuture() {
         public void shouldMaskForCcpaAndTcfWhenUsPolicyIsValidAndGdprIsEnforcedAndCOPPAIsZero() {
             // given
             privacyEnforcementService = new PrivacyEnforcementService(
    -                bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true);
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, true, false);
     
             final String bidder1Name = "bidder1Name";
             final String bidder2Name = "bidder2Name";
    @@ -972,7 +1361,7 @@ public void shouldMaskForCcpaAndTcfWhenUsPolicyIsValidAndGdprIsEnforcedAndCOPPAI
             bidderToUser.put(bidder3Name, notMaskedUser());
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device)
    @@ -986,10 +1375,10 @@ public void shouldMaskForCcpaAndTcfWhenUsPolicyIsValidAndGdprIsEnforcedAndCOPPAI
     
             // when
             final List result = privacyEnforcementService.mask(
    -                context,
    -                bidderToUser,
    -                asList(bidder1Name, bidder2Name, bidder3Name),
    -                BidderAliases.of(bidderCatalog))
    +                        context,
    +                        bidderToUser,
    +                        asList(bidder1Name, bidder2Name, bidder3Name),
    +                        BidderAliases.of(null, null, bidderCatalog))
                     .result();
     
             // then
    @@ -1017,7 +1406,8 @@ public void shouldMaskForCcpaAndTcfWhenUsPolicyIsValidAndGdprIsEnforcedAndCOPPAI
         public void shouldNotMaskForCcpaWhenCatchAllWildcardIsPresentInNosaleList() {
             // given
             privacyEnforcementService = new PrivacyEnforcementService(
    -                bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true);
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, true, false);
     
             given(tcfDefinerService.resultForBidderNames(anySet(), any(), any(), any()))
                     .willReturn(Future.succeededFuture(
    @@ -1029,7 +1419,7 @@ public void shouldNotMaskForCcpaWhenCatchAllWildcardIsPresentInNosaleList() {
             final Map bidderToUser = singletonMap(BIDDER_NAME, notMaskedUser());
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap(BIDDER_NAME, 1)),
    +                        singletonMap(BIDDER_NAME, 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device)
    @@ -1043,7 +1433,7 @@ public void shouldNotMaskForCcpaWhenCatchAllWildcardIsPresentInNosaleList() {
     
             // when
             final List result = privacyEnforcementService
    -                .mask(context, bidderToUser, singletonList(BIDDER_NAME), BidderAliases.of(bidderCatalog))
    +                .mask(context, bidderToUser, singletonList(BIDDER_NAME), BidderAliases.of(null, null, bidderCatalog))
                     .result();
     
             // then
    @@ -1071,10 +1461,13 @@ public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsFalseInConfigur
         public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsTrueInConfigurationAndFalseInAccount() {
             // given
             privacyEnforcementService = new PrivacyEnforcementService(
    -                bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true);
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, true, false);
     
             final Ccpa ccpa = Ccpa.of("1YYY");
    -        final Account account = Account.builder().enforceCcpa(false).build();
    +        final Account account = Account.builder()
    +                .privacy(AccountPrivacyConfig.of(false, null, null))
    +                .build();
     
             // when and then
             assertThat(privacyEnforcementService.isCcpaEnforced(ccpa, account)).isFalse();
    @@ -1084,7 +1477,8 @@ public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsTrueInConfigura
         public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsTrue() {
             // given
             privacyEnforcementService = new PrivacyEnforcementService(
    -                bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true);
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, true, false);
     
             final Ccpa ccpa = Ccpa.of("1YNY");
             final Account account = Account.builder().build();
    @@ -1093,11 +1487,31 @@ public void isCcpaEnforcedShouldReturnFalseWhenEnforcedPropertyIsTrue() {
             assertThat(privacyEnforcementService.isCcpaEnforced(ccpa, account)).isFalse();
         }
     
    +    @Test
    +    public void isCcpaEnforcedShouldReturnFalseWhenAccountCcpaConfigHasEnabledTrue() {
    +        // given
    +        privacyEnforcementService = new PrivacyEnforcementService(
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, false, true);
    +
    +        final Ccpa ccpa = Ccpa.of("1YYY");
    +        final Account account = Account.builder()
    +                .privacy(AccountPrivacyConfig.of(
    +                        null,
    +                        null,
    +                        AccountCcpaConfig.builder().enabled(true).build()))
    +                .build();
    +
    +        // when and then
    +        assertThat(privacyEnforcementService.isCcpaEnforced(ccpa, account)).isTrue();
    +    }
    +
         @Test
         public void isCcpaEnforcedShouldReturnTrueWhenEnforcedPropertyIsTrueAndCcpaReturnsTrue() {
             // given
             privacyEnforcementService = new PrivacyEnforcementService(
    -                bidderCatalog, privacyExtractor, tcfDefinerService, ipAddressHelper, metrics, true);
    +                bidderCatalog, privacyExtractor, tcfDefinerService, implicitParametersExtractor, ipAddressHelper,
    +                metrics, true, false);
     
             final Ccpa ccpa = Ccpa.of("1YYY");
             final Account account = Account.builder().build();
    @@ -1163,7 +1577,8 @@ public void shouldReturnCorrectMaskedForMultipleBidders() {
                     .user(notMaskedUser())
                     .device(notMaskedDevice())
                     .build();
    -        assertThat(result).hasSize(3).containsOnly(expectedBidder1Masked, expectedBidder2Masked, expectedBidder3Masked);
    +        assertThat(result).hasSize(3)
    +                .containsOnly(expectedBidder1Masked, expectedBidder2Masked, expectedBidder3Masked);
     
             final HashSet bidderNames = new HashSet<>(asList(bidder1Name, bidder2Name, bidder3Name));
             verify(tcfDefinerService).resultForBidderNames(eq(bidderNames), any(), any(), any());
    @@ -1177,7 +1592,7 @@ public void shouldIncrementCcpaAndAuctionTcfMetrics() {
             final Map bidderToUser = singletonMap("someAlias", user);
     
             final BidRequest bidRequest = givenBidRequest(givenSingleImp(
    -                singletonMap("someAlias", 1)),
    +                        singletonMap("someAlias", 1)),
                     bidRequestBuilder -> bidRequestBuilder
                             .user(user)
                             .device(device));
    @@ -1246,8 +1661,8 @@ private static User notMaskedUser(ExtUser extUser) {
     
         private static ExtUser notMaskedExtUser() {
             return ExtUser.builder()
    +                .digitrust(mapper.createObjectNode())
                     .eids(singletonList(ExtUserEid.of("Test", "id", emptyList(), null)))
    -                .digitrust(ExtUserDigiTrust.of("idDigit", 12, 23))
                     .prebid(ExtUserPrebid.of(singletonMap("key", "value")))
                     .build();
         }
    @@ -1319,7 +1734,7 @@ private static Device givenCoppaMaskedDevice(UnaryOperator
         }
     
         private static  List givenSingleImp(T ext) {
    -        return singletonList(givenImp(ext, UnaryOperator.identity()));
    +        return singletonList(givenImp(ext, identity()));
         }
     
         private static  Imp givenImp(T ext, UnaryOperator impBuilderCustomizer) {
    @@ -1342,7 +1757,15 @@ private static PrivacyEnforcementAction restrictDeviceAndUser() {
         }
     
         private static BidderInfo givenBidderInfo(int gdprVendorId, boolean enforceCcpa) {
    -        return new BidderInfo(true, null, null, null,
    -                new BidderInfo.GdprInfo(gdprVendorId, true), enforceCcpa, false);
    +        return BidderInfo.of(
    +                true,
    +                true,
    +                null,
    +                null,
    +                null,
    +                null,
    +                new BidderInfo.GdprInfo(gdprVendorId, true),
    +                enforceCcpa,
    +                false);
         }
     }
    diff --git a/src/test/java/org/prebid/server/auction/SchainResolverTest.java b/src/test/java/org/prebid/server/auction/SchainResolverTest.java
    new file mode 100644
    index 00000000000..15109706c6e
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/auction/SchainResolverTest.java
    @@ -0,0 +1,164 @@
    +package org.prebid.server.auction;
    +
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Source;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchainNode;
    +import org.prebid.server.proto.openrtb.ext.request.ExtSource;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static org.assertj.core.api.Assertions.assertThat;
    +
    +public class SchainResolverTest extends VertxTest {
    +
    +    private SchainResolver schainResolver;
    +
    +    @Before
    +    public void setUp() {
    +        schainResolver = SchainResolver.create(null, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void shouldResolveSchainsWhenCatchAllPresent() {
    +        // given
    +        final ExtRequestPrebidSchainSchainNode specificNodes = ExtRequestPrebidSchainSchainNode.of(
    +                "asi", "sid", 1, "rid", "name", "domain", null);
    +        final ExtRequestPrebidSchainSchain specificSchain = ExtRequestPrebidSchainSchain.of(
    +                "ver", 1, singletonList(specificNodes), null);
    +        final ExtRequestPrebidSchain schainForBidders = ExtRequestPrebidSchain.of(
    +                asList("bidder1", "bidder2"), specificSchain);
    +
    +        final ExtRequestPrebidSchainSchainNode generalNodes = ExtRequestPrebidSchainSchainNode.of(
    +                "t", null, 0, "a", null, "ads", null);
    +        final ExtRequestPrebidSchainSchain generalSchain = ExtRequestPrebidSchainSchain.of(
    +                "t", 123, singletonList(generalNodes), null);
    +        final ExtRequestPrebidSchain allSchain = ExtRequestPrebidSchain.of(singletonList("*"), generalSchain);
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .schains(asList(schainForBidders, allSchain))
    +                        .build()))
    +                .build();
    +
    +        // when and then
    +        assertThat(schainResolver.resolveForBidder("bidder1", bidRequest)).isSameAs(specificSchain);
    +        assertThat(schainResolver.resolveForBidder("bidder2", bidRequest)).isSameAs(specificSchain);
    +        assertThat(schainResolver.resolveForBidder("bidder3", bidRequest)).isSameAs(generalSchain);
    +    }
    +
    +    @Test
    +    public void shouldReturnNullWhenAbsentForBidderAndNoCatchAll() {
    +        // given
    +        final ExtRequestPrebidSchainSchainNode specificNodes = ExtRequestPrebidSchainSchainNode.of(
    +                "asi", "sid", 1, "rid", "name", "domain", null);
    +        final ExtRequestPrebidSchainSchain specificSchain = ExtRequestPrebidSchainSchain.of(
    +                "ver", 1, singletonList(specificNodes), null);
    +        final ExtRequestPrebidSchain schainForBidders = ExtRequestPrebidSchain.of(
    +                singletonList("bidder1"), specificSchain);
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .schains(singletonList(schainForBidders))
    +                        .build()))
    +                .build();
    +
    +        // when and then
    +        assertThat(schainResolver.resolveForBidder("bidder2", bidRequest)).isNull();
    +    }
    +
    +    @Test
    +    public void shouldIgnoreDuplicatedBidderSchains() {
    +        // given
    +        final ExtRequestPrebidSchain schain1 = ExtRequestPrebidSchain.of(
    +                singletonList("bidder"), ExtRequestPrebidSchainSchain.of("ver1", null, null, null));
    +        final ExtRequestPrebidSchain schain2 = ExtRequestPrebidSchain.of(
    +                singletonList("bidder"), ExtRequestPrebidSchainSchain.of("ver2", null, null, null));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .schains(asList(schain1, schain2))
    +                        .build()))
    +                .build();
    +
    +        // when and then
    +        assertThat(schainResolver.resolveForBidder("bidder", bidRequest)).isNull();
    +    }
    +
    +    @Test
    +    public void shouldInjectGlobalNodeIntoResolvedSchain() {
    +        // given
    +        schainResolver = SchainResolver.create(
    +                "{\"asi\": \"pbshostcompany.com\", \"sid\":\"00001\"}",
    +                jacksonMapper);
    +
    +        final ExtRequestPrebidSchainSchainNode node = ExtRequestPrebidSchainSchainNode.of(
    +                "asi", "sid", 1, "rid", "name", "domain", null);
    +        final ExtRequestPrebidSchainSchain schain = ExtRequestPrebidSchainSchain.of(
    +                "ver", 1, singletonList(node), null);
    +        final ExtRequestPrebidSchain schainEntry = ExtRequestPrebidSchain.of(
    +                singletonList("bidder"), schain);
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .schains(singletonList(schainEntry))
    +                        .build()))
    +                .build();
    +
    +        // when and then
    +        final ExtRequestPrebidSchainSchainNode globalNode = ExtRequestPrebidSchainSchainNode.of(
    +                "pbshostcompany.com", "00001", null, null, null, null, null);
    +        final ExtRequestPrebidSchainSchain expectedSchain = ExtRequestPrebidSchainSchain.of(
    +                "ver", 1, asList(node, globalNode), null);
    +        assertThat(schainResolver.resolveForBidder("bidder", bidRequest)).isEqualTo(expectedSchain);
    +    }
    +
    +    @Test
    +    public void shouldReturnSchainWithGlobalNodeOnly() {
    +        // given
    +        schainResolver = SchainResolver.create(
    +                "{\"asi\": \"pbshostcompany.com\", \"sid\":\"00001\"}",
    +                jacksonMapper);
    +
    +        final BidRequest bidRequest = BidRequest.builder().build();
    +
    +        // when and then
    +        final ExtRequestPrebidSchainSchainNode globalNode = ExtRequestPrebidSchainSchainNode.of(
    +                "pbshostcompany.com", "00001", null, null, null, null, null);
    +        final ExtRequestPrebidSchainSchain expectedSchain = ExtRequestPrebidSchainSchain.of(
    +                null, null, singletonList(globalNode), null);
    +        assertThat(schainResolver.resolveForBidder("bidder", bidRequest)).isEqualTo(expectedSchain);
    +    }
    +
    +    @Test
    +    public void shouldInjectGlobalNodeIntoExistingSchain() {
    +        // given
    +        schainResolver = SchainResolver.create(
    +                "{\"asi\": \"pbshostcompany.com\", \"sid\":\"00001\"}",
    +                jacksonMapper);
    +
    +        final ExtRequestPrebidSchainSchainNode node = ExtRequestPrebidSchainSchainNode.of(
    +                "asi", "sid", 1, "rid", "name", "domain", null);
    +        final ExtRequestPrebidSchainSchain schain = ExtRequestPrebidSchainSchain.of(
    +                "ver", 1, singletonList(node), null);
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .source(Source.builder()
    +                        .ext(ExtSource.of(schain))
    +                        .build())
    +                .build();
    +
    +        // when and then
    +        final ExtRequestPrebidSchainSchainNode globalNode = ExtRequestPrebidSchainSchainNode.of(
    +                "pbshostcompany.com", "00001", null, null, null, null, null);
    +        final ExtRequestPrebidSchainSchain expectedSchain = ExtRequestPrebidSchainSchain.of(
    +                "ver", 1, asList(node, globalNode), null);
    +        assertThat(schainResolver.resolveForBidder("bidder", bidRequest)).isEqualTo(expectedSchain);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java
    index f3b4dffcc11..31034b72e0b 100644
    --- a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java
    +++ b/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java
    @@ -1,6 +1,7 @@
     package org.prebid.server.auction;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.App;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.BidRequest.BidRequestBuilder;
    @@ -9,6 +10,8 @@
     import com.iab.openrtb.request.Imp.ImpBuilder;
     import com.iab.openrtb.request.Video;
     import io.vertx.core.Future;
    +import io.vertx.core.buffer.Buffer;
    +import io.vertx.core.file.FileSystem;
     import org.junit.Before;
     import org.junit.Rule;
     import org.junit.Test;
    @@ -20,6 +23,8 @@
     import org.prebid.server.exception.InvalidRequestException;
     import org.prebid.server.execution.Timeout;
     import org.prebid.server.execution.TimeoutFactory;
    +import org.prebid.server.identity.IdGenerator;
    +import org.prebid.server.json.JsonMerger;
     import org.prebid.server.metric.Metrics;
     import org.prebid.server.proto.openrtb.ext.request.ExtImp;
     import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
    @@ -50,6 +55,7 @@
     import static org.assertj.core.api.Assertions.entry;
     import static org.mockito.ArgumentMatchers.any;
     import static org.mockito.ArgumentMatchers.anySet;
    +import static org.mockito.ArgumentMatchers.anyString;
     import static org.mockito.ArgumentMatchers.eq;
     import static org.mockito.BDDMockito.given;
     import static org.mockito.Mockito.verify;
    @@ -62,22 +68,32 @@ public class StoredRequestProcessorTest extends VertxTest {
         @Rule
         public final MockitoRule mockitoRule = MockitoJUnit.rule();
     
    +    @Mock
    +    private FileSystem fileSystem;
         @Mock
         private ApplicationSettings applicationSettings;
         @Mock
    +    private IdGenerator idGenerator;
    +    @Mock
         private Metrics metrics;
     
         private StoredRequestProcessor storedRequestProcessor;
     
         @Before
         public void setUp() {
    +        given(idGenerator.generateId()).willReturn("generated-stored-id");
             final TimeoutFactory timeoutFactory = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault()));
    -        storedRequestProcessor = new StoredRequestProcessor(
    +        storedRequestProcessor = StoredRequestProcessor.create(
                     DEFAULT_TIMEOUT,
    +                null,
    +                false,
    +                fileSystem,
                     applicationSettings,
    +                idGenerator,
                     metrics,
                     timeoutFactory,
    -                jacksonMapper);
    +                jacksonMapper,
    +                new JsonMerger(jacksonMapper));
         }
     
         @Test
    @@ -98,13 +114,13 @@ public void shouldReturnMergedBidRequestAndImps() throws IOException {
             final String storedRequestBidRequestJson = mapper.writeValueAsString(BidRequest.builder().id("test-request-id")
                     .tmax(1000L).imp(singletonList(Imp.builder().build())).build());
     
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(
                             StoredDataResult.of(singletonMap("bidRequest", storedRequestBidRequestJson),
                                     singletonMap("imp", storedRequestImpJson), emptyList())));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.succeeded()).isTrue();
    @@ -140,13 +156,13 @@ public void shouldReturnMergedBidRequest() throws IOException {
                     .imp(singletonList(Imp.builder().build()))
                     .build());
     
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(
                             StoredDataResult.of(singletonMap("123", storedRequestBidRequestJson), emptyMap(),
                                     emptyList())));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.succeeded()).isTrue();
    @@ -159,20 +175,227 @@ public void shouldReturnMergedBidRequest() throws IOException {
         }
     
         @Test
    -    public void shouldReturnAmpRequest() throws IOException {
    +    public void shouldReturnMergedDefaultAndBidRequest() throws IOException {
             // given
    -        given(applicationSettings.getAmpStoredData(anySet(), anySet(), any()))
    +        given(fileSystem.readFileBlocking(anyString()))
    +                .willReturn(Buffer.buffer(mapper.writeValueAsString(BidRequest.builder().at(1).build())));
    +
    +        final TimeoutFactory timeoutFactory = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault()));
    +        storedRequestProcessor = StoredRequestProcessor.create(
    +                DEFAULT_TIMEOUT,
    +                "path/to/default/request.json",
    +                false,
    +                fileSystem,
    +                applicationSettings,
    +                idGenerator,
    +                metrics,
    +                timeoutFactory,
    +                jacksonMapper,
    +                new JsonMerger(jacksonMapper));
    +
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .storedrequest(ExtStoredRequest.of("123"))
    +                        .build())));
    +
    +        final String storedRequestBidRequestJson = mapper.writeValueAsString(BidRequest.builder()
    +                .id("test-request-id")
    +                .tmax(1000L)
    +                .imp(singletonList(Imp.builder().build()))
    +                .build());
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.succeededFuture(
    +                        StoredDataResult.of(singletonMap("123", storedRequestBidRequestJson), emptyMap(),
    +                                emptyList())));
    +
    +        // when
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
    +
    +        // then
    +        assertThat(bidRequestFuture.succeeded()).isTrue();
    +        assertThat(bidRequestFuture.result()).isEqualTo(BidRequest.builder()
    +                .id("test-request-id")
    +                .at(1)
    +                .tmax(1000L)
    +                .imp(singletonList(Imp.builder().build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().storedrequest(ExtStoredRequest.of("123")).build()))
    +                .build());
    +    }
    +
    +    @Test
    +    public void processStoredRequestsShouldGenerateIdWhenAppAndFlagIsTrue() throws IOException {
    +        // given
    +        storedRequestProcessor = StoredRequestProcessor.create(
    +                500,
    +                null,
    +                true,
    +                fileSystem,
    +                applicationSettings,
    +                idGenerator,
    +                metrics,
    +                new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())),
    +                jacksonMapper,
    +                new JsonMerger(jacksonMapper));
    +
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .app(App.builder().build())
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .storedrequest(ExtStoredRequest.of("123"))
    +                        .build())));
    +
    +        final String storedRequestBidRequestJson = mapper.writeValueAsString(BidRequest.builder()
    +                .id("stored-bid-request")
    +                .build());
    +
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.succeededFuture(
    +                        StoredDataResult.of(singletonMap("123", storedRequestBidRequestJson), emptyMap(),
    +                                emptyList())));
    +
    +        // when
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
    +
    +        // then
    +        assertThat(bidRequestFuture.succeeded()).isTrue();
    +        assertThat(bidRequestFuture.result()).isEqualTo(BidRequest.builder()
    +                .id("generated-stored-id")
    +                .app(App.builder().build())
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().storedrequest(ExtStoredRequest.of("123")).build()))
    +                .build());
    +    }
    +
    +    @Test
    +    public void processStoredRequestsShouldGenerateIdWhenAppAndGenerateTemplateInStoredBidRequest() throws IOException {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .app(App.builder().build())
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .storedrequest(ExtStoredRequest.of("123"))
    +                        .build())));
    +
    +        final String storedRequestBidRequestJson = mapper.writeValueAsString(BidRequest.builder()
    +                .id("{{UUID}}")
    +                .build());
    +
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.succeededFuture(
    +                        StoredDataResult.of(singletonMap("123", storedRequestBidRequestJson), emptyMap(),
    +                                emptyList())));
    +
    +        // when
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
    +
    +        // then
    +        assertThat(bidRequestFuture.succeeded()).isTrue();
    +        assertThat(bidRequestFuture.result()).isEqualTo(BidRequest.builder()
    +                .app(App.builder().build())
    +                .id("generated-stored-id")
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().storedrequest(ExtStoredRequest.of("123")).build()))
    +                .build());
    +    }
    +
    +    @Test
    +    public void processAmpRequestShouldReturnAmpRequest() throws IOException {
    +        // given
    +        given(applicationSettings.getAmpStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.succeededFuture(StoredDataResult.of(
    +                        singletonMap("123", mapper.writeValueAsString(
    +                                BidRequest.builder().id("test-request-id").build())), emptyMap(), emptyList())));
    +
    +        // when
    +        final Future bidRequestFuture = storedRequestProcessor.processAmpRequest(null, "123",
    +                BidRequest.builder().build());
    +
    +        // then
    +        assertThat(bidRequestFuture.succeeded()).isTrue();
    +        assertThat(bidRequestFuture.result()).isEqualTo(BidRequest.builder()
    +                .id("test-request-id")
    +                .build());
    +    }
    +
    +    @Test
    +    public void shouldReturnMergedDefaultAndAmpRequest() throws IOException {
    +        // given
    +        given(fileSystem.readFileBlocking(anyString()))
    +                .willReturn(Buffer.buffer(mapper.writeValueAsString(BidRequest.builder().at(1).build())));
    +
    +        final TimeoutFactory timeoutFactory = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault()));
    +        storedRequestProcessor = StoredRequestProcessor.create(
    +                DEFAULT_TIMEOUT,
    +                "path/to/default/request.json",
    +                false,
    +                fileSystem,
    +                applicationSettings,
    +                idGenerator,
    +                metrics,
    +                timeoutFactory,
    +                jacksonMapper,
    +                new JsonMerger(jacksonMapper));
    +
    +        given(applicationSettings.getAmpStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(StoredDataResult.of(
                             singletonMap("123", mapper.writeValueAsString(
                                     BidRequest.builder().id("test-request-id").build())), emptyMap(), emptyList())));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processAmpRequest("123");
    +        final Future bidRequestFuture = storedRequestProcessor.processAmpRequest(null, "123",
    +                BidRequest.builder().build());
     
             // then
             assertThat(bidRequestFuture.succeeded()).isTrue();
             assertThat(bidRequestFuture.result()).isEqualTo(BidRequest.builder()
                     .id("test-request-id")
    +                .at(1)
    +                .build());
    +    }
    +
    +    @Test
    +    public void processAmpRequestShouldReplaceBidIdWhenGenerateIdFlagIsTrue() throws IOException {
    +        // given
    +        storedRequestProcessor = StoredRequestProcessor.create(
    +                500,
    +                null,
    +                true,
    +                fileSystem,
    +                applicationSettings,
    +                idGenerator,
    +                metrics,
    +                new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())),
    +                jacksonMapper,
    +                new JsonMerger(jacksonMapper));
    +
    +        given(applicationSettings.getAmpStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.succeededFuture(StoredDataResult.of(
    +                        singletonMap("123", mapper.writeValueAsString(
    +                                BidRequest.builder().id("origin-stored-id").build())), emptyMap(), emptyList())));
    +
    +        // when
    +        final Future bidRequestFuture = storedRequestProcessor.processAmpRequest(null, "123",
    +                BidRequest.builder().build());
    +
    +        // then
    +        assertThat(bidRequestFuture.succeeded()).isTrue();
    +        assertThat(bidRequestFuture.result()).isEqualTo(BidRequest.builder()
    +                .id("generated-stored-id")
    +                .build());
    +    }
    +
    +    @Test
    +    public void processAmpRequestShouldReplaceBidIdGenerateTemplateIsInStoredRequestId() throws IOException {
    +        // given
    +        given(applicationSettings.getAmpStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.succeededFuture(StoredDataResult.of(
    +                        singletonMap("123", mapper.writeValueAsString(
    +                                BidRequest.builder().id("{{UUID}}").build())), emptyMap(), emptyList())));
    +
    +        // when
    +        final Future bidRequestFuture = storedRequestProcessor.processAmpRequest(null, "123",
    +                BidRequest.builder().build());
    +
    +        // then
    +        assertThat(bidRequestFuture.succeeded()).isTrue();
    +        assertThat(bidRequestFuture.result()).isEqualTo(BidRequest.builder()
    +                .id("generated-stored-id")
                     .build());
         }
     
    @@ -185,11 +408,11 @@ public void shouldReturnFailedFutureWhenStoredBidRequestJsonIsNotValid() {
                             .build())));
     
             final Map storedRequestFetchResult = singletonMap("123", "{{}");
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any())).willReturn(Future
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any())).willReturn(Future
                     .succeededFuture(StoredDataResult.of(storedRequestFetchResult, emptyMap(), emptyList())));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.failed()).isTrue();
    @@ -207,11 +430,11 @@ public void shouldReturnFailedFutureWhenMergedResultCouldNotBeConvertedToBidRequ
     
             final Map storedRequestFetchResult = singletonMap("123", mapper.writeValueAsString(
                     mapper.createObjectNode().put("tmax", "stringValue")));
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any())).willReturn(
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any())).willReturn(
                     Future.succeededFuture(StoredDataResult.of(storedRequestFetchResult, emptyMap(), emptyList())));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.failed()).isTrue();
    @@ -229,7 +452,7 @@ public void shouldReturnFailedFutureIfIdWasNotPresentInStoredRequest() {
                             .build())));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.failed()).isTrue();
    @@ -253,12 +476,12 @@ public void shouldReturnBidRequestWithMergedImp() throws IOException {
                                     .build())
                             .build());
     
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(
                             StoredDataResult.of(emptyMap(), singletonMap("123", storedRequestImpJson), emptyList())));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.succeeded()).isTrue();
    @@ -279,7 +502,7 @@ public void shouldReturnFailedFutureWhenIdIsMissedInPrebidRequest() {
                                             null)))))));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.failed()).isTrue();
    @@ -296,12 +519,12 @@ public void shouldReturnFailedFutureWhenJsonBodyWasNotFoundByFetcher() {
                                     ExtImp.of(ExtImpPrebid.builder().storedrequest(ExtStoredRequest.of("123")).build(),
                                             null)))))));
     
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(
                             StoredDataResult.of(emptyMap(), emptyMap(), singletonList("No config found for id: 123"))));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.failed()).isTrue();
    @@ -317,10 +540,10 @@ public void shouldReturnImpAndBidRequestWithoutChangesIfStoredRequestIsAbsentInP
             final BidRequest bidRequest = givenBidRequest(builder -> builder.imp(singletonList(imp)));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
    -        verifyZeroInteractions(applicationSettings);
    +        verifyZeroInteractions(applicationSettings, metrics);
             assertThat(bidRequestFuture.succeeded()).isTrue();
             assertThat(bidRequestFuture.result().getImp().get(0)).isSameAs(imp);
             assertThat(bidRequestFuture.result()).isSameAs(bidRequest);
    @@ -340,12 +563,12 @@ public void shouldReturnChangedImpWithStoredRequestAndNotModifiedImpWithoutStore
             final String storedRequestImpJson = mapper.writeValueAsString(Imp.builder().banner(Banner.builder()
                     .format(singletonList(Format.builder().w(300).h(250).build())).build()).build());
     
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(
                             StoredDataResult.of(emptyMap(), singletonMap("123", storedRequestImpJson), emptyList())));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.succeeded()).isTrue();
    @@ -371,7 +594,7 @@ public void shouldReturnFailedFutureIfOneImpWithValidStoredRequestAndAnotherWith
                                             null)))))));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.failed()).isTrue();
    @@ -389,7 +612,7 @@ public void shouldReturnFailedFutureIfImpsStoredRequestIdHasIncorrectType() {
                                             .set("id", mapper.createObjectNode().putArray("id")
                                                     .add("id"))))).id("imp-test")))));
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // when
             assertThat(bidRequestFuture.failed()).isTrue();
    @@ -407,11 +630,11 @@ public void shouldReturnFailedFutureIfStoredRequestFetcherReturnsFailedFuture()
                                     ExtImp.of(ExtImpPrebid.builder().storedrequest(ExtStoredRequest.of("123")).build(),
                                             null)))))));
     
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.failedFuture(new Exception("Error during file fetching")));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.failed()).isTrue();
    @@ -429,12 +652,12 @@ public void shouldReturnFailedFutureWhenStoredImpJsonIsNotValid() {
                                     ExtImp.of(ExtImpPrebid.builder().storedrequest(ExtStoredRequest.of("123")).build(),
                                             null)))))));
     
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(
                             StoredDataResult.of(emptyMap(), singletonMap("123", "{{}"), emptyList())));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.failed()).isTrue();
    @@ -454,11 +677,11 @@ public void shouldReturnFailedFutureWhenMergedResultCantBeConvertedToImp() throw
     
             final Map storedImpFetchResult = singletonMap("123", mapper.writeValueAsString(
                     mapper.createObjectNode().put("secure", "stringValue")));
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(StoredDataResult.of(emptyMap(), storedImpFetchResult, emptyList())));
     
             // when
    -        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(bidRequest);
    +        final Future bidRequestFuture = storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             assertThat(bidRequestFuture.failed()).isTrue();
    @@ -470,59 +693,61 @@ public void shouldReturnFailedFutureWhenMergedResultCantBeConvertedToImp() throw
         @Test
         public void shouldUseTimeoutFromRequest() {
             // given
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.failedFuture((String) null));
     
             // when
    -        storedRequestProcessor.processStoredRequests(givenBidRequest(builder -> builder
    +        storedRequestProcessor.processStoredRequests(null, givenBidRequest(builder -> builder
                     .ext(ExtRequest.of(ExtRequestPrebid.builder().storedrequest(ExtStoredRequest.of("bidRequest")).build()))
                     .tmax(1000L)));
     
             // then
             final ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Timeout.class);
    -        verify(applicationSettings).getStoredData(anySet(), anySet(), timeoutCaptor.capture());
    +        verify(applicationSettings).getStoredData(any(), anySet(), anySet(), timeoutCaptor.capture());
             assertThat(timeoutCaptor.getValue().remaining()).isEqualTo(1000L);
         }
     
         @Test
         public void shouldUseDefaultTimeoutIfMissingInRequest() {
             // given
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.failedFuture((String) null));
     
             // when
    -        storedRequestProcessor.processStoredRequests(givenBidRequest(builder -> builder
    +        storedRequestProcessor.processStoredRequests(null, givenBidRequest(builder -> builder
                     .ext(ExtRequest.of(ExtRequestPrebid.builder()
                             .storedrequest(ExtStoredRequest.of("bidRequest"))
                             .build()))));
     
             // then
             final ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Timeout.class);
    -        verify(applicationSettings).getStoredData(anySet(), anySet(), timeoutCaptor.capture());
    -        assertThat(timeoutCaptor.getValue().remaining()).isEqualTo(DEFAULT_TIMEOUT);
    +        verify(applicationSettings).getStoredData(any(), anySet(), anySet(), timeoutCaptor.capture());
    +        assertThat(timeoutCaptor.getValue().remaining()).isEqualTo(500);
         }
     
         @Test
    -    public void processStoredRequestsShouldNotUpdateMetrics() {
    +    public void processStoredRequestsShouldNotUpdateMetricsIfApplicationSettingsFailed() {
             // given
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.failedFuture("failed"));
     
             // when
    -        storedRequestProcessor.processStoredRequests(givenBidRequest(Function.identity()));
    +        storedRequestProcessor.processStoredRequests(null, givenBidRequest(builder -> builder
    +                .ext(ExtRequest.of(
    +                        ExtRequestPrebid.builder().storedrequest(ExtStoredRequest.of("bidRequest")).build()))));
     
             // then
             verifyZeroInteractions(metrics);
         }
     
         @Test
    -    public void processAmpRequestShouldNotUpdateMetrics() {
    +    public void processAmpRequestShouldNotUpdateMetricsIfApplicationSettingsFailed() {
             // given
    -        given(applicationSettings.getAmpStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getAmpStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.failedFuture("failed"));
     
             // when
    -        storedRequestProcessor.processAmpRequest("123");
    +        storedRequestProcessor.processAmpRequest(null, "123", BidRequest.builder().build());
     
             // then
             verifyZeroInteractions(metrics);
    @@ -542,12 +767,12 @@ public void processStoredRequestsShouldUpdateRequestAndImpMetricsAsExpected() {
                                             ExtImpPrebid.builder().storedrequest(ExtStoredRequest.of("not_found")).build(),
                                             null)))))));
     
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(StoredDataResult.of(
                             singletonMap("123", "stored_request"), singletonMap("321", "stored_imp"), emptyList())));
     
             // when
    -        storedRequestProcessor.processStoredRequests(bidRequest);
    +        storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             verify(metrics).updateStoredRequestMetric(eq(true));
    @@ -563,11 +788,11 @@ public void processStoredRequestsShouldUpdateRequestMissingMetrics() {
                             .storedrequest(ExtStoredRequest.of("123"))
                             .build())));
     
    -        given(applicationSettings.getStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(StoredDataResult.of(emptyMap(), emptyMap(), emptyList())));
     
             // when
    -        storedRequestProcessor.processStoredRequests(bidRequest);
    +        storedRequestProcessor.processStoredRequests(null, bidRequest);
     
             // then
             verify(metrics).updateStoredRequestMetric(eq(false));
    @@ -576,12 +801,12 @@ public void processStoredRequestsShouldUpdateRequestMissingMetrics() {
         @Test
         public void processAmpRequestShouldUpdateRequestFoundMetric() {
             // given
    -        given(applicationSettings.getAmpStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getAmpStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(
                             StoredDataResult.of(singletonMap("123", "amp"), emptyMap(), emptyList())));
     
             // when
    -        storedRequestProcessor.processAmpRequest("123");
    +        storedRequestProcessor.processAmpRequest(null, "123", BidRequest.builder().build());
     
             // then
             verify(metrics).updateStoredRequestMetric(true);
    @@ -590,12 +815,12 @@ public void processAmpRequestShouldUpdateRequestFoundMetric() {
         @Test
         public void processAmpRequestShouldUpdateRequestMissingMetrics() {
             // given
    -        given(applicationSettings.getAmpStoredData(anySet(), anySet(), any()))
    +        given(applicationSettings.getAmpStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(
                             StoredDataResult.of(emptyMap(), emptyMap(), emptyList())));
     
             // when
    -        storedRequestProcessor.processAmpRequest("123");
    +        storedRequestProcessor.processAmpRequest(null, "123", BidRequest.builder().build());
     
             // then
             verify(metrics).updateStoredRequestMetric(false);
    @@ -619,15 +844,16 @@ public void impToStoredVideoJsonShouldReturnExpectedVideoStoredDataResult() thro
             storedIdToJson.put("st1", mapper.writeValueAsString(storedImp1));
             storedIdToJson.put("st2", mapper.writeValueAsString(storedImp2));
     
    -        given(applicationSettings.getStoredData(any(), any(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(StoredDataResult.of(emptyMap(), storedIdToJson, emptyList())));
     
             // when
    -        final Future result = storedRequestProcessor.videoStoredDataResult(
    +        final Future result = storedRequestProcessor.videoStoredDataResult(null,
                     Arrays.asList(imp1, imp2), emptyList(), null);
     
             // then
    -        verify(applicationSettings).getStoredData(any(), eq(new HashSet<>(Arrays.asList("st1", "st2"))), any());
    +        verify(applicationSettings).getStoredData(any(), anySet(), eq(new HashSet<>(Arrays.asList("st1", "st2"))),
    +                any());
     
             assertThat(result.result().getImpIdToStoredVideo()).containsOnly(entry("id2", storedVideo),
                     entry("id1", storedVideo));
    @@ -649,15 +875,16 @@ public void impToStoredVideoJsonShouldReturnExpectedVideoStoredDataResultErrors(
             final Map storedIdToJson = new HashMap<>();
             storedIdToJson.put("st1", mapper.writeValueAsString(storedImp1));
     
    -        given(applicationSettings.getStoredData(any(), any(), any()))
    +        given(applicationSettings.getStoredData(any(), anySet(), anySet(), any()))
                     .willReturn(Future.succeededFuture(StoredDataResult.of(emptyMap(), storedIdToJson, emptyList())));
     
             // when
    -        final Future result = storedRequestProcessor.videoStoredDataResult(
    +        final Future result = storedRequestProcessor.videoStoredDataResult(null,
                     Arrays.asList(imp1, imp2), new ArrayList<>(), null);
     
             // then
    -        verify(applicationSettings).getStoredData(any(), eq(new HashSet<>(Arrays.asList("st1", "st2"))), any());
    +        verify(applicationSettings).getStoredData(any(), anySet(), eq(new HashSet<>(Arrays.asList("st1", "st2"))),
    +                any());
     
             assertThat(result.result().getErrors()).containsOnly(
                     "No stored Imp for stored id st2",
    diff --git a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java
    index 6cfee994e73..eb8db31babe 100644
    --- a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java
    +++ b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java
    @@ -16,7 +16,6 @@
     import org.prebid.server.VertxTest;
     import org.prebid.server.auction.model.BidderResponse;
     import org.prebid.server.auction.model.StoredResponseResult;
    -import org.prebid.server.bidder.BidderCatalog;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderSeatBid;
     import org.prebid.server.exception.InvalidRequestException;
    @@ -41,12 +40,12 @@
     
     import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
    +import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.Collections.singletonMap;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatThrownBy;
     import static org.mockito.ArgumentMatchers.any;
    -import static org.mockito.ArgumentMatchers.eq;
     import static org.mockito.BDDMockito.given;
     import static org.mockito.Mockito.verifyZeroInteractions;
     
    @@ -57,13 +56,9 @@ public class StoredResponseProcessorTest extends VertxTest {
     
         @Mock
         private ApplicationSettings applicationSettings;
    -    @Mock
    -    private BidderCatalog bidderCatalog;
     
         private StoredResponseProcessor storedResponseProcessor;
     
    -    @Mock
    -    private BidderAliases aliases;
         private Timeout timeout;
     
         @Before
    @@ -71,18 +66,13 @@ public void setUp() {
             final TimeoutFactory timeoutFactory = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault()));
             timeout = timeoutFactory.create(500L);
     
    -        storedResponseProcessor = new StoredResponseProcessor(applicationSettings, bidderCatalog, jacksonMapper);
    +        storedResponseProcessor = new StoredResponseProcessor(applicationSettings, jacksonMapper);
         }
     
         @Test
         public void getStoredResponseResultShouldReturnSeatBidsForAuctionResponseId() throws JsonProcessingException {
             // given
    -        final List imps = singletonList(Imp.builder().id("impId")
    -                .ext(mapper.valueToTree(
    -                        ExtImp.of(
    -                                ExtImpPrebid.builder().storedAuctionResponse(ExtStoredAuctionResponse.of("1")).build(),
    -                                null)))
    -                .build());
    +        final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1"), null));
     
             given(applicationSettings.getStoredResponses(any(), any()))
                     .willReturn(Future.succeededFuture(StoredResponseDataResult.of(singletonMap("1",
    @@ -91,73 +81,71 @@ public void getStoredResponseResultShouldReturnSeatBidsForAuctionResponseId() th
                             emptyList())));
     
             // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final Future result = storedResponseProcessor.getStoredResponseResult(imps, timeout);
     
             // then
    -        assertThat(result.result()).isEqualTo(StoredResponseResult.of(emptyList(),
    -                singletonList(SeatBid.builder().seat("rubicon")
    -                        .bid(singletonList(Bid.builder().id("id").impid("impId").build())).build())));
    +        assertThat(result.result()).isEqualTo(StoredResponseResult.of(
    +                emptyList(),
    +                singletonList(SeatBid.builder()
    +                        .seat("rubicon")
    +                        .bid(singletonList(Bid.builder().id("id").impid("impId").build()))
    +                        .build()),
    +                emptyMap()));
         }
     
         @Test
         public void getStoredResponseResultShouldNotChangeImpsAndReturnSeatBidsWhenThereAreNoStoredIds() {
             // given
    -        final List imps = singletonList(Imp.builder()
    -                .ext(mapper.createObjectNode().put("rubicon", 1))
    -                .build());
    -        given(bidderCatalog.isValidName(any())).willReturn(true);
    -
    +        final Imp imp = Imp.builder()
    +                .ext(mapper.valueToTree(ExtImp.of(
    +                        ExtImpPrebid.builder().bidder(mapper.createObjectNode().put("rubicon", 1)).build(),
    +                        null)))
    +                .build();
             // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final Future result =
    +                storedResponseProcessor.getStoredResponseResult(singletonList(imp), timeout);
     
             // then
             assertThat(result.result()).isEqualTo(StoredResponseResult.of(
    -                singletonList(Imp.builder().ext(mapper.createObjectNode().put("rubicon", 1)).build()),
    -                emptyList()));
    +                singletonList(imp),
    +                emptyList(),
    +                emptyMap()));
             verifyZeroInteractions(applicationSettings);
         }
     
         @Test
    -    public void getStoredResponseResultShouldAddImpToRequiredRequestWhenItsStoredBidResponseIsEmpty() {
    +    public void getStoredResponseResultShouldAddImpToRequiredRequestWhenItsStoredAuctionResponseIsNull() {
             // given
    -        final List imps = singletonList(Imp.builder().id("impId1")
    -                .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                        .storedBidResponse(emptyList())
    -                        .build(), null)))
    -                .build());
    +        final List imps = singletonList(givenImp("impId1", null, null));
     
             // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final Future result =
    +                storedResponseProcessor.getStoredResponseResult(imps, timeout);
     
             // then
             assertThat(result.result()).isEqualTo(StoredResponseResult.of(
    -                singletonList(Imp.builder().id("impId1")
    -                        .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                                .storedBidResponse(emptyList())
    -                                .build(), null)))
    +                singletonList(Imp.builder()
    +                        .id("impId1")
    +                        .ext(mapper.valueToTree(ExtImp.of(
    +                                ExtImpPrebid.builder().storedAuctionResponse(null).build(),
    +                                null)))
                             .build()),
    -                emptyList()));
    +                emptyList(),
    +                emptyMap()));
             verifyZeroInteractions(applicationSettings);
         }
     
         @Test
         public void getStoredResponseResultShouldReturnFailedFutureWhenErrorHappenedDuringRetrievingStoredResponse() {
             // given
    -        final List imps = singletonList(Imp.builder()
    -                .ext(mapper.valueToTree(ExtImp.of(
    -                        ExtImpPrebid.builder().storedAuctionResponse(ExtStoredAuctionResponse.of("1")).build(),
    -                        null)))
    -                .build());
    +        final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1"), null));
     
             given(applicationSettings.getStoredResponses(any(), any()))
                     .willReturn(Future.failedFuture(new PreBidException("Failed.")));
     
             // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final Future result =
    +                storedResponseProcessor.getStoredResponseResult(imps, timeout);
     
             // then
             assertThat(result.failed()).isTrue();
    @@ -167,270 +155,150 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenErrorHappenedDuri
         }
     
         @Test
    -    public void getStoredResponseResultShouldReturnSeatBidsForBidStoredResponseId() throws JsonProcessingException {
    +    public void getStoredResponseResultShouldReturnResultForBidStoredResponseId() {
             // given
    -        final List imps = singletonList(Imp.builder().id("impId1")
    -                .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                                .storedBidResponse(asList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"),
    -                                        ExtStoredBidResponse.of("appnexus", "storedBidResponseId2")))
    -                                .build(),
    -                        null)))
    -                .build());
    -
    -        final Map storedResponse = new HashMap<>();
    -        storedResponse.put("storedBidResponseId1", mapper.writeValueAsString(singletonList(
    -                SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id1").build()))
    -                        .build())));
    -        storedResponse.put("storedBidResponseId2", mapper.writeValueAsString(singletonList(
    -                SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id2").build()))
    -                        .build())));
    +        final Imp imp = givenImp("impId1", null, asList(
    +                ExtStoredBidResponse.of("rubicon", "storedBidResponseId1"),
    +                ExtStoredBidResponse.of("appnexus", "storedBidResponseId2")));
     
             given(applicationSettings.getStoredResponses(any(), any())).willReturn(
    -                Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList())));
    +                Future.succeededFuture(StoredResponseDataResult.of(
    +                        doubleMap("storedBidResponseId1", "storedBidResponse1",
    +                                "storedBidResponseId2", "storedBidResponse2"), emptyList())));
     
             // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final Future result =
    +                storedResponseProcessor.getStoredResponseResult(singletonList(imp), timeout);
     
             // then
    -        assertThat(result.result()).isEqualTo(StoredResponseResult.of(emptyList(),
    -                asList(
    -                        SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id2").impid("impId1")
    -                                .build())).build(),
    -                        SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id1").impid("impId1")
    -                                .build())).build())));
    +        assertThat(result.result()).isEqualTo(StoredResponseResult.of(
    +                singletonList(imp),
    +                emptyList(),
    +                singletonMap("impId1", doubleMap("rubicon", "storedBidResponse1", "appnexus", "storedBidResponse2"))));
         }
     
         @Test
    -    public void getStoredResponseResultShouldReturnSeatBidsForBidAndAuctionStoredResponseId()
    +    public void getStoredResponseResultShouldReturnResultForBidAndAuctionStoredResponseId()
                 throws JsonProcessingException {
             // given
    -        final List imps = asList(
    -                Imp.builder().id("impId1")
    -                        .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                                .storedAuctionResponse(ExtStoredAuctionResponse.of("storedAuctionRequest"))
    -                                .build(), null))).build(),
    -                Imp.builder().id("impId2")
    -                        .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                                        .storedBidResponse(singletonList(
    -                                                ExtStoredBidResponse.of("rubicon", "storedBidRequest")))
    -                                        .build(),
    -                                null)))
    -                        .build());
    +        final Imp imp1 = givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponseId"), null);
    +        final Imp imp2 = givenImp("impId2", null,
    +                singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId")));
    +        final List imps = asList(imp1, imp2);
     
             final Map storedResponse = new HashMap<>();
    -        storedResponse.put("storedAuctionRequest", mapper.writeValueAsString(singletonList(
    -                SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").build()))
    -                        .build())));
    -        storedResponse.put("storedBidRequest", mapper.writeValueAsString(singletonList(
    -                SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").build()))
    -                        .build())));
    +        storedResponse.put("storedAuctionResponseId", mapper.writeValueAsString(singletonList(
    +                SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").build())).build())));
    +        storedResponse.put("storedBidResponseId", "storedBidResponse");
     
             given(applicationSettings.getStoredResponses(any(), any())).willReturn(
                     Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList())));
     
             // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final Future result =
    +                storedResponseProcessor.getStoredResponseResult(imps, timeout);
     
             // then
    -        assertThat(result.result()).isEqualTo(StoredResponseResult.of(emptyList(),
    -                asList(
    -                        SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").impid("impId1")
    -                                .build())).build(),
    -                        SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").impid("impId2")
    -                                .build())).build())));
    +        assertThat(result.result()).isEqualTo(StoredResponseResult.of(
    +                singletonList(imp2),
    +                singletonList(
    +                        SeatBid.builder()
    +                                .seat("appnexus")
    +                                .bid(singletonList(Bid.builder().id("id1").impid("impId1").build()))
    +                                .build()),
    +                singletonMap("impId2", singletonMap("rubicon", "storedBidResponse"))));
         }
     
         @Test
    -    public void getStoredResponseResultShouldRemoveMockedBiddersFromImps() throws JsonProcessingException {
    -        final ObjectNode impExt = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1")))
    -                .build(), null));
    -        impExt.put("rubicon", 1);
    -        impExt.put("appnexus", 2);
    -
    -        given(bidderCatalog.isValidName(any())).willReturn(true);
    -
    -        final List imps = singletonList(Imp.builder().id("impId1").ext(impExt).build());
    -
    -        final Map storedResponse = new HashMap<>();
    -        storedResponse.put("storedBidResponseId1", mapper.writeValueAsString(singletonList(
    -                SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id1").build()))
    -                        .build())));
    -
    +    public void getStoredResponseResultShouldThrowInvalidRequestExceptionWhenStoredAuctionResponseWasNotFound() {
    +        // given
    +        final Imp imp1 = givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponseId"), null);
             given(applicationSettings.getStoredResponses(any(), any())).willReturn(
    -                Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList())));
    +                Future.succeededFuture(StoredResponseDataResult.of(emptyMap(), emptyList())));
     
             // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final Future result =
    +                storedResponseProcessor.getStoredResponseResult(singletonList(imp1), timeout);
     
             // then
    -        final ObjectNode impExtResult = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1")))
    -                .build(), null));
    -        impExtResult.put("appnexus", 2);
    -
    -        assertThat(result.result()).isEqualTo(StoredResponseResult.of(singletonList(Imp.builder().id("impId1")
    -                        .ext(impExtResult).build()),
    -                singletonList(SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().impid("impId1")
    -                        .id("id1").build())).build())));
    +        assertThat(result.failed()).isTrue();
    +        assertThat(result.cause()).isInstanceOf(InvalidRequestException.class)
    +                .hasMessage("Failed to fetch stored auction response for impId = impId1 and storedAuctionResponse id "
    +                        + "= storedAuctionResponseId.");
         }
     
         @Test
         public void getStoredResponseResultShouldMergeStoredSeatBidsForTheSameBidder() throws JsonProcessingException {
             // given
             final List imps = asList(
    -                Imp.builder().id("impId1")
    -                        .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                                        .storedAuctionResponse(ExtStoredAuctionResponse.of("storedAuctionRequest"))
    -                                        .build(),
    -                                null))).build(),
    -                Imp.builder().id("impId2")
    -                        .ext(mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                                .storedBidResponse(
    -                                        singletonList(
    -                                                ExtStoredBidResponse.of("rubicon", "storedBidRequest")))
    -                                .build(), null)))
    -                        .build());
    +                givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponse1"), null),
    +                givenImp("impId2", ExtStoredAuctionResponse.of("storedAuctionResponse2"), null));
     
             final Map storedResponse = new HashMap<>();
    -        storedResponse.put("storedAuctionRequest", mapper.writeValueAsString(asList(
    -                SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").build()))
    -                        .build(), SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id3").build()))
    -                        .build())));
    -        storedResponse.put("storedBidRequest", mapper.writeValueAsString(singletonList(
    -                SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").build()))
    -                        .build())));
    +        storedResponse.put("storedAuctionResponse1", mapper.writeValueAsString(asList(
    +                SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").build())).build(),
    +                SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id3").build())).build())));
    +        storedResponse.put("storedAuctionResponse2", mapper.writeValueAsString(singletonList(
    +                SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").build())).build())));
     
             given(applicationSettings.getStoredResponses(any(), any())).willReturn(
                     Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList())));
     
    -        // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    -
    -        // then
    -        assertThat(result.result()).isEqualTo(StoredResponseResult.of(emptyList(),
    -                asList(
    -                        SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").impid("impId1")
    -                                .build())).build(),
    -                        SeatBid.builder().seat("rubicon").bid(asList(Bid.builder().id("id3").impid("impId1").build(),
    -                                Bid.builder().id("id2").impid("impId2").build())).build())));
    -    }
    -
    -    @Test
    -    public void getStoredResponseResultShouldSupportAliasesWhenDecidingIfImpRequiredRequestToExchange()
    -            throws JsonProcessingException {
    -        final ObjectNode impExt = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1")))
    -                .build(), null));
    -        impExt.put("rubicon", 1);
    -        impExt.put("appnexusAlias", 2);
    -
    -        given(bidderCatalog.isValidName(any())).willReturn(false);
    -
    -        final List imps = singletonList(Imp.builder().id("impId1").ext(impExt).build());
    -
    -        final Map storedResponse = new HashMap<>();
    -        storedResponse.put("storedBidResponseId1", mapper.writeValueAsString(singletonList(
    -                SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id1").build()))
    -                        .build())));
    -
    -        given(applicationSettings.getStoredResponses(any(), any())).willReturn(
    -                Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList())));
    -
    -        given(aliases.isAliasDefined(eq("appnexusAlias"))).willReturn(true);
    -        given(aliases.resolveBidder(eq("appnexusAlias"))).willReturn("appnexus");
    -        given(aliases.resolveAliasVendorId(eq("appnexusAlias"))).willReturn(1);
    -
             // when
             final Future result =
    -                storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout);
    +                storedResponseProcessor.getStoredResponseResult(imps, timeout);
     
             // then
    -        final ObjectNode impExtResult = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", "storedBidResponseId1")))
    -                .build(), null));
    -        impExtResult.put("appnexusAlias", 2);
    -
    -        assertThat(result.result()).isEqualTo(StoredResponseResult.of(singletonList(Imp.builder().ext(impExtResult)
    -                        .id("impId1").build()),
    -                singletonList(SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder()
    -                        .id("id1").impid("impId1").build())).build())));
    +        assertThat(result.result()).isEqualTo(StoredResponseResult.of(
    +                emptyList(),
    +                asList(
    +                        SeatBid.builder()
    +                                .seat("appnexus")
    +                                .bid(singletonList(Bid.builder().id("id1").impid("impId1").build()))
    +                                .build(),
    +                        SeatBid.builder()
    +                                .seat("rubicon")
    +                                .bid(asList(
    +                                        Bid.builder().id("id2").impid("impId2").build(),
    +                                        Bid.builder().id("id3").impid("impId1").build()
    +                                ))
    +                                .build()),
    +                emptyMap()));
         }
     
         @Test
    -    public void getStoredResponseResultShouldReturnFailedFutureWhenImpExtIsNotValid() {
    +    public void getStoredResponseResultShouldThrowInvalidExceptionWhenImpExtIsNotValid() {
             // given
    -        final List imps = singletonList(Imp.builder().id("impId").ext(mapper.createObjectNode()
    -                .put("prebid", 5)).build());
    -
    -        // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final List imps = singletonList(Imp.builder()
    +                .id("impId")
    +                .ext(mapper.createObjectNode().put("prebid", 5))
    +                .build());
     
    -        // then
    -        assertThat(result.failed()).isTrue();
    -        assertThat(result.cause())
    +        // when and then
    +        assertThatThrownBy(() -> storedResponseProcessor.getStoredResponseResult(imps, timeout))
    +                .isInstanceOf(InvalidRequestException.class)
                     .hasMessageStartingWith("Error decoding bidRequest.imp.ext for impId = impId :");
         }
     
    -    @Test
    -    public void getStoredResponseResultShouldReturnFailedFutureWhenBidderIsMissedInStoredBidResponse() {
    -        // given
    -        final ObjectNode impExt = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                .storedBidResponse(singletonList(ExtStoredBidResponse.of(null, "storedBidResponseId1")))
    -                .build(), null));
    -        final List imps = singletonList(Imp.builder().id("impId").ext(impExt).build());
    -
    -        // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    -
    -        // then
    -        assertThat(result.failed()).isTrue();
    -        assertThat(result.cause())
    -                .hasMessage("Bidder was not defined for imp.ext.prebid.storedBidResponse for imp with id impId");
    -    }
    -
    -    @Test
    -    public void getStoredResponseResultShouldReturnFailedFutureWhenIdIsMissedInStoredBidResponse() {
    -        // given
    -        final ObjectNode impExt = mapper.valueToTree(ExtImp.of(ExtImpPrebid.builder()
    -                .storedBidResponse(singletonList(ExtStoredBidResponse.of("rubicon", null))).build(), null));
    -        final List imps = singletonList(Imp.builder().ext(impExt).id("impId").build());
    -
    -        // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    -
    -        // then
    -        assertThat(result.failed()).isTrue();
    -        assertThat(result.cause())
    -                .hasMessage("Id was not defined for imp.ext.prebid.storedBidResponse for imp with id impId");
    -    }
    -
         @Test
         public void getStoredResponseResultShouldReturnFailedFutureWhenSeatIsEmptyInStoredSeatBid()
                 throws JsonProcessingException {
    +
             // given
    -        final List imps = singletonList(Imp.builder()
    -                .ext(mapper.valueToTree(ExtImp.of(
    -                        ExtImpPrebid.builder().storedAuctionResponse(ExtStoredAuctionResponse.of("1")).build(),
    -                        null)))
    -                .build());
    +        final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1"), null));
     
             given(applicationSettings.getStoredResponses(any(), any()))
    -                .willReturn(Future.succeededFuture(StoredResponseDataResult.of(singletonMap("responseId",
    -                        mapper.writeValueAsString(singletonList(SeatBid.builder().bid(singletonList(
    -                                Bid.builder().id("id").build())).build()))),
    +                .willReturn(Future.succeededFuture(StoredResponseDataResult.of(
    +                        singletonMap(
    +                                "1",
    +                                mapper.writeValueAsString(singletonList(SeatBid.builder()
    +                                        .bid(singletonList(Bid.builder().id("id").build()))
    +                                        .build()))),
                             emptyList())));
     
             // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final Future result =
    +                storedResponseProcessor.getStoredResponseResult(imps, timeout);
     
             // then
             assertThat(result.failed()).isTrue();
    @@ -441,23 +309,23 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenSeatIsEmptyInStor
         @Test
         public void getStoredResponseResultShouldReturnFailedFutureWhenBidsAreEmptyInStoredSeatBid()
                 throws JsonProcessingException {
    +
             // given
    -        final List imps = singletonList(Imp.builder()
    -                .ext(mapper.valueToTree(ExtImp.of(
    -                        ExtImpPrebid.builder().storedAuctionResponse(ExtStoredAuctionResponse.of("1")).build(),
    -                        null)))
    -                .build());
    +        final List imps = singletonList(
    +                givenImp("impId", ExtStoredAuctionResponse.of("1"), null));
     
             given(applicationSettings.getStoredResponses(any(), any()))
    -                .willReturn(Future.succeededFuture(StoredResponseDataResult.of(singletonMap("responseId",
    -                        mapper.writeValueAsString(singletonList(SeatBid.builder()
    -                                .seat("seat")
    -                                .build()))),
    +                .willReturn(Future.succeededFuture(StoredResponseDataResult.of(
    +                        singletonMap(
    +                                "1",
    +                                mapper.writeValueAsString(singletonList(SeatBid.builder()
    +                                        .seat("seat")
    +                                        .build()))),
                             emptyList())));
     
             // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final Future result =
    +                storedResponseProcessor.getStoredResponseResult(imps, timeout);
     
             // then
             assertThat(result.failed()).isTrue();
    @@ -466,20 +334,16 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenBidsAreEmptyInSto
         }
     
         @Test
    -    public void getStoredResponseResultShouldReturnFailedFutureSeatBidsCantBeParsed() {
    +    public void getStoredResponseResultShouldReturnFailedFutureSeatBidsCannotBeParsed() {
             // given
    -        final List imps = singletonList(Imp.builder().id("impId")
    -                .ext(mapper.valueToTree(ExtImp.of(
    -                        ExtImpPrebid.builder().storedAuctionResponse(ExtStoredAuctionResponse.of("1")).build(),
    -                        null))).build());
    +        final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1"), null));
     
    -        given(applicationSettings.getStoredResponses(any(), any()))
    -                .willReturn(Future.succeededFuture(StoredResponseDataResult.of(
    -                        singletonMap("1", "{invalid"), emptyList())));
    +        given(applicationSettings.getStoredResponses(any(), any())).willReturn(Future.succeededFuture(
    +                StoredResponseDataResult.of(singletonMap("1", "{invalid"), emptyList())));
     
             // when
    -        final Future result = storedResponseProcessor.getStoredResponseResult(imps,
    -                aliases, timeout);
    +        final Future result =
    +                storedResponseProcessor.getStoredResponseResult(imps, timeout);
     
             // then
             assertThat(result.failed()).isTrue();
    @@ -490,121 +354,210 @@ public void getStoredResponseResultShouldReturnFailedFutureSeatBidsCantBeParsed(
         @Test
         public void mergeWithBidderResponsesShouldReturnMergedStoredSeatWithResponse() {
             // given
    -        final List bidderResponses = singletonList(BidderResponse.of("rubicon", BidderSeatBid.of(
    -                singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(),
    -                emptyList()), 100));
    +        final List bidderResponses = singletonList(BidderResponse.of(
    +                "rubicon",
    +                BidderSeatBid.of(
    +                        singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")),
    +                        emptyList(),
    +                        emptyList()),
    +                100));
     
             final List seatBid = singletonList(SeatBid.builder()
    -                .seat("rubicon").bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())).build());
    +                .seat("rubicon")
    +                .bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build()))
    +                .build());
     
             final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build());
     
             // when
    -        final List result = storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid,
    -                imps);
    +        final List result =
    +                storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, imps);
     
             // then
    -        assertThat(result).contains(BidderResponse.of("rubicon", BidderSeatBid.of(
    -                asList(BidderBid.of(Bid.builder().id("bid2").impid("storedImp").build(), BidType.banner, "USD"),
    -                        BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(),
    -                emptyList()), 100));
    +        assertThat(result).contains(BidderResponse.of(
    +                "rubicon",
    +                BidderSeatBid.of(
    +                        asList(
    +                                BidderBid.of(
    +                                        Bid.builder()
    +                                                .id("bid2")
    +                                                .impid("storedImp")
    +                                                .build(),
    +                                        BidType.banner,
    +                                        "USD"),
    +                                BidderBid.of(
    +                                        Bid.builder()
    +                                                .id("bid1")
    +                                                .build(),
    +                                        BidType.banner,
    +                                        "USD")),
    +                        emptyList(),
    +                        emptyList()),
    +                100));
         }
     
         @Test
         public void mergeWithBidderResponsesShouldMergeBidderResponsesWithoutCorrespondingStoredSeatBid() {
             // given
    -        final List bidderResponses = singletonList(BidderResponse.of("rubicon", BidderSeatBid.of(
    -                singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(),
    -                emptyList()), 100));
    +        final List bidderResponses = singletonList(BidderResponse.of(
    +                "rubicon",
    +                BidderSeatBid.of(
    +                        singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")),
    +                        emptyList(),
    +                        emptyList()),
    +                100));
     
             final List seatBid = singletonList(SeatBid.builder()
    -                .seat("appnexus").bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())).build());
    +                .seat("appnexus")
    +                .bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build()))
    +                .build());
     
             final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build());
     
             // when
    -        final List result = storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid,
    -                imps);
    +        final List result =
    +                storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, imps);
     
             // then
             assertThat(result).contains(
    -                BidderResponse.of("rubicon", BidderSeatBid.of(
    -                        singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")),
    -                        emptyList(),
    -                        emptyList()), 100),
    -                BidderResponse.of("appnexus", BidderSeatBid.of(
    -                        singletonList(BidderBid.of(Bid.builder().id("bid2").impid("storedImp").build(),
    -                                BidType.banner, "USD")), emptyList(), emptyList()), 0));
    +                BidderResponse.of(
    +                        "rubicon",
    +                        BidderSeatBid.of(
    +                                singletonList(BidderBid.of(
    +                                        Bid.builder().id("bid1").build(),
    +                                        BidType.banner,
    +                                        "USD")),
    +                                emptyList(),
    +                                emptyList()),
    +                        100),
    +                BidderResponse.of(
    +                        "appnexus",
    +                        BidderSeatBid.of(
    +                                singletonList(BidderBid.of(
    +                                        Bid.builder().id("bid2").impid("storedImp").build(),
    +                                        BidType.banner,
    +                                        "USD")),
    +                                emptyList(),
    +                                emptyList()),
    +                        0));
         }
     
         @Test
         public void mergeWithBidderResponsesShouldMergeStoredSeatBidsWithoutBidderResponses() {
             // given
             final List seatBid = singletonList(SeatBid.builder()
    -                .seat("rubicon").bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())).build());
    +                .seat("rubicon")
    +                .bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build()))
    +                .build());
     
             final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build());
     
             // when
    -        final List result = storedResponseProcessor.mergeWithBidderResponses(emptyList(), seatBid,
    -                imps);
    +        final List result =
    +                storedResponseProcessor.mergeWithBidderResponses(emptyList(), seatBid, imps);
     
             // then
    -        assertThat(result).contains(BidderResponse.of("rubicon", BidderSeatBid.of(
    -                singletonList(BidderBid.of(Bid.builder().id("bid2").impid("storedImp").build(), BidType.banner, "USD")),
    -                emptyList(), emptyList()), 0));
    +        assertThat(result).contains(BidderResponse.of(
    +                "rubicon",
    +                BidderSeatBid.of(
    +                        singletonList(BidderBid.of(
    +                                Bid.builder().id("bid2").impid("storedImp").build(),
    +                                BidType.banner,
    +                                "USD")),
    +                        emptyList(),
    +                        emptyList()),
    +                0));
         }
     
         @Test
         public void mergeWithBidderResponsesShouldResolveCurrencyFromBidderResponse() {
             // given
    -        final List bidderResponses = singletonList(BidderResponse.of("rubicon", BidderSeatBid.of(
    -                singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "EUR")), emptyList(),
    -                emptyList()), 100));
    +        final List bidderResponses = singletonList(BidderResponse.of(
    +                "rubicon",
    +                BidderSeatBid.of(
    +                        singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "EUR")),
    +                        emptyList(),
    +                        emptyList()),
    +                100));
     
             final List seatBid = singletonList(SeatBid.builder()
    -                .seat("rubicon").bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build())).build());
    +                .seat("rubicon")
    +                .bid(singletonList(Bid.builder().id("bid2").impid("storedImp").build()))
    +                .build());
     
             final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build());
     
             // when
    -        final List result = storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid,
    -                imps);
    +        final List result =
    +                storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, imps);
     
             // then
    -        assertThat(result).contains(BidderResponse.of("rubicon", BidderSeatBid.of(
    -                asList(BidderBid.of(Bid.builder().id("bid2").impid("storedImp").build(), BidType.banner, "EUR"),
    -                        BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "EUR")), emptyList(),
    -                emptyList()), 100));
    +        assertThat(result).contains(BidderResponse.of(
    +                "rubicon",
    +                BidderSeatBid.of(
    +                        asList(
    +                                BidderBid.of(
    +                                        Bid.builder().id("bid2").impid("storedImp").build(),
    +                                        BidType.banner,
    +                                        "EUR"),
    +                                BidderBid.of(
    +                                        Bid.builder().id("bid1").build(),
    +                                        BidType.banner,
    +                                        "EUR")),
    +                        emptyList(),
    +                        emptyList()),
    +                100));
         }
     
         @Test
         public void mergeWithBidderResponsesShouldResolveBidTypeFromStoredBidExt() {
             // given
    -        final List bidderResponses = singletonList(BidderResponse.of("rubicon", BidderSeatBid.of(
    -                singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(),
    -                emptyList()), 100));
    +        final List bidderResponses = singletonList(BidderResponse.of(
    +                "rubicon",
    +                BidderSeatBid.of(
    +                        singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")),
    +                        emptyList(),
    +                        emptyList()),
    +                100));
     
             final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().type(BidType.video).build();
     
             final List seatBid = singletonList(SeatBid.builder()
    -                .seat("rubicon").bid(singletonList(Bid.builder().ext(mapper.createObjectNode()
    -                        .set("prebid", mapper.valueToTree(extBidPrebid))).id("bid2").impid("storedImp").build()))
    +                .seat("rubicon")
    +                .bid(singletonList(Bid.builder()
    +                        .id("bid2")
    +                        .impid("storedImp")
    +                        .ext(mapper.createObjectNode().set("prebid", mapper.valueToTree(extBidPrebid)))
    +                        .build()))
                     .build());
     
             final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build());
     
             // when
    -        final List result = storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid,
    -                imps);
    +        final List result =
    +                storedResponseProcessor.mergeWithBidderResponses(bidderResponses, seatBid, imps);
     
             // then
    -        assertThat(result).contains(BidderResponse.of("rubicon", BidderSeatBid.of(
    -                asList(BidderBid.of(
    -                        Bid.builder().id("bid2").impid("storedImp").ext(mapper.createObjectNode()
    -                                .set("prebid", mapper.valueToTree(extBidPrebid))).build(), BidType.video, "USD"),
    -                        BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(),
    -                emptyList()), 100));
    +        assertThat(result).contains(BidderResponse.of(
    +                "rubicon",
    +                BidderSeatBid.of(
    +                        asList(BidderBid.of(
    +                                Bid.builder()
    +                                        .id("bid2")
    +                                        .impid("storedImp")
    +                                        .ext(mapper.createObjectNode().set("prebid", mapper.valueToTree(extBidPrebid)))
    +                                        .build(),
    +                                BidType.video,
    +                                "USD"),
    +                                BidderBid.of(
    +                                        Bid.builder()
    +                                                .id("bid1")
    +                                                .build(),
    +                                        BidType.banner,
    +                                        "USD")),
    +                        emptyList(),
    +                        emptyList()),
    +                100));
         }
     
         @Test
    @@ -613,8 +566,13 @@ public void mergeWithBidderResponsesShouldThrowPrebidExceptionWhenExtBidPrebidIn
             final ObjectNode extBidPrebid = mapper.createObjectNode().put("type", "invalid");
     
             final List seatBid = singletonList(SeatBid.builder()
    -                .seat("rubicon").bid(singletonList(Bid.builder().ext(mapper.createObjectNode()
    -                        .set("prebid", extBidPrebid)).id("bid2").impid("storedImp").build())).build());
    +                .seat("rubicon")
    +                .bid(singletonList(Bid.builder()
    +                        .id("bid2")
    +                        .impid("storedImp")
    +                        .ext(mapper.createObjectNode().set("prebid", extBidPrebid))
    +                        .build()))
    +                .build());
     
             final List imps = singletonList(Imp.builder().id("storedImp").banner(Banner.builder().build()).build());
     
    @@ -626,19 +584,48 @@ public void mergeWithBidderResponsesShouldThrowPrebidExceptionWhenExtBidPrebidIn
         @Test
         public void mergeWithBidderResponsesShouldReturnSameResponseWhenThereAreNoStoredResponses() {
             // given
    -        final List bidderResponses = singletonList(BidderResponse.of("rubicon", BidderSeatBid.of(
    -                singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(),
    -                emptyList()), 100));
    +        final List bidderResponses = singletonList(BidderResponse.of(
    +                "rubicon",
    +                BidderSeatBid.of(
    +                        singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")),
    +                        emptyList(),
    +                        emptyList()),
    +                100));
     
             final List imps = singletonList(Imp.builder().banner(Banner.builder().build()).build());
     
             // when
    -        final List result = storedResponseProcessor.mergeWithBidderResponses(bidderResponses,
    -                emptyList(), imps);
    +        final List result =
    +                storedResponseProcessor.mergeWithBidderResponses(bidderResponses, emptyList(), imps);
     
             // then
    -        assertThat(result).containsOnly(BidderResponse.of("rubicon", BidderSeatBid.of(
    -                singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")), emptyList(),
    -                emptyList()), 100));
    +        assertThat(result).containsOnly(BidderResponse.of(
    +                "rubicon",
    +                BidderSeatBid.of(
    +                        singletonList(BidderBid.of(Bid.builder().id("bid1").build(), BidType.banner, "USD")),
    +                        emptyList(),
    +                        emptyList()),
    +                100));
    +    }
    +
    +    private  Map doubleMap(K key1, V value1, K key2, V value2) {
    +        final Map map = new HashMap<>();
    +        map.put(key1, value1);
    +        map.put(key2, value2);
    +        return map;
    +    }
    +
    +    private Imp givenImp(String impId,
    +                         ExtStoredAuctionResponse storedAuctionResponse,
    +                         List extStoredBidResponse) {
    +        return Imp.builder()
    +                .id(impId)
    +                .ext(mapper.valueToTree(ExtImp.of(
    +                        ExtImpPrebid.builder()
    +                                .storedAuctionResponse(storedAuctionResponse)
    +                                .storedBidResponse(extStoredBidResponse)
    +                                .build(),
    +                        null)))
    +                .build();
         }
     }
    diff --git a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java
    index 48860242eb9..d09a16198e6 100644
    --- a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java
    +++ b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java
    @@ -1,13 +1,12 @@
     package org.prebid.server.auction;
     
    -import org.apache.commons.lang3.StringUtils;
    +import com.iab.openrtb.response.Bid;
     import org.junit.Rule;
     import org.junit.Test;
     import org.mockito.junit.MockitoJUnit;
     import org.mockito.junit.MockitoRule;
     import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange;
     import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity;
    -import org.prebid.server.proto.response.Bid;
     
     import java.math.BigDecimal;
     import java.util.Map;
    @@ -27,10 +26,10 @@ public class TargetingKeywordsCreatorTest {
         public final MockitoRule mockitoRule = MockitoJUnit.rule();
     
         @Test
    -    public void shouldReturnTargetingKeywordsForOrdinaryBid() {
    +    public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() {
             // given
    -        final Bid bid = Bid.builder().bidder("bidder1").price(BigDecimal.ONE).dealId("dealId1").cacheId("cacheId1")
    -                .width(50).height(100).build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE)
    +                .dealid("dealId1").w(50).h(100).build();
     
             // when
             final Map keywords = TargetingKeywordsCreator.create(
    @@ -40,25 +39,25 @@ public void shouldReturnTargetingKeywordsForOrdinaryBid() {
                     true,
                     true,
                     false,
    +                false,
                     0,
                     null,
                     null,
                     null)
    -                .makeFor(bid, false);
    +                .makeFor(bid, "bidder1", false, null, null, null);
     
             // then
             assertThat(keywords).containsOnly(
                     entry("hb_pb_bidder1", "1.00"),
                     entry("hb_bidder_bidder1", "bidder1"),
    -                entry("hb_cache_id_bidder1", "cacheId1"),
                     entry("hb_size_bidder1", "50x100"),
                     entry("hb_deal_bidder1", "dealId1"));
         }
     
         @Test
    -    public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() {
    +    public void shouldReturnTargetingKeywordsWithEntireKeysOpenrtb() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE)
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE)
                     .dealid("dealId1").w(50).h(100).build();
     
             // when
    @@ -69,25 +68,30 @@ public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() {
                     true,
                     true,
                     false,
    +                false,
                     0,
                     null,
                     null,
                     null)
    -                .makeFor(bid, "bidder1", false, null, null);
    +                .makeFor(bid, "veryververyverylongbidder1", false, null, null, null);
     
             // then
             assertThat(keywords).containsOnly(
    -                entry("hb_pb_bidder1", "1.00"),
    -                entry("hb_bidder_bidder1", "bidder1"),
    -                entry("hb_size_bidder1", "50x100"),
    -                entry("hb_deal_bidder1", "dealId1"));
    +                entry("hb_pb_veryververyverylongbidder1", "1.00"),
    +                entry("hb_bidder_veryververyverylongbidder1", "veryververyverylongbidder1"),
    +                entry("hb_size_veryververyverylongbidder1", "50x100"),
    +                entry("hb_deal_veryververyverylongbidder1", "dealId1"));
         }
     
         @Test
    -    public void shouldReturnTargetingKeywordsWithEntireKeys() {
    +    public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() {
             // given
    -        final Bid bid = Bid.builder().bidder("veryververyverylongbidder1").price(BigDecimal.ONE).dealId("dealId1")
    -                .cacheId("cacheId1").width(50).height(100).build();
    +        final Bid bid = Bid.builder()
    +                .price(BigDecimal.ONE)
    +                .dealid("dealId1")
    +                .w(50)
    +                .h(100)
    +                .build();
     
             // when
             final Map keywords = TargetingKeywordsCreator.create(
    @@ -96,27 +100,37 @@ public void shouldReturnTargetingKeywordsWithEntireKeys() {
                             singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))),
                     true,
                     true,
    +                true,
                     false,
                     0,
                     null,
                     null,
                     null)
    -                .makeFor(bid, false);
    +                .makeFor(bid, "bidder1", true, "cacheId1", "banner", "videoCacheId1");
     
             // then
             assertThat(keywords).containsOnly(
    -                entry("hb_pb_veryververyverylongbidder1", "1.00"),
    -                entry("hb_bidder_veryververyverylongbidder1", "veryververyverylongbidder1"),
    -                entry("hb_cache_id_veryververyverylongbidder1", "cacheId1"),
    -                entry("hb_size_veryververyverylongbidder1", "50x100"),
    -                entry("hb_deal_veryververyverylongbidder1", "dealId1"));
    +                entry("hb_pb_bidder1", "1.00"),
    +                entry("hb_bidder_bidder1", "bidder1"),
    +                entry("hb_size_bidder1", "50x100"),
    +                entry("hb_deal_bidder1", "dealId1"),
    +                entry("hb_pb", "1.00"),
    +                entry("hb_bidder", "bidder1"),
    +                entry("hb_size", "50x100"),
    +                entry("hb_deal", "dealId1"),
    +                entry("hb_cache_id", "cacheId1"),
    +                entry("hb_cache_id_bidder1", "cacheId1"),
    +                entry("hb_uuid", "videoCacheId1"),
    +                entry("hb_uuid_bidder1", "videoCacheId1"),
    +                entry("hb_format", "banner"),
    +                entry("hb_format_bidder1", "banner"));
         }
     
         @Test
    -    public void shouldReturnTargetingKeywordsWithEntireKeysOpenrtb() {
    +    public void shouldIncludeFormatOpenrtb() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE)
    -                .dealid("dealId1").w(50).h(100).build();
    +        final Bid bid = Bid.builder()
    +                .price(BigDecimal.valueOf(3.87)).build();
     
             // when
             final Map keywords = TargetingKeywordsCreator.create(
    @@ -125,32 +139,22 @@ public void shouldReturnTargetingKeywordsWithEntireKeysOpenrtb() {
                             singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))),
                     true,
                     true,
    +                true,
                     false,
                     0,
                     null,
                     null,
                     null)
    -                .makeFor(bid, "veryververyverylongbidder1", false, null, null);
    +                .makeFor(bid, "", true, null, "banner", null);
     
             // then
    -        assertThat(keywords).containsOnly(
    -                entry("hb_pb_veryververyverylongbidder1", "1.00"),
    -                entry("hb_bidder_veryververyverylongbidder1", "veryververyverylongbidder1"),
    -                entry("hb_size_veryververyverylongbidder1", "50x100"),
    -                entry("hb_deal_veryververyverylongbidder1", "dealId1"));
    +        assertThat(keywords).contains(entry("hb_format", "banner"));
         }
     
         @Test
    -    public void shouldReturnTargetingKeywordsForWinningBid() {
    +    public void shouldNotIncludeCacheIdAndDealIdAndSizeOpenrtb() {
             // given
    -        final Bid bid = Bid.builder()
    -                .bidder("bidder1")
    -                .price(BigDecimal.ONE)
    -                .dealId("dealId1")
    -                .cacheId("cacheId1")
    -                .width(50)
    -                .height(100)
    -                .build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).build();
     
             // when
             final Map keywords = TargetingKeywordsCreator.create(
    @@ -160,35 +164,22 @@ public void shouldReturnTargetingKeywordsForWinningBid() {
                     true,
                     true,
                     false,
    +                false,
                     0,
                     null,
                     null,
                     null)
    -                .makeFor(bid, true);
    +                .makeFor(bid, "bidder", true, null, null, null);
     
             // then
    -        assertThat(keywords).containsOnly(
    -                entry("hb_pb_bidder1", "1.00"),
    -                entry("hb_bidder_bidder1", "bidder1"),
    -                entry("hb_cache_id_bidder1", "cacheId1"),
    -                entry("hb_size_bidder1", "50x100"),
    -                entry("hb_deal_bidder1", "dealId1"),
    -                entry("hb_pb", "1.00"),
    -                entry("hb_bidder", "bidder1"),
    -                entry("hb_cache_id", "cacheId1"),
    -                entry("hb_size", "50x100"),
    -                entry("hb_deal", "dealId1"));
    +        assertThat(keywords).doesNotContainKeys("hb_cache_id_bidder", "hb_deal_bidder", "hb_size_bidder",
    +                "hb_cache_id", "hb_uuid", "hb_deal", "hb_size");
         }
     
         @Test
    -    public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() {
    +    public void shouldReturnEnvKeyForAppRequestOpenrtb() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder()
    -                .price(BigDecimal.ONE)
    -                .dealid("dealId1")
    -                .w(50)
    -                .h(100)
    -                .build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).build();
     
             // when
             final Map keywords = TargetingKeywordsCreator.create(
    @@ -198,120 +189,12 @@ public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() {
                     true,
                     true,
                     false,
    +                true,
                     0,
                     null,
                     null,
                     null)
    -                .makeFor(bid, "bidder1", true, "cacheId1", "videoCacheId1");
    -
    -        // then
    -        assertThat(keywords).containsOnly(
    -                entry("hb_pb_bidder1", "1.00"),
    -                entry("hb_bidder_bidder1", "bidder1"),
    -                entry("hb_size_bidder1", "50x100"),
    -                entry("hb_deal_bidder1", "dealId1"),
    -                entry("hb_pb", "1.00"),
    -                entry("hb_bidder", "bidder1"),
    -                entry("hb_size", "50x100"),
    -                entry("hb_deal", "dealId1"),
    -                entry("hb_cache_id", "cacheId1"),
    -                entry("hb_cache_id_bidder1", "cacheId1"),
    -                entry("hb_uuid", "videoCacheId1"),
    -                entry("hb_uuid_bidder1", "videoCacheId1"));
    -    }
    -
    -    @Test
    -    public void shouldFallbackToDefaultPriceIfInvalidPriceGranularity() {
    -        // given
    -        final Bid bid = Bid.builder().bidder("").price(BigDecimal.valueOf(3.87)).build();
    -
    -        // when
    -        final Map keywords = TargetingKeywordsCreator.create(
    -                "invalid", true, true, false, 0)
    -                .makeFor(bid, true);
    -
    -        // then
    -        assertThat(keywords).contains(entry("hb_pb", StringUtils.EMPTY));
    -    }
    -
    -    @Test
    -    public void shouldUseDefaultPriceGranularity() {
    -        // given
    -        final Bid bid = Bid.builder().bidder("").price(BigDecimal.valueOf(3.87)).build();
    -
    -        // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, 0)
    -                .makeFor(bid, true);
    -
    -        // then
    -        assertThat(keywords).contains(entry("hb_pb", "3.80"));
    -    }
    -
    -    @Test
    -    public void shouldUseDefaultPriceGranularityOpenrtb() {
    -        // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder()
    -                .price(BigDecimal.valueOf(3.87)).build();
    -
    -        // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, 0)
    -                .makeFor(bid, "", true, null, null);
    -
    -        // then
    -        assertThat(keywords).contains(entry("hb_pb", "3.80"));
    -    }
    -
    -    @Test
    -    public void shouldNotIncludeCacheIdAndDealIdAndSize() {
    -        // given
    -        final Bid bid = Bid.builder().bidder("bidder").price(BigDecimal.ONE).build();
    -
    -        // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, 0)
    -                .makeFor(bid, true);
    -
    -        // then
    -        assertThat(keywords).doesNotContainKeys("hb_cache_id_bidder", "hb_deal_bidder", "hb_size_bidder",
    -                "hb_cache_id", "hb_deal", "hb_size");
    -    }
    -
    -    @Test
    -    public void shouldNotIncludeCacheIdAndDealIdAndSizeOpenrtb() {
    -        // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build();
    -
    -        // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, 0)
    -                .makeFor(bid, "bidder", true, null, null);
    -
    -        // then
    -        assertThat(keywords).doesNotContainKeys("hb_cache_id_bidder", "hb_deal_bidder", "hb_size_bidder",
    -                "hb_cache_id", "hb_uuid", "hb_deal", "hb_size");
    -    }
    -
    -    @Test
    -    public void shouldReturnEnvKeyForAppRequest() {
    -        // given
    -        final Bid bid = Bid.builder().bidder("bidder").price(BigDecimal.ONE).build();
    -
    -        // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, true, true, true, 0)
    -                .makeFor(bid, true);
    -
    -        // then
    -        assertThat(keywords).contains(
    -                entry("hb_env", "mobile-app"),
    -                entry("hb_env_bidder", "mobile-app"));
    -    }
    -
    -    @Test
    -    public void shouldReturnEnvKeyForAppRequestOpenrtb() {
    -        // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build();
    -
    -        // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, true, true, true, 0)
    -                .makeFor(bid, "bidder", true, null, null);
    +                .makeFor(bid, "bidder", true, null, null, null);
     
             // then
             assertThat(keywords).contains(
    @@ -322,11 +205,22 @@ public void shouldReturnEnvKeyForAppRequestOpenrtb() {
         @Test
         public void shouldNotIncludeWinningBidTargetingIfIncludeWinnersFlagIsFalse() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).build();
     
             // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, 0)
    -                .makeFor(bid, "bidder1", true, null, null);
    +        final Map keywords = TargetingKeywordsCreator.create(
    +                ExtPriceGranularity.of(
    +                        2,
    +                        singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))),
    +                false,
    +                true,
    +                false,
    +                false,
    +                0,
    +                null,
    +                null,
    +                null)
    +                .makeFor(bid, "bidder1", true, null, null, null);
     
             // then
             assertThat(keywords).doesNotContainKeys("hb_bidder", "hb_pb");
    @@ -335,11 +229,22 @@ public void shouldNotIncludeWinningBidTargetingIfIncludeWinnersFlagIsFalse() {
         @Test
         public void shouldIncludeWinningBidTargetingIfIncludeWinnersFlagIsTrue() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).build();
     
             // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, true, true, false, 0)
    -                .makeFor(bid, "bidder1", true, null, null);
    +        final Map keywords = TargetingKeywordsCreator.create(
    +                ExtPriceGranularity.of(
    +                        2,
    +                        singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))),
    +                true,
    +                true,
    +                false,
    +                false,
    +                0,
    +                null,
    +                null,
    +                null)
    +                .makeFor(bid, "bidder1", true, null, null, null);
     
             // then
             assertThat(keywords).containsKeys("hb_bidder", "hb_pb");
    @@ -348,11 +253,22 @@ public void shouldIncludeWinningBidTargetingIfIncludeWinnersFlagIsTrue() {
         @Test
         public void shouldNotIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).build();
     
             // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, false, false, false, 0)
    -                .makeFor(bid, "bidder1", true, null, null);
    +        final Map keywords = TargetingKeywordsCreator.create(
    +                ExtPriceGranularity.of(
    +                        2,
    +                        singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))),
    +                false,
    +                false,
    +                false,
    +                false,
    +                0,
    +                null,
    +                null,
    +                null)
    +                .makeFor(bid, "bidder1", true, null, null, null);
     
             // then
             assertThat(keywords).doesNotContainKeys("hb_bidder_bidder1", "hb_pb_bidder1");
    @@ -361,11 +277,22 @@ public void shouldNotIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse()
         @Test
         public void shouldIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsTrue() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).build();
     
             // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, 0)
    -                .makeFor(bid, "bidder1", true, null, null);
    +        final Map keywords = TargetingKeywordsCreator.create(
    +                ExtPriceGranularity.of(
    +                        2,
    +                        singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))),
    +                false,
    +                true,
    +                false,
    +                false,
    +                0,
    +                null,
    +                null,
    +                null)
    +                .makeFor(bid, "bidder1", true, null, null, null);
     
             // then
             assertThat(keywords).containsKeys("hb_bidder_bidder1", "hb_pb_bidder1");
    @@ -374,11 +301,22 @@ public void shouldIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsTrue() {
         @Test
         public void shouldTruncateTargetingBidderKeywordsIfTruncateAttrCharsIsDefined() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).build();
     
             // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, 20)
    -                .makeFor(bid, "someVeryLongBidderName", true, null, null);
    +        final Map keywords = TargetingKeywordsCreator.create(
    +                ExtPriceGranularity.of(
    +                        2,
    +                        singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))),
    +                false,
    +                true,
    +                false,
    +                false,
    +                20,
    +                null,
    +                null,
    +                null)
    +                .makeFor(bid, "someVeryLongBidderName", true, null, null, null);
     
             // then
             assertThat(keywords).hasSize(2)
    @@ -388,25 +326,73 @@ public void shouldTruncateTargetingBidderKeywordsIfTruncateAttrCharsIsDefined()
         @Test
         public void shouldTruncateTargetingWithoutBidderSuffixKeywordsIfTruncateAttrCharsIsDefined() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).build();
     
             // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, true, false, false, 7)
    -                .makeFor(bid, "bidder", true, null, null);
    +        final Map keywords = TargetingKeywordsCreator.create(
    +                ExtPriceGranularity.of(
    +                        2,
    +                        singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))),
    +                true,
    +                false,
    +                false,
    +                false,
    +                7,
    +                null,
    +                null,
    +                null)
    +                .makeFor(bid, "bidder", true, null, null, null);
     
             // then
             assertThat(keywords).hasSize(2)
                     .containsKeys("hb_bidd", "hb_pb");
         }
     
    +    @Test
    +    public void shouldTruncateTargetingAndDropDuplicatedWhenTruncateIsTooShort() {
    +        // given
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).build();
    +
    +        // when
    +        final Map keywords = TargetingKeywordsCreator.create(
    +                ExtPriceGranularity.of(
    +                        2,
    +                        singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))),
    +                true,
    +                true,
    +                true,
    +                true,
    +                6,
    +                null,
    +                null,
    +                null)
    +                .makeFor(bid, "bidder", true, null, null, null);
    +
    +        // then
    +        // Without truncating: "hb_bidder", "hb_bidder_bidder", "hb_env", "hb_env_bidder", "hb_pb", "hb_pb_bidder"
    +        assertThat(keywords).hasSize(4)
    +                .containsKeys("hb_bid", "hb_env", "hb_pb", "hb_pb_");
    +    }
    +
         @Test
         public void shouldNotTruncateTargetingKeywordsIfTruncateAttrCharsIsNotDefined() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder().price(BigDecimal.ONE).build();
    +        final Bid bid = Bid.builder().price(BigDecimal.ONE).build();
     
             // when
    -        final Map keywords = TargetingKeywordsCreator.create(null, false, true, false, 0)
    -                .makeFor(bid, "someVeryLongBidderName", true, null, null);
    +        final Map keywords = TargetingKeywordsCreator.create(
    +                ExtPriceGranularity.of(
    +                        2,
    +                        singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))),
    +                false,
    +                true,
    +                false,
    +                false,
    +                0,
    +                null,
    +                null,
    +                null)
    +                .makeFor(bid, "someVeryLongBidderName", true, null, null, null);
     
             // then
             assertThat(keywords).hasSize(2)
    @@ -416,7 +402,7 @@ public void shouldNotTruncateTargetingKeywordsIfTruncateAttrCharsIsNotDefined()
         @Test
         public void shouldTruncateKeysFromResolver() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder()
    +        final Bid bid = Bid.builder()
                     .id("bid1")
                     .price(BigDecimal.ONE)
                     .build();
    @@ -432,11 +418,12 @@ public void shouldTruncateKeysFromResolver() {
                     true,
                     true,
                     false,
    +                false,
                     20,
                     null,
                     null,
                     resolver)
    -                .makeFor(bid, "bidder1", true, null, null);
    +                .makeFor(bid, "bidder1", true, null, null, null);
     
             // then
             assertThat(keywords).contains(entry("key_longer_than_twen", "value1"));
    @@ -445,7 +432,7 @@ public void shouldTruncateKeysFromResolver() {
         @Test
         public void shouldIncludeKeywordsFromResolver() {
             // given
    -        final com.iab.openrtb.response.Bid bid = com.iab.openrtb.response.Bid.builder()
    +        final Bid bid = Bid.builder()
                     .id("bid1")
                     .price(BigDecimal.ONE)
                     .build();
    @@ -461,11 +448,12 @@ public void shouldIncludeKeywordsFromResolver() {
                     true,
                     true,
                     false,
    +                false,
                     0,
                     null,
                     null,
                     resolver)
    -                .makeFor(bid, "bidder1", true, null, null);
    +                .makeFor(bid, "bidder1", true, null, null, null);
     
             // then
             assertThat(keywords).contains(entry("keyword1", "value1"));
    diff --git a/src/test/java/org/prebid/server/auction/VideoRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/VideoRequestFactoryTest.java
    deleted file mode 100644
    index 53fbbefba83..00000000000
    --- a/src/test/java/org/prebid/server/auction/VideoRequestFactoryTest.java
    +++ /dev/null
    @@ -1,269 +0,0 @@
    -package org.prebid.server.auction;
    -
    -import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.Content;
    -import com.iab.openrtb.request.Device;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Site;
    -import com.iab.openrtb.request.User;
    -import com.iab.openrtb.request.Video;
    -import com.iab.openrtb.request.video.BidRequestVideo;
    -import com.iab.openrtb.request.video.PodError;
    -import io.vertx.core.Future;
    -import io.vertx.core.buffer.Buffer;
    -import io.vertx.core.http.HttpServerRequest;
    -import io.vertx.ext.web.RoutingContext;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AuctionContext;
    -import org.prebid.server.auction.model.WithPodErrors;
    -import org.prebid.server.exception.InvalidRequestException;
    -import org.prebid.server.metric.MetricName;
    -import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheVastxml;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
    -import org.prebid.server.util.HttpUtil;
    -
    -import java.util.ArrayList;
    -import java.util.Arrays;
    -
    -import static java.util.Collections.emptySet;
    -import static java.util.Collections.singletonList;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.mockito.ArgumentMatchers.any;
    -import static org.mockito.ArgumentMatchers.anyList;
    -import static org.mockito.ArgumentMatchers.anyLong;
    -import static org.mockito.ArgumentMatchers.anyString;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -import static org.mockito.Mockito.verify;
    -
    -public class VideoRequestFactoryTest extends VertxTest {
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private VideoStoredRequestProcessor videoStoredRequestProcessor;
    -
    -    @Mock
    -    private AuctionRequestFactory auctionRequestFactory;
    -
    -    private VideoRequestFactory factory;
    -
    -    @Mock
    -    private RoutingContext routingContext;
    -    @Mock
    -    private HttpServerRequest httpServerRequest;
    -    @Mock
    -    private TimeoutResolver timeoutResolver;
    -
    -    @Before
    -    public void setUp() {
    -        given(routingContext.request()).willReturn(httpServerRequest);
    -        given(httpServerRequest.getParam(anyString())).willReturn("test");
    -
    -        factory = new VideoRequestFactory(
    -                Integer.MAX_VALUE,
    -                false,
    -                videoStoredRequestProcessor,
    -                auctionRequestFactory,
    -                timeoutResolver,
    -                jacksonMapper);
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfRequestBodyIsMissing() {
    -        // given
    -        given(routingContext.getBody()).willReturn(null);
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause())
    -                .isInstanceOf(InvalidRequestException.class)
    -                .hasMessage("Incoming request has no body");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfStoredRequestIsEnforcedAndIdIsNotProvided() throws JsonProcessingException {
    -        // given
    -        given(routingContext.getBody())
    -                .willReturn(Buffer.buffer(mapper.writeValueAsBytes(BidRequestVideo.builder().build())));
    -        given(routingContext.request().getHeader(HttpUtil.USER_AGENT_HEADER)).willReturn("123");
    -        factory = new VideoRequestFactory(
    -                Integer.MAX_VALUE,
    -                true,
    -                videoStoredRequestProcessor,
    -                auctionRequestFactory,
    -                timeoutResolver,
    -                jacksonMapper);
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause())
    -                .isInstanceOf(InvalidRequestException.class)
    -                .hasMessage("Unable to find required stored request id");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() {
    -        // given
    -        factory = new VideoRequestFactory(
    -                2,
    -                true,
    -                videoStoredRequestProcessor,
    -                auctionRequestFactory,
    -                timeoutResolver,
    -                jacksonMapper);
    -
    -        given(routingContext.getBody()).willReturn(Buffer.buffer("body"));
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause())
    -                .isInstanceOf(InvalidRequestException.class)
    -                .hasMessage("Request size exceeded max size of 2 bytes.");
    -    }
    -
    -    @Test
    -    public void shouldReturnFailedFutureIfRequestBodyCouldNotBeParsed() {
    -        // given
    -        given(routingContext.getBody()).willReturn(Buffer.buffer("body"));
    -
    -        // when
    -        final Future future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    -        assertThat(((InvalidRequestException) future.cause()).getMessages()).hasSize(1)
    -                .element(0).asString().startsWith("Failed to decode:");
    -    }
    -
    -    @Test
    -    public void shouldReturnExpectedResultAndReturnErrors() throws JsonProcessingException {
    -        // given
    -        final Content content = Content.builder()
    -                .len(900)
    -                .livestream(0)
    -                .build();
    -        final Imp expectedImp1 = Imp.builder()
    -                .id("123_0")
    -                .video(Video.builder()
    -                        .mimes(singletonList("mime"))
    -                        .maxduration(100)
    -                        .protocols(singletonList(123))
    -                        .build())
    -                .build();
    -        final Imp expectedImp2 = Imp.builder()
    -                .id("123_1")
    -                .video(Video.builder()
    -                        .mimes(singletonList("mime"))
    -                        .maxduration(100)
    -                        .protocols(singletonList(123))
    -                        .build())
    -                .build();
    -        final ExtRequestPrebid ext = ExtRequestPrebid.builder()
    -                .cache(ExtRequestPrebidCache.of(null, ExtRequestPrebidCacheVastxml.of(null, null), null))
    -                .targeting(ExtRequestTargeting.builder()
    -                        .pricegranularity(mapper.valueToTree(PriceGranularity.createFromString("med")))
    -                        .includebidderkeys(true)
    -                        .includebrandcategory(ExtIncludeBrandCategory.of(null, null, false))
    -                        .build())
    -                .build();
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .id("bid_id")
    -                .imp(Arrays.asList(expectedImp1, expectedImp2))
    -                .user(User.builder().buyeruid("appnexus").yob(123).gender("gender").keywords("keywords").build())
    -                .site(Site.builder().id("siteId").content(content).build())
    -                .bcat(singletonList("bcat"))
    -                .badv(singletonList("badv"))
    -                .cur(singletonList("USD"))
    -                .tmax(0L)
    -                .ext(ExtRequest.of(ext))
    -                .build();
    -
    -        final WithPodErrors mergedBidRequest = WithPodErrors.of(
    -                bidRequest, singletonList(PodError.of(1, 1, singletonList("TEST"))));
    -
    -        final BidRequestVideo requestVideo = BidRequestVideo.builder().device(
    -                Device.builder().ua("123").build()).build();
    -        given(routingContext.getBody()).willReturn(Buffer.buffer(mapper.writeValueAsBytes(requestVideo)));
    -        given(videoStoredRequestProcessor.processVideoRequest(any(), any(), any()))
    -                .willReturn(Future.succeededFuture(mergedBidRequest));
    -        given(auctionRequestFactory.validateRequest(any())).willAnswer(invocation -> invocation.getArgument(0));
    -        given(auctionRequestFactory.fillImplicitParameters(any(), any(), any()))
    -                .willAnswer(invocation -> invocation.getArgument(0));
    -        given(auctionRequestFactory.toAuctionContext(any(), any(), any(), anyList(), anyLong(), any()))
    -                .willReturn(Future.succeededFuture());
    -
    -        // when
    -        final Future> result = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        verify(routingContext).getBody();
    -        verify(videoStoredRequestProcessor).processVideoRequest(null, emptySet(), requestVideo);
    -        verify(auctionRequestFactory).validateRequest(bidRequest);
    -        verify(auctionRequestFactory).fillImplicitParameters(bidRequest, routingContext, timeoutResolver);
    -        verify(auctionRequestFactory).toAuctionContext(
    -                routingContext, bidRequest, MetricName.video, new ArrayList<>(), 0, timeoutResolver);
    -
    -        assertThat(result.result().getPodErrors()).isEqualTo(mergedBidRequest.getPodErrors());
    -    }
    -
    -    @Test
    -    public void shouldReplaceDeviceUaWithUserAgentHeaderIfPresented() throws JsonProcessingException {
    -        // given
    -        final BidRequestVideo requestVideo = BidRequestVideo.builder().build();
    -        given(routingContext.getBody()).willReturn(Buffer.buffer(mapper.writeValueAsBytes(requestVideo)));
    -        given(routingContext.request().getHeader(HttpUtil.USER_AGENT_HEADER)).willReturn("user-agent-123");
    -
    -        final WithPodErrors emptyMergeObject = WithPodErrors.of(null, null);
    -        given(videoStoredRequestProcessor.processVideoRequest(any(), any(), any()))
    -                .willReturn(Future.succeededFuture(emptyMergeObject));
    -
    -        // when
    -        factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        verify(videoStoredRequestProcessor).processVideoRequest(any(), any(), eq(BidRequestVideo.builder()
    -                .device(Device.builder()
    -                        .ua("user-agent-123")
    -                        .build())
    -                .build()));
    -    }
    -
    -    @Test
    -    public void shouldReturnErrorIfDeviceUaAndUserAgentHeaderIsEmpty() throws JsonProcessingException {
    -        // given
    -        final BidRequestVideo requestVideo = BidRequestVideo.builder().build();
    -        given(routingContext.getBody()).willReturn(Buffer.buffer(mapper.writeValueAsBytes(requestVideo)));
    -
    -        // when
    -        Future> future = factory.fromRequest(routingContext, 0L);
    -
    -        // then
    -        assertThat(future.failed()).isTrue();
    -        assertThat(future.cause())
    -                .isInstanceOf(InvalidRequestException.class)
    -                .hasMessage("Device.UA and User-Agent Header is not presented");
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/auction/VideoResponseFactoryTest.java b/src/test/java/org/prebid/server/auction/VideoResponseFactoryTest.java
    index 6916a5b9e10..9adef19da48 100644
    --- a/src/test/java/org/prebid/server/auction/VideoResponseFactoryTest.java
    +++ b/src/test/java/org/prebid/server/auction/VideoResponseFactoryTest.java
    @@ -1,6 +1,5 @@
     package org.prebid.server.auction;
     
    -import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.video.PodError;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
    @@ -8,10 +7,18 @@
     import org.junit.Before;
     import org.junit.Test;
     import org.prebid.server.VertxTest;
    +import org.prebid.server.auction.model.AuctionContext;
    +import org.prebid.server.auction.model.DebugContext;
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.response.ExtAdPod;
     import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
    +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse;
    +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModules;
    +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace;
     import org.prebid.server.proto.openrtb.ext.response.ExtResponseVideoTargeting;
    +import org.prebid.server.proto.response.ExtAmpVideoPrebid;
    +import org.prebid.server.proto.response.ExtAmpVideoResponse;
     import org.prebid.server.proto.response.VideoResponse;
     
     import java.util.Arrays;
    @@ -19,7 +26,9 @@
     import java.util.List;
     import java.util.Map;
     
    +import static java.util.Collections.emptyList;
     import static java.util.Collections.singletonList;
    +import static java.util.Collections.singletonMap;
     import static org.assertj.core.api.Assertions.assertThat;
     
     public class VideoResponseFactoryTest extends VertxTest {
    @@ -35,9 +44,9 @@ public void setUp() {
         public void shouldReturnExpectedVideoResponse() {
             // given
             final Map targeting = new HashMap<>();
    -        targeting.put("hb_uuid", "value1");
    -        targeting.put("hb_pb", "hb_pb");
    -        targeting.put("hb_pb_cat_dur", "hb_pb_cat_dur");
    +        targeting.put("hb_uuid_appnexus", "hb_uuidVal");
    +        targeting.put("hb_pb_appnexus", "hb_pbVal");
    +        targeting.put("hb_pb_cat_dur_appnexus", "hb_pb_cat_durVal");
     
             final Bid bid0 = Bid.builder()
                     .impid("0_0")
    @@ -58,30 +67,48 @@ public void shouldReturnExpectedVideoResponse() {
                                     mapper.createObjectNode())))
                     .build();
     
    +        final ExtBidResponse extResponse = ExtBidResponse.builder()
    +                .prebid(ExtBidResponsePrebid.of(
    +                        1000L,
    +                        ExtModules.of(
    +                                singletonMap(
    +                                        "module1", singletonMap("hook1", singletonList("error1"))),
    +                                singletonMap(
    +                                        "module1", singletonMap("hook1", singletonList("warning1"))),
    +                                ExtModulesTrace.of(2L, emptyList()))))
    +                .build();
    +
             final BidResponse bidResponse = BidResponse.builder()
                     .seatbid(singletonList(SeatBid.builder()
                             .seat("bidder1")
                             .bid(Arrays.asList(bid0, bid1, bid2))
                             .build()))
    +                .ext(extResponse)
                     .build();
     
             final PodError podError = PodError.of(3, 1, singletonList("Error"));
     
             // when
    -        final VideoResponse result = target.toVideoResponse(BidRequest.builder().build(), bidResponse,
    +        final VideoResponse result = target.toVideoResponse(
    +                AuctionContext.builder()
    +                        .debugContext(DebugContext.empty())
    +                        .build(),
    +                bidResponse,
                     singletonList(podError));
     
             // then
             final ExtAdPod expectedExtAdPod0 = ExtAdPod.of(0,
    -                singletonList(ExtResponseVideoTargeting.of("hb_pb", "hb_pb_cat_dur", "value1")), null);
    +                singletonList(ExtResponseVideoTargeting.of("hb_pbVal", "hb_pb_cat_durVal", "hb_uuidVal")), null);
             final ExtAdPod expectedExtAdPod1 = ExtAdPod.of(
    -                1, singletonList(ExtResponseVideoTargeting.of("hb_pb", "hb_pb_cat_dur", "value1")), null);
    +                1, singletonList(ExtResponseVideoTargeting.of("hb_pbVal", "hb_pb_cat_durVal", "hb_uuidVal")), null);
             final ExtAdPod expectedErrorExtAdPod3 = ExtAdPod.of(3, null, singletonList("Error"));
             final List expectedAdPodResponse = Arrays.asList(expectedExtAdPod0, expectedExtAdPod1,
                     expectedErrorExtAdPod3);
     
    -        final VideoResponse videoResponse = VideoResponse.of(expectedAdPodResponse, null, null, null);
    -
    -        assertThat(result).isEqualTo(videoResponse);
    +        assertThat(result).isEqualTo(VideoResponse.of(
    +                expectedAdPodResponse,
    +                null,
    +                null,
    +                ExtAmpVideoResponse.of(ExtAmpVideoPrebid.of(extResponse.getPrebid().getModules()))));
         }
     }
    diff --git a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java
    index 4c64e691d21..2d64aed7408 100644
    --- a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java
    +++ b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java
    @@ -1,5 +1,6 @@
     package org.prebid.server.auction;
     
    +import com.fasterxml.jackson.core.JsonProcessingException;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.Content;
     import com.iab.openrtb.request.Imp;
    @@ -12,6 +13,8 @@
     import com.iab.openrtb.request.video.PodError;
     import com.iab.openrtb.request.video.Podconfig;
     import io.vertx.core.Future;
    +import io.vertx.core.buffer.Buffer;
    +import io.vertx.core.file.FileSystem;
     import org.junit.Before;
     import org.junit.Rule;
     import org.junit.Test;
    @@ -22,8 +25,11 @@
     import org.prebid.server.auction.model.WithPodErrors;
     import org.prebid.server.exception.InvalidRequestException;
     import org.prebid.server.execution.TimeoutFactory;
    +import org.prebid.server.json.JsonMerger;
     import org.prebid.server.metric.Metrics;
     import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory;
    +import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange;
    +import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache;
    @@ -33,9 +39,11 @@
     import org.prebid.server.settings.model.StoredDataResult;
     import org.prebid.server.validation.VideoRequestValidator;
     
    +import java.math.BigDecimal;
     import java.util.Arrays;
     import java.util.function.UnaryOperator;
     
    +import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
     import static java.util.Collections.emptyMap;
     import static java.util.Collections.emptySet;
    @@ -45,6 +53,8 @@
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.mockito.ArgumentMatchers.any;
     import static org.mockito.ArgumentMatchers.anyBoolean;
    +import static org.mockito.ArgumentMatchers.anySet;
    +import static org.mockito.ArgumentMatchers.anyString;
     import static org.mockito.ArgumentMatchers.eq;
     import static org.mockito.BDDMockito.given;
     import static org.mockito.Mockito.never;
    @@ -54,9 +64,12 @@ public class VideoStoredRequestProcessorTest extends VertxTest {
     
         private static final String STORED_REQUEST_ID = "storedReqId";
         private static final String STORED_POD_ID = "storedPodId";
    +
         @Rule
         public final MockitoRule mockitoRule = MockitoJUnit.rule();
     
    +    @Mock
    +    private FileSystem fileSystem;
         @Mock
         private ApplicationSettings applicationSettings;
         @Mock
    @@ -71,18 +84,34 @@ public class VideoStoredRequestProcessorTest extends VertxTest {
         private VideoStoredRequestProcessor target;
     
         @Before
    -    public void setUp() {
    -        target = new VideoStoredRequestProcessor(applicationSettings, validator, false, emptyList(),
    -                BidRequest.builder().build(), metrics, timeoutFactory, timeoutResolver, 2000L, "USD", jacksonMapper);
    +    public void setUp() throws JsonProcessingException {
    +        given(fileSystem.readFileBlocking(anyString()))
    +                .willReturn(Buffer.buffer(mapper.writeValueAsString(BidRequest.builder().at(1).build())));
    +
    +        target = VideoStoredRequestProcessor.create(
    +                false,
    +                emptyList(),
    +                2000L,
    +                "USD",
    +                "path/to/default/request.json",
    +                fileSystem,
    +                applicationSettings,
    +                validator,
    +                metrics,
    +                timeoutFactory,
    +                timeoutResolver,
    +                jacksonMapper,
    +                new JsonMerger(jacksonMapper));
         }
     
         @Test
         public void shouldReturnFailedFutureWhenFetchStoredIsFailed() {
             // given
    -        given(applicationSettings.getVideoStoredData(any(), any(), any())).willReturn(Future.failedFuture("ERROR"));
    +        given(applicationSettings.getVideoStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.failedFuture("ERROR"));
     
             // when
    -        final Future> result = target.processVideoRequest(STORED_REQUEST_ID,
    +        final Future> result = target.processVideoRequest(null, STORED_REQUEST_ID,
                     singleton(STORED_POD_ID), null);
     
             // then
    @@ -103,7 +132,7 @@ public void shouldReturnFutureWithMergedStoredAndDefaultRequest() {
                     .livestream(0)
                     .build();
     
    -        final BidRequestVideo storedVideo = givenValidDataResult(requestBuilder -> requestBuilder
    +        final BidRequestVideo storedVideo = givenValidDataResult(builder -> builder
                             .user(user)
                             .content(content)
                             .cacheconfig(CacheConfig.of(42))
    @@ -113,25 +142,26 @@ public void shouldReturnFutureWithMergedStoredAndDefaultRequest() {
     
             final BidRequestVideo requestVideo = givenValidDataResult(
                     UnaryOperator.identity(),
    -                podconfigBuilder -> podconfigBuilder.pods(singletonList(Pod.of(123, 20, STORED_POD_ID))));
    +                builder -> builder.pods(singletonList(Pod.of(123, 20, STORED_POD_ID))));
     
             final StoredDataResult storedDataResult = StoredDataResult.of(
                     singletonMap(STORED_REQUEST_ID, jacksonMapper.encode(storedVideo)),
                     singletonMap(STORED_POD_ID, "{}"),
                     emptyList());
     
    -        given(applicationSettings.getVideoStoredData(any(), any(), any())).willReturn(
    -                Future.succeededFuture(storedDataResult));
    -        given(validator.validPods(any(), any())).willReturn(
    -                WithPodErrors.of(singletonList(Pod.of(123, 20, STORED_POD_ID)), emptyList()));
    +        given(applicationSettings.getVideoStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.succeededFuture(storedDataResult));
    +        given(validator.validPods(any(), any()))
    +                .willReturn(WithPodErrors.of(singletonList(Pod.of(123, 20, STORED_POD_ID)), emptyList()));
     
             // when
    -        final Future> result = target.processVideoRequest(STORED_REQUEST_ID,
    +        final Future> result = target.processVideoRequest(null, STORED_REQUEST_ID,
                     singleton(STORED_POD_ID), requestVideo);
     
             // then
    -        verify(applicationSettings).getVideoStoredData(eq(singleton(STORED_REQUEST_ID)), eq(singleton(STORED_POD_ID)),
    -                any());
    +        verify(applicationSettings).getVideoStoredData(any(), eq(singleton(STORED_REQUEST_ID)),
    +                eq(singleton(STORED_POD_ID)), any());
    +
             verify(metrics).updateStoredRequestMetric(true);
             verify(metrics).updateStoredImpsMetric(true);
     
    @@ -140,14 +170,21 @@ public void shouldReturnFutureWithMergedStoredAndDefaultRequest() {
     
             final Imp expectedImp1 = Imp.builder()
                     .id("123_0")
    -                .video(Video.builder().mimes(singletonList("mime")).maxduration(100).protocols(
    -                        singletonList(123)).build())
    +                .video(Video.builder()
    +                        .mimes(singletonList("mime"))
    +                        .maxduration(200)
    +                        .protocols(singletonList(123))
    +                        .build())
                     .build();
             final Imp expectedImp2 = Imp.builder()
                     .id("123_1")
    -                .video(Video.builder().mimes(singletonList("mime")).maxduration(100).protocols(
    -                        singletonList(123)).build())
    +                .video(Video.builder()
    +                        .mimes(singletonList("mime"))
    +                        .maxduration(200)
    +                        .protocols(singletonList(123))
    +                        .build())
                     .build();
    +
             final ExtRequestPrebid ext = ExtRequestPrebid.builder()
                     .cache(ExtRequestPrebidCache.of(null, ExtRequestPrebidCacheVastxml.of(null, null), null))
                     .targeting(ExtRequestTargeting.builder()
    @@ -156,10 +193,12 @@ public void shouldReturnFutureWithMergedStoredAndDefaultRequest() {
                             .includebrandcategory(ExtIncludeBrandCategory.of(null, null, false))
                             .build())
                     .build();
    +
             final BidRequest expectedMergedRequest = BidRequest.builder()
                     .id("bid_id")
    -                .imp(Arrays.asList(expectedImp1, expectedImp2))
    -                .user(User.builder().buyeruid("appnexus").yob(123).gender("gender").keywords("keywords").build())
    +                .at(1)
    +                .imp(asList(expectedImp1, expectedImp2))
    +                .user(User.builder().buyeruid("value").yob(123).gender("gender").keywords("keywords").build())
                     .site(Site.builder().id("siteId").content(content).build())
                     .bcat(singletonList("bcat"))
                     .badv(singletonList("badv"))
    @@ -178,21 +217,21 @@ public void processVideoRequestShouldFailWhenThereAreNoStoredImpsFound() {
             final Pod pod2 = Pod.of(2, 3, "222");
             final BidRequestVideo requestVideo = givenValidDataResult(
                     UnaryOperator.identity(),
    -                podconfigBuilder -> podconfigBuilder.pods(Arrays.asList(pod1, pod2)));
    +                builder -> builder.pods(asList(pod1, pod2)));
     
             final StoredDataResult storedDataResult = StoredDataResult.of(emptyMap(), emptyMap(), emptyList());
     
    -        given(applicationSettings.getVideoStoredData(any(), any(), any())).willReturn(
    -                Future.succeededFuture(storedDataResult));
    +        given(applicationSettings.getVideoStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.succeededFuture(storedDataResult));
     
             final PodError podError1 = PodError.of(1, 1, singletonList("ERROR1"));
             final PodError podError2 = PodError.of(2, 2, singletonList("ERROR2"));
     
    -        given(validator.validPods(any(), any())).willReturn(
    -                WithPodErrors.of(emptyList(), Arrays.asList(podError1, podError2)));
    +        given(validator.validPods(any(), any()))
    +                .willReturn(WithPodErrors.of(emptyList(), asList(podError1, podError2)));
     
             // when
    -        final Future> result = target.processVideoRequest(STORED_REQUEST_ID,
    +        final Future> result = target.processVideoRequest(null, STORED_REQUEST_ID,
                     singleton(STORED_POD_ID), requestVideo);
     
             // then
    @@ -202,8 +241,8 @@ public void processVideoRequestShouldFailWhenThereAreNoStoredImpsFound() {
             verify(validator).validateStoredBidRequest(any(), anyBoolean(), any());
     
             final Podconfig expectedPodConfig = Podconfig.builder()
    -                .durationRangeSec(Arrays.asList(200, 100))
    -                .pods(Arrays.asList(pod1, pod2))
    +                .durationRangeSec(asList(200, 100))
    +                .pods(asList(pod1, pod2))
                     .build();
             verify(validator).validPods(eq(expectedPodConfig), eq(emptySet()));
     
    @@ -213,13 +252,128 @@ public void processVideoRequestShouldFailWhenThereAreNoStoredImpsFound() {
                     .hasMessage("Stored request fetching failed: all pods are incorrect:  ERROR1; ERROR2");
         }
     
    +    @Test
    +    public void shouldReturnFutureWithCorrectAdPodDurationIfRequireExactDurationIsTrue() {
    +        // given
    +        final BidRequestVideo storedVideo = givenValidDataResult(builder -> builder
    +                        .cacheconfig(CacheConfig.of(42)),
    +                UnaryOperator.identity());
    +
    +        final BidRequestVideo requestVideo = givenValidDataResult(
    +                UnaryOperator.identity(),
    +                builder -> builder.requireExactDuration(true)
    +                        .durationRangeSec(Arrays.asList(30, 60, 80))
    +                        .pods(singletonList(Pod.of(123, 30, STORED_POD_ID))));
    +
    +        final StoredDataResult storedDataResult = StoredDataResult.of(
    +                singletonMap(STORED_REQUEST_ID, jacksonMapper.encode(storedVideo)),
    +                singletonMap(STORED_POD_ID, "{}"),
    +                emptyList());
    +
    +        given(applicationSettings.getVideoStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.succeededFuture(storedDataResult));
    +        given(validator.validPods(any(), any()))
    +                .willReturn(WithPodErrors.of(singletonList(Pod.of(123, 20, STORED_POD_ID)), emptyList()));
    +
    +        // when
    +        final Future> result = target.processVideoRequest(null, STORED_REQUEST_ID,
    +                singleton(STORED_POD_ID), requestVideo);
    +
    +        // then
    +        verify(applicationSettings).getVideoStoredData(any(), eq(singleton(STORED_REQUEST_ID)),
    +                eq(singleton(STORED_POD_ID)), any());
    +
    +        verify(metrics).updateStoredRequestMetric(true);
    +        verify(metrics).updateStoredImpsMetric(true);
    +
    +        verify(validator).validateStoredBidRequest(any(), anyBoolean(), any());
    +        verify(validator).validPods(any(), eq(singleton(STORED_POD_ID)));
    +
    +        final Imp expectedImp1 = Imp.builder()
    +                .id("123_0")
    +                .video(Video.builder()
    +                        .mimes(singletonList("mime"))
    +                        .minduration(30)
    +                        .maxduration(30)
    +                        .protocols(singletonList(123))
    +                        .build())
    +                .build();
    +        final Imp expectedImp2 = Imp.builder()
    +                .id("123_1")
    +                .video(Video.builder()
    +                        .mimes(singletonList("mime"))
    +                        .minduration(60)
    +                        .maxduration(60)
    +                        .protocols(singletonList(123))
    +                        .build())
    +                .build();
    +        final Imp expectedImp3 = Imp.builder()
    +                .id("123_2")
    +                .video(Video.builder()
    +                        .mimes(singletonList("mime"))
    +                        .minduration(80)
    +                        .maxduration(80)
    +                        .protocols(singletonList(123))
    +                        .build())
    +                .build();
    +
    +        assertThat(result.result().getData().getImp())
    +                .isEqualTo(Arrays.asList(expectedImp1, expectedImp2, expectedImp3));
    +    }
    +
    +    @Test
    +    public void shouldReturnFutureWithCorrectPriceGranularityInRequest() {
    +        // given
    +        final BidRequestVideo storedVideo = givenValidDataResult(builder -> builder
    +                        .cacheconfig(CacheConfig.of(42))
    +                        .bcat(singletonList("bcat"))
    +                        .badv(singletonList("badv")),
    +                UnaryOperator.identity());
    +        final PriceGranularity priceGranularity = PriceGranularity.createFromExtPriceGranularity(
    +                ExtPriceGranularity.of(1,
    +                        singletonList(ExtGranularityRange.of(new BigDecimal(10), new BigDecimal("0.5"))))
    +        );
    +
    +        final BidRequestVideo requestVideo = givenValidDataResult(
    +                bidRequestVideoBuilder -> bidRequestVideoBuilder.priceGranularity(priceGranularity),
    +                builder -> builder.pods(singletonList(Pod.of(123, 20, STORED_POD_ID))));
    +
    +        final StoredDataResult storedDataResult = StoredDataResult.of(
    +                singletonMap(STORED_REQUEST_ID, jacksonMapper.encode(storedVideo)),
    +                singletonMap(STORED_POD_ID, "{}"),
    +                emptyList());
    +
    +        given(applicationSettings.getVideoStoredData(any(), anySet(), anySet(), any()))
    +                .willReturn(Future.succeededFuture(storedDataResult));
    +        given(validator.validPods(any(), any()))
    +                .willReturn(WithPodErrors.of(singletonList(Pod.of(123, 20, STORED_POD_ID)), emptyList()));
    +
    +        // when
    +        final Future> result = target.processVideoRequest(null, STORED_REQUEST_ID,
    +                singleton(STORED_POD_ID), requestVideo);
    +
    +        // then
    +        verify(applicationSettings).getVideoStoredData(any(), eq(singleton(STORED_REQUEST_ID)),
    +                eq(singleton(STORED_POD_ID)), any());
    +
    +        verify(metrics).updateStoredRequestMetric(true);
    +        verify(metrics).updateStoredImpsMetric(true);
    +
    +        verify(validator).validateStoredBidRequest(any(), anyBoolean(), any());
    +        verify(validator).validPods(any(), eq(singleton(STORED_POD_ID)));
    +
    +        assertThat(result.result().getData().getExt().getPrebid().getTargeting().getPricegranularity())
    +                .isEqualTo(mapper.valueToTree(priceGranularity));
    +    }
    +
         private BidRequestVideo givenValidDataResult(
                 UnaryOperator requestCustomizer,
                 UnaryOperator podconfigCustomizer) {
    +
             return requestCustomizer.apply(BidRequestVideo.builder()
                     .storedrequestid("storedrequestid")
                     .podconfig(podconfigCustomizer.apply(Podconfig.builder()
    -                        .durationRangeSec(Arrays.asList(200, 100)))
    +                        .durationRangeSec(asList(200, 100)))
                             .build())
                     .site(Site.builder().id("siteId").build())
                     .video(Video.builder().mimes(singletonList("mime")).protocols(singletonList(123)).build()))
    diff --git a/src/test/java/org/prebid/server/auction/WinningBidComparatorFactoryTest.java b/src/test/java/org/prebid/server/auction/WinningBidComparatorFactoryTest.java
    new file mode 100644
    index 00000000000..170722e68ae
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/auction/WinningBidComparatorFactoryTest.java
    @@ -0,0 +1,397 @@
    +package org.prebid.server.auction;
    +
    +import com.iab.openrtb.request.Deal;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Pmp;
    +import com.iab.openrtb.response.Bid;
    +import org.junit.Test;
    +import org.prebid.server.auction.model.BidInfo;
    +
    +import java.math.BigDecimal;
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.stream.Collectors;
    +
    +import static java.util.Collections.emptyList;
    +import static java.util.Collections.singletonList;
    +import static org.assertj.core.api.Java6Assertions.assertThat;
    +
    +public class WinningBidComparatorFactoryTest {
    +
    +    private static final String IMP_ID = "impId";
    +    private final WinningBidComparatorFactory winningBidComparatorFactory = new WinningBidComparatorFactory();
    +
    +    @Test
    +    public void preferDealsComparatorCompareShouldReturnMoreThanZeroForNonDealsWhenFirstHasHigherPrice() {
    +        // given
    +        final BidInfo higherPriceBidInfo = givenBidInfo(5.0f);
    +        final BidInfo lowerPriceBidInfo = givenBidInfo(1.0f);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(higherPriceBidInfo, lowerPriceBidInfo);
    +
    +        // then
    +        assertThat(result).isGreaterThan(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorCompareShouldReturnLessThanZeroForNonDealsWhenFirstHasLowerPrice() {
    +        // given
    +        final BidInfo lowerPriceBidInfo = givenBidInfo(1.0f);
    +        final BidInfo higherPriceBidInfo = givenBidInfo(5.0f);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(lowerPriceBidInfo, higherPriceBidInfo);
    +
    +        // then
    +        assertThat(result).isLessThan(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorCompareShouldReturnZeroWhenPriceAreEqualForNonDealsBids() {
    +        // given
    +        final BidInfo bidInfo1 = givenBidInfo(5.0f);
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isEqualTo(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorCompareShouldReturnMoreThanZeroWhenFirstHasNonPgDeal() {
    +        // given
    +        final BidInfo dealPriceBidInfo = givenBidInfo(5.0f, "dealId", emptyList());
    +        final BidInfo higherPriceBidInfo = givenBidInfo(10.0f, null, emptyList());
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(dealPriceBidInfo, higherPriceBidInfo);
    +
    +        // then
    +        assertThat(result).isGreaterThan(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorCompareShouldReturnLessThanZeroWhenFirstHasNoDeal() {
    +        // given
    +        final BidInfo higherPriceBidInfo = givenBidInfo(10.0f, null, emptyList());
    +        final BidInfo dealPriceBidInfo = givenBidInfo(5.0f, "dealId", emptyList());
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(higherPriceBidInfo, dealPriceBidInfo);
    +
    +        // then
    +        assertThat(result).isLessThan(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorCompareShouldReturnZeroWhenBothHaveNonPgDeals() {
    +        // given
    +        final BidInfo bidInfo1 = givenBidInfo(5.0f, "dealId", emptyList());
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f, "dealId", emptyList());
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isEqualTo(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorCompareShouldReturnZeroWhenBothHaveSamePgDealIdAndHasSamePrice() {
    +        // given
    +        final List impDeals = singletonList("dealId");
    +        final BidInfo bidInfo1 = givenBidInfo(5.0f, "dealId", impDeals);
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f, "dealId", impDeals);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isEqualTo(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorCompareShouldReturnMoreThanZeroWhenBothHaveSamePgDealIdAndFirstHasHigherPrice() {
    +        // given
    +        final List impDeals = singletonList("dealId");
    +        final BidInfo bidInfo1 = givenBidInfo(10.0f, "dealId", impDeals);
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f, "dealId", impDeals);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isGreaterThan(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorShouldReturnLessThanZeroWhenFirstHasHigherPriceAndSecondHasLessImpPgDealIndex() {
    +        // given
    +        final List impDeals = Arrays.asList("dealId1", "dealId2");
    +        final BidInfo bidInfo1 = givenBidInfo(10.0f, "dealId2", impDeals);
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f, "dealId1", impDeals);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isLessThan(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorShouldReturnLessThanZeroWhenFirstIsNonPgDealWithHigherPriceAndSecondPgDeal() {
    +        // given
    +        final List impDeals = singletonList("dealId1");
    +        final BidInfo bidInfo1 = givenBidInfo(10.0f, "dealId2", impDeals);
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f, "dealId1", impDeals);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isLessThan(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorShouldReturnGreaterThanZeroWhenFirstPgDealAndSecondMonPgDeal() {
    +        // given
    +        final List impDeals = singletonList("dealId2");
    +        final BidInfo bidInfo1 = givenBidInfo(5.0f, "dealId2", impDeals);
    +        final BidInfo bidInfo2 = givenBidInfo(10.0f, "dealId1", impDeals);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(true).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isGreaterThan(0);
    +    }
    +
    +    @Test
    +    public void preferDealsComparatorSortShouldReturnExpectedSortedResultWithDeals() {
    +        // given
    +        final String dealId1 = "pgDealId1";
    +        final String dealId2 = "pgDealId2";
    +        final List impDeals = Arrays.asList(dealId1, dealId2);
    +
    +        final BidInfo bidInfo1 = givenBidInfo(1.0f, dealId1, impDeals); // pg deal with lower price
    +        final BidInfo bidInfo2 = givenBidInfo(2.0f, dealId1, impDeals); // pg deal with middle price
    +        final BidInfo bidInfo3 = givenBidInfo(4.1f, dealId2, impDeals); // pg deal with higher price
    +        final BidInfo bidInfo4 = givenBidInfo(5.0f, null, impDeals); // non deal with lower price
    +        final BidInfo bidInfo5 = givenBidInfo(100.1f, null, impDeals); // non deal with higher price
    +        final BidInfo bidInfo6 = givenBidInfo(0.5f, "dealId1", impDeals); // non pg deal with lower price
    +        final BidInfo bidInfo7 = givenBidInfo(1f, "dealId2", impDeals); // non pg deal with middle price
    +        final BidInfo bidInfo8 = givenBidInfo(4.4f, "dealId3", impDeals); // non pg deal with higher price
    +
    +        final List bidInfos = Arrays.asList(bidInfo5, bidInfo3, bidInfo1, bidInfo2, bidInfo1, bidInfo4,
    +                bidInfo6, bidInfo7, bidInfo8);
    +
    +        // when
    +        bidInfos.sort(winningBidComparatorFactory.create(true));
    +
    +        // then
    +        assertThat(bidInfos).containsOnly(bidInfo4, bidInfo5, bidInfo6, bidInfo7, bidInfo8, bidInfo3, bidInfo2,
    +                bidInfo1, bidInfo1);
    +    }
    +
    +    @Test
    +    public void priceComparatorCompareShouldReturnMoreThatZeroWhenFirstHasHigherPriceForNonDealsBids() {
    +        // given
    +        final BidInfo higherPriceBidInfo = givenBidInfo(5.0f);
    +        final BidInfo lowerPriceBidInfo = givenBidInfo(1.0f);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(higherPriceBidInfo, lowerPriceBidInfo);
    +
    +        // then
    +        assertThat(result).isGreaterThan(0);
    +    }
    +
    +    @Test
    +    public void priceComparatorCompareShouldReturnLessThatZeroWhenFirstHasLowerPriceForNonDealsBids() {
    +        // given
    +        final BidInfo lowerPriceBidInfo = givenBidInfo(1.0f);
    +        final BidInfo higherPriceBidInfo = givenBidInfo(5.0f);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(lowerPriceBidInfo, higherPriceBidInfo);
    +
    +        // then
    +        assertThat(result).isLessThan(0);
    +    }
    +
    +    @Test
    +    public void priceComparatorCompareShouldReturnZeroWhenPriceAreEqualForNonDealsBids() {
    +        // given
    +        final BidInfo bidInfo1 = givenBidInfo(5.0f);
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isEqualTo(0);
    +    }
    +
    +    @Test
    +    public void preferPriceComparatorCompareShouldReturnLessThanZeroWhenFirstHasNonPgDeal() {
    +        // given
    +        final BidInfo dealPriceBidInfo = givenBidInfo(5.0f, "dealId", emptyList());
    +        final BidInfo higherPriceBidInfo = givenBidInfo(10.0f, null, emptyList());
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(dealPriceBidInfo, higherPriceBidInfo);
    +
    +        // then
    +        assertThat(result).isLessThan(0);
    +    }
    +
    +    @Test
    +    public void preferPriceComparatorCompareShouldReturnGreaterThanZeroWhenFirstHasNoDeal() {
    +        // given
    +        final BidInfo higherPriceBidInfo = givenBidInfo(10.0f, null, emptyList());
    +        final BidInfo dealPriceBidInfo = givenBidInfo(5.0f, "dealId", emptyList());
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(higherPriceBidInfo, dealPriceBidInfo);
    +
    +        // then
    +        assertThat(result).isGreaterThan(0);
    +    }
    +
    +    @Test
    +    public void preferPriceComparatorCompareShouldReturnZeroWhenBothHaveNonPgDeals() {
    +        // given
    +        final BidInfo bidInfo1 = givenBidInfo(5.0f, "dealId", emptyList());
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f, "dealId", emptyList());
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isEqualTo(0);
    +    }
    +
    +    @Test
    +    public void preferPriceComparatorCompareShouldReturnZeroWhenBothHaveSamePgDealIdAndHasSamePrice() {
    +        // given
    +        final List impDeals = singletonList("dealId");
    +        final BidInfo bidInfo1 = givenBidInfo(5.0f, "dealId", impDeals);
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f, "dealId", impDeals);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isEqualTo(0);
    +    }
    +
    +    @Test
    +    public void preferPriceComparatorCompareShouldReturnMoreThanZeroWhenBothHaveSamePgDealIdAndFirstHasHigherPrice() {
    +        // given
    +        final List impDeals = singletonList("dealId");
    +        final BidInfo bidInfo1 = givenBidInfo(10.0f, "dealId", impDeals);
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f, "dealId", impDeals);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isGreaterThan(0);
    +    }
    +
    +    @Test
    +    public void preferPriceComparatorShouldReturnLessThanZeroWhenFirstHasHigherPriceAndSecondHasLessImpPgDealIndex() {
    +        // given
    +        final List impDeals = Arrays.asList("dealId1", "dealId2");
    +        final BidInfo bidInfo1 = givenBidInfo(10.0f, "dealId2", impDeals);
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f, "dealId1", impDeals);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isLessThan(0);
    +    }
    +
    +    @Test
    +    public void preferPriceComparatorShouldReturnLessThanZeroWhenFirstIsNonPgDealWithHigherPriceAndSecondPgDeal() {
    +        // given
    +        final List impDeals = singletonList("dealId1");
    +        final BidInfo bidInfo1 = givenBidInfo(10.0f, "dealId2", impDeals);
    +        final BidInfo bidInfo2 = givenBidInfo(5.0f, "dealId1", impDeals);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isLessThan(0);
    +    }
    +
    +    @Test
    +    public void preferPriceComparatorShouldReturnGreaterThanZeroWhenFirstPgDealAndSecondMonPgDeal() {
    +        // given
    +        final List impDeals = singletonList("dealId2");
    +        final BidInfo bidInfo1 = givenBidInfo(5.0f, "dealId2", impDeals);
    +        final BidInfo bidInfo2 = givenBidInfo(10.0f, "dealId1", impDeals);
    +
    +        // when
    +        final int result = winningBidComparatorFactory.create(false).compare(bidInfo1, bidInfo2);
    +
    +        // then
    +        assertThat(result).isGreaterThan(0);
    +    }
    +
    +    @Test
    +    public void preferPriceComparatorSortShouldReturnExpectedSortedResultWithDeals() {
    +        // given
    +        final String dealId1 = "pgDealId1";
    +        final String dealId2 = "pgDealId2";
    +        final List impDeals = Arrays.asList(dealId1, dealId2);
    +
    +        final BidInfo bidInfo1 = givenBidInfo(1.0f, dealId1, impDeals); // pg deal with lower price
    +        final BidInfo bidInfo2 = givenBidInfo(2.0f, dealId1, impDeals); // pg deal with middle price
    +        final BidInfo bidInfo3 = givenBidInfo(4.1f, dealId2, impDeals); // pg deal with higher price
    +        final BidInfo bidInfo4 = givenBidInfo(5.0f, null, impDeals); // non deal with lower price
    +        final BidInfo bidInfo5 = givenBidInfo(100.1f, null, impDeals); // non deal with higher price
    +        final BidInfo bidInfo6 = givenBidInfo(0.5f, "dealId1", impDeals); // non pg deal with lower price
    +        final BidInfo bidInfo7 = givenBidInfo(1f, "dealId2", impDeals); // non pg deal with middle price
    +        final BidInfo bidInfo8 = givenBidInfo(4.4f, "dealId3", impDeals); // non pg deal with higher price
    +
    +        final List bidInfos = Arrays.asList(bidInfo5, bidInfo3, bidInfo1, bidInfo2, bidInfo1, bidInfo4,
    +                bidInfo6, bidInfo7, bidInfo8);
    +
    +        // when
    +        bidInfos.sort(winningBidComparatorFactory.create(false));
    +
    +        // then
    +        assertThat(bidInfos).containsOnly(bidInfo6, bidInfo7, bidInfo8, bidInfo4, bidInfo5, bidInfo1,
    +                bidInfo1, bidInfo2, bidInfo3);
    +    }
    +
    +    private static BidInfo givenBidInfo(float price) {
    +        return givenBidInfo(price, null);
    +    }
    +
    +    private static BidInfo givenBidInfo(float price, String dealId) {
    +        return BidInfo.builder()
    +                .correspondingImp(Imp.builder().build())
    +                .bid(Bid.builder().impid(IMP_ID).price(BigDecimal.valueOf(price)).dealid(dealId).build())
    +                .correspondingImp(Imp.builder().id(IMP_ID).build())
    +                .build();
    +    }
    +
    +    private static BidInfo givenBidInfo(float price, String dealId, List impDealIds) {
    +        final List impDeals = impDealIds.stream()
    +                .map(impDealId -> Deal.builder().id(impDealId).build())
    +                .collect(Collectors.toList());
    +        final Pmp pmp = Pmp.builder().deals(impDeals).build();
    +
    +        return BidInfo.builder()
    +                .bid(Bid.builder().impid(IMP_ID).price(BigDecimal.valueOf(price)).dealid(dealId).build())
    +                .correspondingImp(Imp.builder().id(IMP_ID).pmp(pmp).build())
    +                .build();
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java
    new file mode 100644
    index 00000000000..d588076a8b8
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java
    @@ -0,0 +1,1705 @@
    +package org.prebid.server.auction.requestfactory;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.App;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Publisher;
    +import com.iab.openrtb.request.Regs;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.request.User;
    +import io.vertx.core.Future;
    +import io.vertx.core.MultiMap;
    +import io.vertx.core.http.HttpServerRequest;
    +import io.vertx.core.net.impl.SocketAddressImpl;
    +import io.vertx.ext.web.RoutingContext;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
    +import org.mockito.stubbing.Answer;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.auction.FpdResolver;
    +import org.prebid.server.auction.ImplicitParametersExtractor;
    +import org.prebid.server.auction.OrtbTypesResolver;
    +import org.prebid.server.auction.PrivacyEnforcementService;
    +import org.prebid.server.auction.StoredRequestProcessor;
    +import org.prebid.server.auction.TimeoutResolver;
    +import org.prebid.server.auction.model.AuctionContext;
    +import org.prebid.server.exception.InvalidRequestException;
    +import org.prebid.server.geolocation.model.GeoInfo;
    +import org.prebid.server.metric.MetricName;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
    +import org.prebid.server.model.Endpoint;
    +import org.prebid.server.model.HttpRequestContext;
    +import org.prebid.server.privacy.ccpa.Ccpa;
    +import org.prebid.server.privacy.gdpr.model.TcfContext;
    +import org.prebid.server.privacy.model.Privacy;
    +import org.prebid.server.privacy.model.PrivacyContext;
    +import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory;
    +import org.prebid.server.proto.openrtb.ext.request.ConsentedProvidersSettings;
    +import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange;
    +import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAmp;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheBids;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheVastxml;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
    +import org.prebid.server.proto.openrtb.ext.request.ExtSite;
    +import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    +import org.prebid.server.proto.request.Targeting;
    +
    +import java.math.BigDecimal;
    +import java.util.ArrayList;
    +import java.util.Arrays;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static java.util.Collections.singletonMap;
    +import static java.util.function.Function.identity;
    +import static org.apache.commons.lang3.StringUtils.EMPTY;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.tuple;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyList;
    +import static org.mockito.ArgumentMatchers.anyLong;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.ArgumentMatchers.eq;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.doAnswer;
    +import static org.mockito.Mockito.doReturn;
    +import static org.mockito.Mockito.verify;
    +import static org.mockito.Mockito.verifyZeroInteractions;
    +import static org.prebid.server.assertion.FutureAssertion.assertThat;
    +
    +public class AmpRequestFactoryTest extends VertxTest {
    +
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
    +    @Mock
    +    private StoredRequestProcessor storedRequestProcessor;
    +    @Mock
    +    private Ortb2RequestFactory ortb2RequestFactory;
    +    @Mock
    +    private OrtbTypesResolver ortbTypesResolver;
    +    @Mock
    +    private ImplicitParametersExtractor implicitParametersExtractor;
    +    @Mock
    +    private Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver;
    +    @Mock
    +    private FpdResolver fpdResolver;
    +    @Mock
    +    private PrivacyEnforcementService privacyEnforcementService;
    +    @Mock
    +    private TimeoutResolver timeoutResolver;
    +
    +    private AmpRequestFactory target;
    +
    +    @Mock
    +    private HttpServerRequest httpRequest;
    +    @Mock
    +    private RoutingContext routingContext;
    +
    +    private BidRequest defaultBidRequest;
    +
    +    @Before
    +    public void setUp() {
    +        defaultBidRequest = BidRequest.builder().build();
    +
    +        given(timeoutResolver.resolve(any())).willReturn(2000L);
    +        given(timeoutResolver.adjustTimeout(anyLong())).willReturn(1900L);
    +
    +        given(routingContext.request()).willReturn(httpRequest);
    +        given(routingContext.queryParams()).willReturn(
    +                MultiMap.caseInsensitiveMultiMap()
    +                        .add("tag_id", "tagId"));
    +        given(httpRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap());
    +        given(httpRequest.remoteAddress()).willReturn(new SocketAddressImpl(1234, "host"));
    +
    +        given(ortb2RequestFactory.createAuctionContext(any(), eq(MetricName.amp))).willReturn(AuctionContext.builder()
    +                .prebidErrors(new ArrayList<>())
    +                .build());
    +        given(ortb2RequestFactory.executeEntrypointHooks(any(), any(), any()))
    +                .willAnswer(invocation -> toHttpRequest(invocation.getArgument(0), invocation.getArgument(1)));
    +        given(ortb2RequestFactory.restoreResultFromRejection(any()))
    +                .willAnswer(invocation -> Future.failedFuture((Throwable) invocation.getArgument(0)));
    +
    +        given(fpdResolver.resolveApp(any(), any()))
    +                .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
    +        given(fpdResolver.resolveSite(any(), any()))
    +                .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
    +        given(fpdResolver.resolveUser(any(), any()))
    +                .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
    +        given(fpdResolver.resolveImpExt(any(), any())).willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
    +        given(fpdResolver.resolveBidRequestExt(any(), any())).willAnswer(invocationOnMock -> invocationOnMock
    +                .getArgument(0));
    +        given(ortb2RequestFactory.populateDealsInfo(any()))
    +                .willAnswer(invocationOnMock -> Future.succeededFuture(invocationOnMock.getArgument(0)));
    +
    +        final PrivacyContext defaultPrivacyContext = PrivacyContext.of(
    +                Privacy.of("0", EMPTY, Ccpa.EMPTY, 0),
    +                TcfContext.empty());
    +        given(privacyEnforcementService.contextFromBidRequest(any()))
    +                .willReturn(Future.succeededFuture(defaultPrivacyContext));
    +
    +        target = new AmpRequestFactory(
    +                storedRequestProcessor,
    +                ortb2RequestFactory,
    +                ortbTypesResolver,
    +                implicitParametersExtractor,
    +                ortb2ImplicitParametersResolver,
    +                fpdResolver,
    +                privacyEnforcementService,
    +                timeoutResolver,
    +                jacksonMapper);
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfRequestHasNoTagId() {
    +        // given
    +        routingContext.queryParams().set("tag_id", (String) null);
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        verifyZeroInteractions(storedRequestProcessor);
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    +        assertThat(((InvalidRequestException) future.cause()).getMessages())
    +                .hasSize(1).containsOnly("AMP requests require an AMP tag_id");
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfStoredBidRequestHasNoImp() {
    +        // given
    +        givenBidRequest(identity());
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    +        assertThat(((InvalidRequestException) future.cause()).getMessages())
    +                .hasSize(1).containsOnly("data for tag_id='tagId' does not define the required imp array.");
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfStoredBidRequestHasMoreThenOneImp() {
    +        // given
    +        final Imp imp = Imp.builder().build();
    +        givenBidRequest(identity(), imp, imp);
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    +        assertThat(((InvalidRequestException) future.cause()).getMessages())
    +                .hasSize(1).containsOnly("data for tag_id 'tagId' includes 2 imp elements. Only one is allowed");
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfStoredBidRequestHasApp() {
    +        // given
    +        final Imp imp = Imp.builder().build();
    +        givenBidRequest(builder -> builder.app(App.builder().build()), imp);
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    +        assertThat(((InvalidRequestException) future.cause()).getMessages())
    +                .hasSize(1).containsOnly("request.app must not exist in AMP stored requests.");
    +    }
    +
    +    @Test
    +    public void shouldUseQueryParamsModifiedByEntrypointHooks() {
    +        // given
    +        routingContext.queryParams().set("debug", "0");
    +
    +        doAnswer(invocation -> Future.succeededFuture(HttpRequestContext.builder()
    +                .queryParams(CaseInsensitiveMultiMap.builder()
    +                        .add("tag_id", "tagId")
    +                        .add("debug", "1")
    +                        .build())
    +                .build()))
    +                .when(ortb2RequestFactory)
    +                .executeEntrypointHooks(any(), any(), any());
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getDebug)
    +                .containsExactly(1);
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfEntrypointHookRejectedRequest() {
    +        // given
    +        givenBidRequest();
    +
    +        final Throwable exception = new RuntimeException();
    +        doAnswer(invocation -> Future.failedFuture(exception))
    +                .when(ortb2RequestFactory)
    +                .executeEntrypointHooks(any(), any(), any());
    +
    +        final AuctionContext auctionContext = AuctionContext.builder().requestRejected(true).build();
    +        doReturn(Future.succeededFuture(auctionContext))
    +                .when(ortb2RequestFactory)
    +                .restoreResultFromRejection(eq(exception));
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future).succeededWith(auctionContext);
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithDefaultPrebidValuesIfPrebidIsNullInStoredRequest() {
    +        // given
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        // result was wrapped to list because extracting method works different on iterable and not iterable objects,
    +        // which force to make type casting or exception handling in lambdas
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .containsExactly(ExtRequestPrebid.builder()
    +                        .storedrequest(ExtStoredRequest.of("tagId"))
    +                        .amp(ExtRequestPrebidAmp.of(singletonMap("tag_id", "tagId")))
    +                        .targeting(ExtRequestTargeting.builder()
    +                                .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(2,
    +                                        singletonList(ExtGranularityRange.of(BigDecimal.valueOf(20),
    +                                                BigDecimal.valueOf(0.1))))))
    +                                .includewinners(true)
    +                                .includebidderkeys(true)
    +                                .build())
    +                        .cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(null, null),
    +                                ExtRequestPrebidCacheVastxml.of(null, null), null))
    +                        .channel(ExtRequestPrebidChannel.of("amp"))
    +                        .build());
    +    }
    +
    +    @Test
    +    public void shouldCallOrtbTypeResolver() {
    +        // given
    +        givenBidRequest();
    +
    +        // when
    +        target.fromRequest(routingContext, 0L).result();
    +
    +        // then
    +        verify(ortbTypesResolver).normalizeTargeting(any(), anyList(), any());
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithDefaultTargetingIfStoredBidRequestExtHasNoTargeting() {
    +        // given
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getPricegranularity, ExtRequestTargeting::getIncludewinners)
    +                .containsExactly(tuple(
    +                        // default priceGranularity
    +                        mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of(
    +                                BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))),
    +                        // default includeWinners
    +                        true));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithDefaultIncludeWinnersIfStoredBidRequestExtTargetingHasNoIncludeWinners() {
    +        // given
    +        givenBidRequest(
    +                builder -> builder
    +                        .ext(givenRequestExt(
    +                                ExtRequestTargeting.builder()
    +                                        .pricegranularity(mapper.createObjectNode().put("foo", "bar"))
    +                                        .build())),
    +                Imp.builder().build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludewinners)
    +                .containsExactly(true);
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithIncludeWinnersFromStoredBidRequest() {
    +        // given
    +        givenBidRequest(
    +                builder -> builder
    +                        .ext(givenRequestExt(
    +                                ExtRequestTargeting.builder()
    +                                        .pricegranularity(mapper.createObjectNode().put("foo", "bar"))
    +                                        .includewinners(false)
    +                                        .build())),
    +                Imp.builder().build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludewinners)
    +                .containsExactly(false);
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithIncludeFormatFromStoredBidRequest() {
    +        // given
    +        givenBidRequest(
    +                builder -> builder
    +                        .ext(givenRequestExt(
    +                                ExtRequestTargeting.builder()
    +                                        .pricegranularity(mapper.createObjectNode().put("foo", "bar"))
    +                                        .includeformat(true)
    +                                        .build())),
    +                Imp.builder().build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludeformat)
    +                .containsExactly(true);
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithDefaultIncludeBidderKeysIfStoredRequestExtTargetingHasNoIncludeBidderKeys() {
    +        // given
    +        givenBidRequest(
    +                builder -> builder.ext(givenRequestExt(ExtRequestTargeting.builder().includewinners(false).build())),
    +                Imp.builder().build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludewinners, ExtRequestTargeting::getIncludebidderkeys)
    +                // assert that includeBidderKeys was set with default value and includewinners remained unchanged
    +                .containsExactly(tuple(false, true));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithIncludeBidderKeysFromStoredBidRequest() {
    +        // given
    +        givenBidRequest(
    +                builder -> builder
    +                        .ext(givenRequestExt(
    +                                ExtRequestTargeting.builder()
    +                                        .pricegranularity(mapper.createObjectNode().put("foo", "bar"))
    +                                        .includebidderkeys(false)
    +                                        .build())),
    +                Imp.builder().build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludebidderkeys)
    +                .containsExactly(false);
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithDefaultPriceGranularityIfStoredBidRequestExtTargetingHasNoPriceGranularity() {
    +        // given
    +        givenBidRequest(
    +                builder -> builder
    +                        .ext(givenRequestExt(ExtRequestTargeting.builder().includewinners(false).build())),
    +                Imp.builder().build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludewinners, ExtRequestTargeting::getPricegranularity)
    +                // assert that priceGranularity was set with default value and includeWinners remained unchanged
    +                .containsExactly(
    +                        tuple(false, mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(
    +                                ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1)))))));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithNotChangedExtRequestPrebidTargetingFields() {
    +        // given
    +        givenBidRequest(
    +                builder -> builder
    +                        .ext(givenRequestExt(ExtRequestTargeting.builder()
    +                                .includebrandcategory(ExtIncludeBrandCategory.of(1, "publisher", true))
    +                                .truncateattrchars(10)
    +                                .build())),
    +                Imp.builder().build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludebrandcategory, ExtRequestTargeting::getTruncateattrchars)
    +                .containsOnly(tuple(ExtIncludeBrandCategory.of(1, "publisher", true), 10));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithDefaultCachingIfStoredBidRequestExtHasNoCaching() {
    +        // given
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(extBidRequest -> extBidRequest.getPrebid().getCache().getBids())
    +                .containsExactly(ExtRequestPrebidCacheBids.of(null, null));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithChannelIfStoredBidRequestExtHasNoChannel() {
    +        // given
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt)
    +                .extracting(extBidRequest -> extBidRequest.getPrebid().getChannel())
    +                .containsExactly(ExtRequestPrebidChannel.of("amp"));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithChannelFromStoredBidRequest() {
    +        // given
    +        givenBidRequest(
    +                builder -> builder
    +                        .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                                .channel(ExtRequestPrebidChannel.of("custom"))
    +                                .build())),
    +                Imp.builder().build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt)
    +                .extracting(extBidRequest -> extBidRequest.getPrebid().getChannel())
    +                .containsExactly(ExtRequestPrebidChannel.of("custom"));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithImpSecureEqualsToOneIfInitiallyItWasNotSecured() {
    +        // given
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getSecure)
    +                .containsExactly(1);
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithExtPrebidDebugOneWhenDebugQueryParamIsOne() {
    +        // given
    +        routingContext.queryParams().add("debug", "1");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getDebug)
    +                .containsOnly(1);
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithExtPrebidDebugZeroWhenDebugQueryParamIsZero() {
    +        // given
    +        routingContext.queryParams().add("debug", "0");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getDebug)
    +                .containsOnly(0);
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutExtPrebidDebugWhenDebugQueryParamIsNotValid() {
    +        // given
    +        routingContext.queryParams().add("debug", "3");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getDebug)
    +                .containsNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOverriddenTagIdBySlotParamValue() {
    +        // given
    +        routingContext.queryParams().add("slot", "Overridden-tagId");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid)
    +                .containsOnly("Overridden-tagId");
    +    }
    +
    +    @Test
    +    public void shouldSetBidRequestSiteExt() {
    +        // given
    +        routingContext.queryParams().add("curl", "");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getExt)
    +                .containsOnly(ExtSite.of(1, null));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithSitePageAndDomainFromCurlQueryParam() {
    +        // given
    +        routingContext.queryParams().add("curl", "http://overridden.site.page:8080/path");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPage, Site::getDomain, Site::getExt)
    +                .containsOnly(tuple(
    +                        "http://overridden.site.page:8080/path", "overridden.site.page", ExtSite.of(1, null)));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithSitePublisherIdFromAccountQueryParam() {
    +        // given
    +        routingContext.queryParams().add("account", "accountId");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPublisher, Site::getExt)
    +                .containsOnly(tuple(
    +                        Publisher.builder().id("accountId").build(),
    +                        ExtSite.of(1, null)));
    +    }
    +
    +    @Test
    +    public void shouldReturnRequestWithOverriddenBannerFormatByOverwriteWHParamsRespectingThemOverWH() {
    +        // given
    +        routingContext.queryParams().add("w", "10");
    +        routingContext.queryParams().add("ow", "1000");
    +        routingContext.queryParams().add("h", "20");
    +        routingContext.queryParams().add("oh", "2000");
    +        routingContext.queryParams().add("ms", "44x88,66x99");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(1000, 2000), tuple(44, 88), tuple(66, 99));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOverriddenBannerFromOWAndHParamAndMultiListIfOHIsMissed() {
    +        // given
    +        routingContext.queryParams().add("ow", "10");
    +        routingContext.queryParams().add("w", "30");
    +        routingContext.queryParams().add("h", "40");
    +        routingContext.queryParams().add("ms", "50x60");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(10, 40), tuple(50, 60));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOverriddenBannerFromWAndOHParamAndMultiListIfOWIsMissed() {
    +        // given
    +        routingContext.queryParams().add("oh", "20");
    +        routingContext.queryParams().add("w", "30");
    +        routingContext.queryParams().add("h", "40");
    +        routingContext.queryParams().add("ms", "50x60");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(30, 20), tuple(50, 60));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithBannerFromHWParamsAndMultiList() {
    +        // given
    +        routingContext.queryParams().add("w", "30");
    +        routingContext.queryParams().add("h", "40");
    +        routingContext.queryParams().add("ms", "50x60");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(30, 40), tuple(50, 60));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOverriddenBannerFromWAndHParamsIfOwOhAndMultiListAreMissed() {
    +        // given
    +        routingContext.queryParams().add("w", "30");
    +        routingContext.queryParams().add("h", "40");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(30, 40));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithUpdatedWidthForAllBannerFormatsWhenOnlyWIsPresentInParams() {
    +        // given
    +        routingContext.queryParams().add("w", "30");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(asList(Format.builder().w(1).h(2).build(),
    +                                        Format.builder().w(3).h(4).build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(30, 2), tuple(30, 4));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithUpdatedHeightForAllBannerFormatsWhenOnlyHIsPresentInParams() {
    +        // given
    +        routingContext.queryParams().add("h", "40");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(asList(Format.builder().w(1).h(2).build(),
    +                                        Format.builder().w(3).h(4).build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(1, 40), tuple(3, 40));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOverriddenBannerFormatsByMultiSizeParams() {
    +        // given
    +        routingContext.queryParams().add("ms", "44x88,66x99");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(44, 88), tuple(66, 99));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOriginalBannerFormatsWhenMultiSizeParamContainsCompletelyInvalidValue() {
    +        // given
    +        routingContext.queryParams().add("ms", ",");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(1, 2));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOriginBannerFormatsWhenMultiSizeParamContainsAtLeastOneInvalidValue() {
    +        // given
    +        routingContext.queryParams().add("ms", ",33x,44x77,abc,");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(1, 2));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestOverriddenBannerFormatsWhenMsParamSizePairHasOneInvalidValue() {
    +        // given
    +        routingContext.queryParams().add("ms", "900xZ");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(900, 0));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOriginBannerFormatsWhenMultiSizeParamContainsAtLeastOneZeroPairSize() {
    +        // given
    +        routingContext.queryParams().add("ms", "44x77, 0x0");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(1, 2));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOverriddenBannerFormatsWhenMultiSizeParamContainsPartiallyInvalidParams() {
    +        // given
    +        routingContext.queryParams().add("ms", "33x,44x77");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(33, 0), tuple(44, 77));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOriginBannerFormatsWhenAllParametersAreZero() {
    +        // given
    +        routingContext.queryParams().add("ow", "0");
    +        routingContext.queryParams().add("oh", "0");
    +        routingContext.queryParams().add("w", "0");
    +        routingContext.queryParams().add("h", "0");
    +        routingContext.queryParams().add("ms", "0x0");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(1, 2));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithOverriddenBannerWhenInvalidParamTreatedAsZeroValue() {
    +        // given
    +        routingContext.queryParams().add("ow", "100");
    +        routingContext.queryParams().add("oh", "invalid");
    +        routingContext.queryParams().add("h", "200");
    +
    +        givenBidRequest(
    +                identity(),
    +                Imp.builder()
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder()
    +                                        .w(1)
    +                                        .h(2)
    +                                        .build()))
    +                                .build()).build());
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .extracting(Format::getW, Format::getH)
    +                .containsOnly(tuple(100, 200));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithTmaxFromTimeoutQueryParam() {
    +        // given
    +        routingContext.queryParams().add("timeout", "1000");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(request.getTmax()).isEqualTo(1000L);
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutUserWhenGdprConsentQueryParamIsBlank() {
    +        // given
    +        routingContext.queryParams().add("gdpr_consent", "");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getUser()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithUserExtConsentWhenGdprConsentIsValidAndConsentTypeIsNotPresent() {
    +        // given
    +        routingContext.queryParams().add("gdpr_consent", "BONV8oqONXwgmADACHENAO7pqzAAppY");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getUser())
    +                .isEqualTo(User.builder()
    +                        .ext(ExtUser.builder().consent("BONV8oqONXwgmADACHENAO7pqzAAppY").build())
    +                        .build());
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithUserExtConsentWhenGdprConsentIsValidAndConsentTypeIsTCFV2() {
    +        // given
    +        routingContext.queryParams()
    +                .add("gdpr_consent", "BONV8oqONXwgmADACHENAO7pqzAAppY")
    +                .add("consent_type", "2");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getUser())
    +                .isEqualTo(User.builder()
    +                        .ext(ExtUser.builder().consent("BONV8oqONXwgmADACHENAO7pqzAAppY").build())
    +                        .build());
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutUserExtConsentWhenGdprConsentIsValidAndConsentTypeIsTCFV1() {
    +        // given
    +        routingContext.queryParams()
    +                .add("gdpr_consent", "BONV8oqONXwgmADACHENAO7pqzAAppY")
    +                .add("consent_type", "1");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getUser()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutUserExtConsentWhenGdprConsentIsValidAndConsentTypeIsUsPrivacy() {
    +        // given
    +        routingContext.queryParams()
    +                .add("gdpr_consent", "BONV8oqONXwgmADACHENAO7pqzAAppY")
    +                .add("consent_type", "3");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getUser()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutUserExtConsentWhenGdprConsentIsValidAndConsentTypeIsUnknown() {
    +        // given
    +        routingContext.queryParams()
    +                .add("gdpr_consent", "BONV8oqONXwgmADACHENAO7pqzAAppY")
    +                .add("consent_type", "23");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getUser()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithProvidersSettingsContainsAttlConsentIfParamIsPresent() {
    +        // given
    +        routingContext.queryParams()
    +                .add("attl_consent", "someConsent");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getUser())
    +                .isEqualTo(User.builder()
    +                        .ext(ExtUser.builder()
    +                                .consentedProvidersSettings(ConsentedProvidersSettings.of("someConsent"))
    +                                .build())
    +                        .build());
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutProvidersSettingsIfAttlConsentIsMissed() {
    +        // given
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getUser()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutProvidersSettingsIfAttlConsentIsBlank() {
    +        // given
    +        routingContext.queryParams()
    +                .add("attl_consent", "  ");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getUser()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutUserWhenGdprConsentQueryParamIsInvalid() {
    +        // given
    +        routingContext.queryParams().add("gdpr_consent", "consent-value");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getUser()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldAddErrorToAuctionContextWhenGdprConsentQueryParamIsInvalid() {
    +        // given
    +        routingContext.queryParams().add("gdpr_consent", "consent-value");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final AuctionContext result = target.fromRequest(routingContext, 0L).result();
    +
    +        // then
    +        assertThat(result.getPrebidErrors())
    +                .contains("Amp request parameter consent_string or gdpr_consent have invalid format: consent-value");
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithExtPrebidDataBiddersUpdatedByFpdResolver() throws JsonProcessingException {
    +        // given
    +        routingContext.queryParams()
    +                .add("targeting", mapper.writeValueAsString(
    +                        Targeting.of(Arrays.asList("appnexus", "rubicon"), null, null)));
    +
    +        given(fpdResolver.resolveBidRequestExt(any(), any()))
    +                .willReturn(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .data(ExtRequestPrebidData.of(Arrays.asList("appnexus", "rubicon"), null)).build()));
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        verify(fpdResolver).resolveBidRequestExt(any(), any());
    +        assertThat(request)
    +                .extracting(BidRequest::getExt)
    +                .containsOnly(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .data(ExtRequestPrebidData.of(Arrays.asList("appnexus", "rubicon"), null)).build()));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestImpExtContextDataWithTargetingAttributes() throws JsonProcessingException {
    +        // given
    +        routingContext.queryParams()
    +                .add("targeting", mapper.writeValueAsString(
    +                        Targeting.of(Arrays.asList("appnexus", "rubicon"), null, null)));
    +
    +        given(fpdResolver.resolveImpExt(any(), any()))
    +                .willReturn(mapper.createObjectNode().set("context", mapper.createObjectNode()
    +                        .set("data", mapper.createObjectNode().put("attr1", "value1").put("attr2", "value2"))));
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        verify(fpdResolver).resolveBidRequestExt(any(), any());
    +        assertThat(singletonList(request))
    +                .flatExtracting(BidRequest::getImp)
    +                .containsOnly(Imp.builder().secure(1).ext(mapper.createObjectNode().set("context",
    +                        mapper.createObjectNode().set("data", mapper.createObjectNode().put("attr1", "value1")
    +                                .put("attr2", "value2")))).build());
    +    }
    +
    +    @Test
    +    public void shouldThrowInvalidRequestExceptionWhenTargetingHasTypeOtherToObject() {
    +        // given
    +        routingContext.queryParams().add("targeting", "[\"a\"]");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final Future result = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(result.failed()).isTrue();
    +        assertThat(result.cause())
    +                .isInstanceOf(InvalidRequestException.class)
    +                .hasMessage("Error decoding targeting, expected type is `object` but was ARRAY");
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutRegsExtWhenNoPrivacyPolicyIsExist() {
    +        // given
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getRegs()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithRegsContainsGdprEqualOneIfGdprAppliesIsTrue() {
    +        // given
    +        routingContext.queryParams().add("gdpr_applies", "true");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getRegs())
    +                .isEqualTo(Regs.of(null, ExtRegs.of(1, null)));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithRegsContainsGdprEqualZeroIfGdprAppliesIsFalse() {
    +        // given
    +        routingContext.queryParams().add("gdpr_applies", "false");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getRegs())
    +                .isEqualTo(Regs.of(null, ExtRegs.of(0, null)));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutRegsIfGdprAppliesIsNotPresent() {
    +        // given
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getRegs()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithRegsExtUsPrivacyWhenGdprConsentQueryParamIsValidUsPrivacyString() {
    +        // given
    +        routingContext.queryParams().add("gdpr_consent", "1N--");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getRegs())
    +                .isEqualTo(Regs.of(null, ExtRegs.of(null, "1N--")));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithRegsExtUsPrivacyWhenConsentStringIsValidAndConsentTypeIsNotPresent() {
    +        // given
    +        routingContext.queryParams().add("consent_string", "1Y-N");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getRegs())
    +                .isEqualTo(Regs.of(null, ExtRegs.of(null, "1Y-N")));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithRegsExtUsPrivacyWhenConsentStringIsValidAndConsentTypeIsUsPrivacy() {
    +        // given
    +        routingContext.queryParams()
    +                .add("consent_string", "1Y-N")
    +                .add("consent_type", "3");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getRegs())
    +                .isEqualTo(Regs.of(null, ExtRegs.of(null, "1Y-N")));
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutRegsExtUsPrivacyWhenConsentStringIsValidAndConsentTypeIsTcfV1() {
    +        // given
    +        routingContext.queryParams()
    +                .add("consent_string", "1Y-N")
    +                .add("consent_type", "1");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getRegs()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutRegsExtUsPrivacyWhenConsentStringIsValidAndConsentTypeIsTcfV2() {
    +        // given
    +        routingContext.queryParams()
    +                .add("consent_string", "1Y-N")
    +                .add("consent_type", "2");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getRegs()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnBidRequestWithoutRegsExtUsPrivacyWhenConsentStringIsValidAndConsentTypeIsUnknown() {
    +        // given
    +        routingContext.queryParams()
    +                .add("consent_string", "1Y-N")
    +                .add("consent_type", "23");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        assertThat(result.getRegs()).isNull();
    +    }
    +
    +    @SuppressWarnings("unchecked")
    +    @Test
    +    public void shouldReturnBidRequestWithCreatedExtPrebidAmpData() {
    +        // given
    +        routingContext.queryParams()
    +                .add("queryParam1", "value1")
    +                .add("queryParam2", "value2");
    +
    +        givenBidRequest();
    +
    +        // when
    +        final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
    +
    +        // then
    +        final Map expectedAmpData = new HashMap<>();
    +        expectedAmpData.put("queryParam1", "value1");
    +        expectedAmpData.put("queryParam2", "value2");
    +        expectedAmpData.put("tag_id", "tagId");
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getAmp)
    +                .extracting(ExtRequestPrebidAmp::getData)
    +                .containsOnly(expectedAmpData);
    +    }
    +
    +    @Test
    +    public void shouldReturnModifiedBidRequestWhenRequestWasPopulatedWithImplicitParams() {
    +        // given
    +        givenBidRequest();
    +
    +        final BidRequest updatedBidRequest = defaultBidRequest.toBuilder().id("updated").build();
    +        given(ortb2ImplicitParametersResolver.resolve(any(), any(), any(), any()))
    +                .willReturn(updatedBidRequest);
    +
    +        // when
    +        final AuctionContext result = target.fromRequest(routingContext, 0L).result();
    +
    +        // then
    +        assertThat(result.getBidRequest()).isEqualTo(updatedBidRequest);
    +    }
    +
    +    @Test
    +    public void shouldReturnPopulatedPrivacyContextAndGeoWhenPrivacyEnforcementReturnContext() {
    +        // given
    +        givenBidRequest();
    +
    +        final GeoInfo geoInfo = GeoInfo.builder().vendor("vendor").city("found").build();
    +        final PrivacyContext privacyContext = PrivacyContext.of(
    +                Privacy.of("1", "consent", Ccpa.EMPTY, 0),
    +                TcfContext.builder().geoInfo(geoInfo).build());
    +        given(privacyEnforcementService.contextFromBidRequest(any()))
    +                .willReturn(Future.succeededFuture(privacyContext));
    +
    +        // when
    +        final AuctionContext result = target.fromRequest(routingContext, 0L).result();
    +
    +        // then
    +        assertThat(result.getPrivacyContext()).isEqualTo(privacyContext);
    +        assertThat(result.getGeoInfo()).isEqualTo(geoInfo);
    +    }
    +
    +    @Test
    +    public void shouldPassAmpEndpointAndRequestMetricType() {
    +        // given
    +        givenBidRequest();
    +
    +        // when
    +        target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        verify(ortb2RequestFactory).createAuctionContext(eq(Endpoint.openrtb2_amp), eq(MetricName.amp));
    +    }
    +
    +    @Test
    +    public void shouldUseBidRequestModifiedByProcessedAuctionRequestHooks() {
    +        // given
    +        givenBidRequest(
    +                builder -> builder
    +                        .site(Site.builder().domain("example.com").build())
    +                        .ext(ExtRequest.empty()),
    +                Imp.builder().build());
    +
    +        final BidRequest modifiedBidRequest = BidRequest.builder()
    +                .app(App.builder().bundle("org.company.application").build())
    +                .build();
    +        doAnswer(invocation -> Future.succeededFuture(modifiedBidRequest))
    +                .when(ortb2RequestFactory)
    +                .executeProcessedAuctionRequestHooks(any());
    +
    +        // when
    +        final Future result = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +
    +        final BidRequest resultBidRequest = result.result().getBidRequest();
    +        assertThat(resultBidRequest.getSite()).isNull();
    +        assertThat(resultBidRequest.getApp()).isEqualTo(App.builder().bundle("org.company.application").build());
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfProcessedAuctionRequestHookRejectedRequest() {
    +        // given
    +        givenBidRequest();
    +
    +        final Throwable exception = new RuntimeException();
    +        doAnswer(invocation -> Future.failedFuture(exception))
    +                .when(ortb2RequestFactory)
    +                .executeProcessedAuctionRequestHooks(any());
    +
    +        final AuctionContext auctionContext = AuctionContext.builder().requestRejected(true).build();
    +        doReturn(Future.succeededFuture(auctionContext))
    +                .when(ortb2RequestFactory)
    +                .restoreResultFromRejection(eq(exception));
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future).succeededWith(auctionContext);
    +    }
    +
    +    private void givenBidRequest(
    +            Function storedBidRequestBuilderCustomizer,
    +            Imp... imps) {
    +        final List impList = imps.length > 0 ? asList(imps) : null;
    +
    +        given(storedRequestProcessor.processAmpRequest(any(), anyString(), any()))
    +                .willAnswer(invocation -> {
    +                    final BidRequest argument = invocation.getArgument(2);
    +                    return Future.succeededFuture(
    +                            storedBidRequestBuilderCustomizer.apply(argument.toBuilder().imp(impList)).build());
    +                });
    +
    +        given(ortb2RequestFactory.enrichAuctionContext(any(), any(), any(), anyLong()))
    +                .willAnswer(invocationOnMock -> ((AuctionContext) invocationOnMock.getArguments()[0]).toBuilder()
    +                        .httpRequest((HttpRequestContext) invocationOnMock.getArguments()[1])
    +                        .bidRequest((BidRequest) invocationOnMock.getArguments()[2])
    +                        .build());
    +        given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.succeededFuture());
    +
    +        given(ortb2ImplicitParametersResolver.resolve(any(), any(), any(), any())).willAnswer(
    +                answerWithFirstArgument());
    +        given(ortb2RequestFactory.validateRequest(any())).willAnswer(answerWithFirstArgument());
    +
    +        given(ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(any()))
    +                .willAnswer(invocation -> ((AuctionContext) invocation.getArgument(0)).getBidRequest());
    +        given(ortb2RequestFactory.executeProcessedAuctionRequestHooks(any()))
    +                .willAnswer(invocation -> Future.succeededFuture(
    +                        ((AuctionContext) invocation.getArgument(0)).getBidRequest()));
    +    }
    +
    +    private void givenBidRequest() {
    +        givenBidRequest(identity(), Imp.builder().build());
    +    }
    +
    +    private Answer answerWithFirstArgument() {
    +        return invocationOnMock -> invocationOnMock.getArguments()[0];
    +    }
    +
    +    private static Future toHttpRequest(RoutingContext routingContext, String body) {
    +        return Future.succeededFuture(HttpRequestContext.builder()
    +                .absoluteUri(routingContext.request().absoluteURI())
    +                .queryParams(toCaseInsensitiveMultiMap(routingContext.queryParams()))
    +                .headers(toCaseInsensitiveMultiMap(routingContext.request().headers()))
    +                .body(body)
    +                .scheme(routingContext.request().scheme())
    +                .remoteHost(routingContext.request().remoteAddress().host())
    +                .build());
    +    }
    +
    +    private static CaseInsensitiveMultiMap toCaseInsensitiveMultiMap(MultiMap originalMap) {
    +        final CaseInsensitiveMultiMap.Builder mapBuilder = CaseInsensitiveMultiMap.builder();
    +        originalMap.entries().forEach(entry -> mapBuilder.add(entry.getKey(), entry.getValue()));
    +
    +        return mapBuilder.build();
    +    }
    +
    +    private static ExtRequest givenRequestExt(ExtRequestTargeting extRequestTargeting) {
    +        return ExtRequest.of(ExtRequestPrebid.builder()
    +                .targeting(extRequestTargeting)
    +                .build());
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java
    new file mode 100644
    index 00000000000..09ce27fb57f
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java
    @@ -0,0 +1,650 @@
    +package org.prebid.server.auction.requestfactory;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.node.ArrayNode;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.iab.openrtb.request.App;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Site;
    +import io.vertx.core.Future;
    +import io.vertx.core.MultiMap;
    +import io.vertx.core.http.HttpServerRequest;
    +import io.vertx.core.net.impl.SocketAddressImpl;
    +import io.vertx.ext.web.RoutingContext;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.mockito.ArgumentCaptor;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.auction.ImplicitParametersExtractor;
    +import org.prebid.server.auction.InterstitialProcessor;
    +import org.prebid.server.auction.OrtbTypesResolver;
    +import org.prebid.server.auction.PrivacyEnforcementService;
    +import org.prebid.server.auction.StoredRequestProcessor;
    +import org.prebid.server.auction.TimeoutResolver;
    +import org.prebid.server.auction.model.AuctionContext;
    +import org.prebid.server.exception.InvalidRequestException;
    +import org.prebid.server.geolocation.model.GeoInfo;
    +import org.prebid.server.metric.MetricName;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
    +import org.prebid.server.model.HttpRequestContext;
    +import org.prebid.server.privacy.ccpa.Ccpa;
    +import org.prebid.server.privacy.gdpr.model.TcfContext;
    +import org.prebid.server.privacy.model.Privacy;
    +import org.prebid.server.privacy.model.PrivacyContext;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions;
    +import org.prebid.server.settings.model.Account;
    +
    +import java.util.ArrayList;
    +
    +import static java.util.Collections.emptyList;
    +import static java.util.Collections.singletonList;
    +import static java.util.Collections.singletonMap;
    +import static org.apache.commons.lang3.StringUtils.EMPTY;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyLong;
    +import static org.mockito.ArgumentMatchers.eq;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.doAnswer;
    +import static org.mockito.Mockito.doReturn;
    +import static org.mockito.Mockito.verify;
    +import static org.prebid.server.assertion.FutureAssertion.assertThat;
    +
    +public class AuctionRequestFactoryTest extends VertxTest {
    +
    +    private static final String ACCOUNT_ID = "acc_id";
    +
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
    +    @Mock
    +    private Ortb2RequestFactory ortb2RequestFactory;
    +    @Mock
    +    private StoredRequestProcessor storedRequestProcessor;
    +    @Mock
    +    private ImplicitParametersExtractor paramsExtractor;
    +    @Mock
    +    private Ortb2ImplicitParametersResolver paramsResolver;
    +    @Mock
    +    private InterstitialProcessor interstitialProcessor;
    +    @Mock
    +    private OrtbTypesResolver ortbTypesResolver;
    +    @Mock
    +    private PrivacyEnforcementService privacyEnforcementService;
    +    @Mock
    +    private TimeoutResolver timeoutResolver;
    +
    +    @Mock
    +    private AuctionRequestFactory target;
    +
    +    @Mock
    +    private RoutingContext routingContext;
    +    @Mock
    +    private HttpServerRequest httpRequest;
    +
    +    private Account defaultAccount;
    +    private BidRequest defaultBidRequest;
    +    private AuctionContext defaultActionContext;
    +
    +    @Before
    +    public void setUp() {
    +        defaultBidRequest = BidRequest.builder().build();
    +        defaultAccount = Account.empty(ACCOUNT_ID);
    +
    +        final PrivacyContext defaultPrivacyContext = PrivacyContext.of(
    +                Privacy.of("0", EMPTY, Ccpa.EMPTY, 0),
    +                TcfContext.empty());
    +        defaultActionContext = AuctionContext.builder()
    +                .requestTypeMetric(MetricName.openrtb2web)
    +                .bidRequest(defaultBidRequest)
    +                .account(defaultAccount)
    +                .prebidErrors(new ArrayList<>())
    +                .privacyContext(defaultPrivacyContext)
    +                .build();
    +
    +        given(routingContext.request()).willReturn(httpRequest);
    +        given(routingContext.queryParams()).willReturn(MultiMap.caseInsensitiveMultiMap());
    +        given(httpRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap());
    +        given(httpRequest.remoteAddress()).willReturn(new SocketAddressImpl(1234, "host"));
    +
    +        given(timeoutResolver.resolve(any())).willReturn(2000L);
    +        given(timeoutResolver.adjustTimeout(anyLong())).willReturn(1900L);
    +
    +        given(ortb2RequestFactory.createAuctionContext(any(), any())).willReturn(defaultActionContext);
    +        given(ortb2RequestFactory.executeEntrypointHooks(any(), any(), any()))
    +                .willAnswer(invocation -> toHttpRequest(invocation.getArgument(0), invocation.getArgument(1)));
    +        given(ortb2RequestFactory.executeRawAuctionRequestHooks(any()))
    +                .willAnswer(invocation -> Future.succeededFuture(
    +                        ((AuctionContext) invocation.getArgument(0)).getBidRequest()));
    +
    +        given(paramsResolver.resolve(any(), any(), any(), any()))
    +                .will(invocationOnMock -> invocationOnMock.getArgument(0));
    +        given(ortb2RequestFactory.validateRequest(any()))
    +                .will(invocationOnMock -> invocationOnMock.getArgument(0));
    +        given(interstitialProcessor.process(any()))
    +                .will(invocationOnMock -> invocationOnMock.getArgument(0));
    +
    +        given(privacyEnforcementService.contextFromBidRequest(any()))
    +                .willReturn(Future.succeededFuture(defaultPrivacyContext));
    +
    +        given(ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(any()))
    +                .willAnswer(invocation -> ((AuctionContext) invocation.getArgument(0)).getBidRequest());
    +        given(ortb2RequestFactory.executeProcessedAuctionRequestHooks(any()))
    +                .willAnswer(invocation -> Future.succeededFuture(
    +                        ((AuctionContext) invocation.getArgument(0)).getBidRequest()));
    +        given(ortb2RequestFactory.populateDealsInfo(any()))
    +                .willAnswer(invocationOnMock -> Future.succeededFuture(invocationOnMock.getArgument(0)));
    +        given(ortb2RequestFactory.restoreResultFromRejection(any()))
    +                .willAnswer(invocation -> Future.failedFuture((Throwable) invocation.getArgument(0)));
    +
    +        target = new AuctionRequestFactory(
    +                Integer.MAX_VALUE,
    +                ortb2RequestFactory,
    +                storedRequestProcessor,
    +                paramsExtractor,
    +                paramsResolver,
    +                interstitialProcessor,
    +                ortbTypesResolver,
    +                privacyEnforcementService,
    +                timeoutResolver,
    +                jacksonMapper);
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfRequestBodyIsMissing() {
    +        // given
    +        given(routingContext.getBody()).willReturn(null);
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause())
    +                .isInstanceOf(InvalidRequestException.class)
    +                .hasMessage("Incoming request has no body");
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() {
    +        // given
    +        target = new AuctionRequestFactory(
    +                1,
    +                ortb2RequestFactory,
    +                storedRequestProcessor,
    +                paramsExtractor,
    +                paramsResolver,
    +                interstitialProcessor,
    +                ortbTypesResolver,
    +                privacyEnforcementService,
    +                timeoutResolver,
    +                jacksonMapper);
    +
    +        given(routingContext.getBodyAsString()).willReturn("body");
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause())
    +                .isInstanceOf(InvalidRequestException.class)
    +                .hasMessage("Request size exceeded max size of 1 bytes.");
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfRequestBodyCouldNotBeParsed() {
    +        // given
    +        given(routingContext.getBodyAsString()).willReturn("body");
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    +        assertThat(((InvalidRequestException) future.cause()).getMessages()).hasSize(1)
    +                .element(0).asString().startsWith("Error decoding bidRequest: Unrecognized token 'body'");
    +    }
    +
    +    @Test
    +    public void shouldUseBodyAndHeadersModifiedByEntrypointHooks() {
    +        // given
    +        final BidRequest receivedBidRequest = BidRequest.builder()
    +                .device(Device.builder().dnt(1).build())
    +                .site(Site.builder().domain("example.com").build())
    +                .build();
    +
    +        givenBidRequest(receivedBidRequest);
    +
    +        final String rawModifiedBidRequest = bidRequestToString(BidRequest.builder()
    +                .app(App.builder().bundle("org.company.application").build())
    +                .build());
    +        doAnswer(invocation -> Future.succeededFuture(HttpRequestContext.builder().body(rawModifiedBidRequest).build()))
    +                .when(ortb2RequestFactory)
    +                .executeEntrypointHooks(any(), any(), any());
    +
    +        // when
    +        target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class);
    +        verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), captor.capture(), anyLong());
    +
    +        final BidRequest capturedRequest = captor.getValue();
    +        assertThat(capturedRequest.getSite()).isNull();
    +        assertThat(capturedRequest.getApp()).isEqualTo(App.builder().bundle("org.company.application").build());
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfEntrypointHookRejectedRequest() {
    +        // given
    +        givenValidBidRequest(defaultBidRequest);
    +
    +        final Throwable exception = new RuntimeException();
    +        doAnswer(invocation -> Future.failedFuture(exception))
    +                .when(ortb2RequestFactory)
    +                .executeEntrypointHooks(any(), any(), any());
    +
    +        final AuctionContext auctionContext = AuctionContext.builder().requestRejected(true).build();
    +        doReturn(Future.succeededFuture(auctionContext))
    +                .when(ortb2RequestFactory)
    +                .restoreResultFromRejection(eq(exception));
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future).succeededWith(auctionContext);
    +    }
    +
    +    @Test
    +    public void shouldUseBidRequestModifiedByRawAuctionRequestHooks() {
    +        // given
    +        givenValidBidRequest(BidRequest.builder()
    +                .site(Site.builder().domain("example.com").build())
    +                .build());
    +
    +        final BidRequest modifiedBidRequest = BidRequest.builder()
    +                .app(App.builder().bundle("org.company.application").build())
    +                .build();
    +        doAnswer(invocation -> Future.succeededFuture(modifiedBidRequest))
    +                .when(ortb2RequestFactory)
    +                .executeRawAuctionRequestHooks(any());
    +
    +        // when
    +        target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class);
    +        verify(storedRequestProcessor).processStoredRequests(any(), captor.capture());
    +
    +        final BidRequest capturedRequest = captor.getValue();
    +        assertThat(capturedRequest.getSite()).isNull();
    +        assertThat(capturedRequest.getApp()).isEqualTo(App.builder().bundle("org.company.application").build());
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfRawAuctionRequestHookRejectedRequest() {
    +        // given
    +        givenValidBidRequest(defaultBidRequest);
    +
    +        final Throwable exception = new RuntimeException();
    +        doAnswer(invocation -> Future.failedFuture(exception))
    +                .when(ortb2RequestFactory)
    +                .executeRawAuctionRequestHooks(any());
    +
    +        final AuctionContext auctionContext = AuctionContext.builder().requestRejected(true).build();
    +        doReturn(Future.succeededFuture(auctionContext))
    +                .when(ortb2RequestFactory)
    +                .restoreResultFromRejection(eq(exception));
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future).succeededWith(auctionContext);
    +    }
    +
    +    @Test
    +    public void shouldUseBidRequestModifiedByProcessedAuctionRequestHooks() {
    +        // given
    +        givenValidBidRequest(BidRequest.builder()
    +                .site(Site.builder().domain("example.com").build())
    +                .build());
    +
    +        final BidRequest modifiedBidRequest = BidRequest.builder()
    +                .app(App.builder().bundle("org.company.application").build())
    +                .build();
    +        doAnswer(invocation -> Future.succeededFuture(modifiedBidRequest))
    +                .when(ortb2RequestFactory)
    +                .executeProcessedAuctionRequestHooks(any());
    +
    +        // when
    +        final Future result = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +
    +        final BidRequest resultBidRequest = result.result().getBidRequest();
    +        assertThat(resultBidRequest.getSite()).isNull();
    +        assertThat(resultBidRequest.getApp()).isEqualTo(App.builder().bundle("org.company.application").build());
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfProcessedAuctionRequestHookRejectedRequest() {
    +        // given
    +        givenValidBidRequest(defaultBidRequest);
    +
    +        final Throwable exception = new RuntimeException();
    +        doAnswer(invocation -> Future.failedFuture(exception))
    +                .when(ortb2RequestFactory)
    +                .executeProcessedAuctionRequestHooks(any());
    +
    +        final AuctionContext auctionContext = AuctionContext.builder().requestRejected(true).build();
    +        doReturn(Future.succeededFuture(auctionContext))
    +                .when(ortb2RequestFactory)
    +                .restoreResultFromRejection(eq(exception));
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future).succeededWith(auctionContext);
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfEidsPermissionsContainsWrongDataType() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .data(ExtRequestPrebidData.of(emptyList(), null))
    +                        .build()))
    +                .build();
    +
    +        final ObjectNode requestNode = mapper.convertValue(bidRequest, ObjectNode.class);
    +        final JsonNode eidPermissionNode = mapper.convertValue(
    +                ExtRequestPrebidDataEidPermissions.of("source", emptyList()), JsonNode.class);
    +
    +        requestNode.with("ext").with("prebid").with("data").set("eidpermissions", eidPermissionNode);
    +
    +        given(routingContext.getBodyAsString()).willReturn(requestNode.toString());
    +
    +        // when
    +        final Future result = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(result.failed()).isTrue();
    +        assertThat(result.cause()).isInstanceOf(InvalidRequestException.class);
    +        assertThat(((InvalidRequestException) result.cause()).getMessages()).hasSize(1)
    +                .allSatisfy(message ->
    +                        assertThat(message).startsWith("Error decoding bidRequest: Cannot deserialize instance"));
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfEidsPermissionsBiddersContainsWrongDataType() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .data(ExtRequestPrebidData.of(emptyList(), null))
    +                        .build()))
    +                .build();
    +
    +        final ObjectNode requestNode = mapper.convertValue(bidRequest, ObjectNode.class);
    +
    +        final ObjectNode eidPermissionNode = mapper.convertValue(
    +                ExtRequestPrebidDataEidPermissions.of("source", emptyList()), ObjectNode.class);
    +
    +        eidPermissionNode.put("bidders", "notArrayValue");
    +
    +        final ArrayNode arrayNode = requestNode
    +                .with("ext")
    +                .with("prebid")
    +                .with("data")
    +                .putArray("eidpermissions");
    +        arrayNode.add(eidPermissionNode);
    +
    +        given(routingContext.getBodyAsString()).willReturn(requestNode.toString());
    +
    +        // when
    +        final Future result = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(result.failed()).isTrue();
    +        assertThat(result.cause()).isInstanceOf(InvalidRequestException.class);
    +        assertThat(((InvalidRequestException) result.cause()).getMessages()).hasSize(1)
    +                .allSatisfy(message ->
    +                        assertThat(message).startsWith("Error decoding bidRequest: Cannot deserialize instance"));
    +    }
    +
    +    @Test
    +    public void shouldTolerateMissingImpExtWhenProcessingAliases() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(null).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .aliases(singletonMap("alias", "bidder"))
    +                        .build()))
    +                .build();
    +        givenBidRequest(bidRequest);
    +        givenAuctionContext(bidRequest, defaultAccount);
    +        givenProcessStoredRequest(bidRequest);
    +
    +        // when
    +        final Future result = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(result.succeeded()).isTrue();
    +    }
    +
    +    @Test
    +    public void shouldCallOrtbFieldsResolver() {
    +        // given
    +        givenValidBidRequest();
    +
    +        // when
    +        target.fromRequest(routingContext, 0L).result();
    +
    +        // then
    +        verify(ortbTypesResolver).normalizeBidRequest(any(), any(), any());
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfOrtb2RequestFactoryReturnedFailedFuture() {
    +        // given
    +        givenValidBidRequest(BidRequest.builder().build());
    +        given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.failedFuture("error"));
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause()).hasMessage("error");
    +    }
    +
    +    @Test
    +    public void shouldSetWebRequestTypeInAuctionContextWhenSiteIsPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder().site(Site.builder().build()).build();
    +        givenValidBidRequest(bidRequest);
    +
    +        // when
    +        final Future result = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(result).isSucceeded();
    +        assertThat(result.result().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web);
    +    }
    +
    +    @Test
    +    public void shouldSetAppRequestTypeInContextWhenAppIsPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder().app(App.builder().build()).build();
    +        givenValidBidRequest(bidRequest);
    +
    +        // when
    +        final Future result = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(result).isSucceeded();
    +        assertThat(result.result().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2app);
    +    }
    +
    +    @Test
    +    public void storedRequestProcessorShouldUseAccountIdFetchedByOrtb2RequestFactory() {
    +        // given
    +        givenValidBidRequest();
    +
    +        // when
    +        target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        verify(storedRequestProcessor).processStoredRequests(eq(ACCOUNT_ID), any());
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfProcessStoredRequestsFailed() {
    +        // given
    +        givenValidBidRequest();
    +        given(storedRequestProcessor.processStoredRequests(any(), any()))
    +                .willReturn(Future.failedFuture("error"));
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause()).hasMessage("error");
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfRequestValidationFailed() {
    +        // given
    +        givenValidBidRequest();
    +
    +        given(ortb2RequestFactory.validateRequest(any()))
    +                .willThrow(new InvalidRequestException("errors"));
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    +        assertThat(((InvalidRequestException) future.cause()).getMessages()).containsOnly("errors");
    +    }
    +
    +    @Test
    +    public void shouldReturnAuctionContextWithExpectedParameters() {
    +        // given
    +        givenValidBidRequest();
    +
    +        // when
    +        final AuctionContext result = target.fromRequest(routingContext, 0L).result();
    +
    +        // then
    +        assertThat(result).isEqualTo(defaultActionContext);
    +    }
    +
    +    @Test
    +    public void shouldReturnModifiedBidRequestInAuctionContextWhenRequestWasPopulatedWithImplicitParams() {
    +        // given
    +        givenValidBidRequest();
    +
    +        final BidRequest updatedBidRequest = defaultBidRequest.toBuilder().id("updated").build();
    +        given(paramsResolver.resolve(any(), any(), any(), any())).willReturn(updatedBidRequest);
    +
    +        // when
    +        final AuctionContext result = target.fromRequest(routingContext, 0L).result();
    +
    +        // then
    +        assertThat(result.getBidRequest()).isEqualTo(updatedBidRequest);
    +    }
    +
    +    @Test
    +    public void shouldReturnPopulatedPrivacyContextAndGetWhenPrivacyEnforcementReturnContext() {
    +        // given
    +        givenValidBidRequest();
    +
    +        final GeoInfo geoInfo = GeoInfo.builder().vendor("vendor").city("found").build();
    +        final PrivacyContext privacyContext = PrivacyContext.of(
    +                Privacy.of("1", "consent", Ccpa.EMPTY, 0),
    +                TcfContext.builder().geoInfo(geoInfo).build());
    +        given(privacyEnforcementService.contextFromBidRequest(any()))
    +                .willReturn(Future.succeededFuture(privacyContext));
    +
    +        // when
    +        final AuctionContext result = target.fromRequest(routingContext, 0L).result();
    +
    +        // then
    +        assertThat(result.getPrivacyContext()).isEqualTo(privacyContext);
    +        assertThat(result.getGeoInfo()).isEqualTo(geoInfo);
    +    }
    +
    +    private void givenBidRequest(BidRequest bidRequest) {
    +        try {
    +            given(routingContext.getBodyAsString()).willReturn(mapper.writeValueAsString(bidRequest));
    +        } catch (JsonProcessingException e) {
    +            throw new RuntimeException(e);
    +        }
    +    }
    +
    +    private void givenAuctionContext(BidRequest bidRequest, Account account) {
    +        given(ortb2RequestFactory.enrichAuctionContext(any(), any(), any(), anyLong()))
    +                .willReturn(defaultActionContext.toBuilder()
    +                        .bidRequest(bidRequest)
    +                        .build());
    +        given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.succeededFuture(account));
    +    }
    +
    +    private void givenProcessStoredRequest(BidRequest bidRequest) {
    +        given(storedRequestProcessor.processStoredRequests(any(), any()))
    +                .willReturn(Future.succeededFuture(bidRequest));
    +    }
    +
    +    private void givenValidBidRequest() {
    +        givenValidBidRequest(defaultBidRequest);
    +    }
    +
    +    private void givenValidBidRequest(BidRequest bidRequest) {
    +        givenBidRequest(bidRequest);
    +        givenAuctionContext(bidRequest, defaultAccount);
    +        givenProcessStoredRequest(bidRequest);
    +    }
    +
    +    private static String bidRequestToString(BidRequest bidRequest) {
    +        try {
    +            return mapper.writeValueAsString(bidRequest);
    +        } catch (JsonProcessingException e) {
    +            throw new RuntimeException(e);
    +        }
    +    }
    +
    +    private static Future toHttpRequest(RoutingContext routingContext, String body) {
    +        return Future.succeededFuture(HttpRequestContext.builder()
    +                .absoluteUri(routingContext.request().absoluteURI())
    +                .queryParams(toCaseInsensitiveMultiMap(routingContext.queryParams()))
    +                .headers(toCaseInsensitiveMultiMap(routingContext.request().headers()))
    +                .body(body)
    +                .scheme(routingContext.request().scheme())
    +                .remoteHost(routingContext.request().remoteAddress().host())
    +                .build());
    +    }
    +
    +    private static CaseInsensitiveMultiMap toCaseInsensitiveMultiMap(MultiMap originalMap) {
    +        final CaseInsensitiveMultiMap.Builder mapBuilder = CaseInsensitiveMultiMap.builder();
    +        originalMap.entries().forEach(entry -> mapBuilder.add(entry.getKey(), entry.getValue()));
    +
    +        return mapBuilder.build();
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java
    new file mode 100644
    index 00000000000..44f165dd0b0
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java
    @@ -0,0 +1,1836 @@
    +package org.prebid.server.auction.requestfactory;
    +
    +import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.fasterxml.jackson.databind.node.TextNode;
    +import com.iab.openrtb.request.App;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Publisher;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.request.Source;
    +import com.iab.openrtb.request.User;
    +import com.iab.openrtb.request.Video;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.auction.ImplicitParametersExtractor;
    +import org.prebid.server.auction.IpAddressHelper;
    +import org.prebid.server.auction.TimeoutResolver;
    +import org.prebid.server.auction.model.IpAddress;
    +import org.prebid.server.exception.BlacklistedAppException;
    +import org.prebid.server.exception.InvalidRequestException;
    +import org.prebid.server.exception.PreBidException;
    +import org.prebid.server.identity.IdGenerator;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
    +import org.prebid.server.model.Endpoint;
    +import org.prebid.server.model.HttpRequestContext;
    +import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDevice;
    +import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange;
    +import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity;
    +import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheBids;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheVastxml;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidPbs;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
    +import org.prebid.server.proto.openrtb.ext.request.ExtSite;
    +
    +import java.math.BigDecimal;
    +import java.util.Arrays;
    +import java.util.List;
    +
    +import static java.util.Collections.singleton;
    +import static java.util.Collections.singletonList;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
    +import static org.assertj.core.api.Assertions.assertThatThrownBy;
    +import static org.assertj.core.groups.Tuple.tuple;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.ArgumentMatchers.eq;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.never;
    +import static org.mockito.Mockito.verify;
    +
    +public class Ortb2ImplicitParametersResolverTest extends VertxTest {
    +
    +    private static final List BLACKLISTED_APPS = singletonList("bad_app");
    +
    +    private static final String ENDPOINT = Endpoint.openrtb2_amp.value();
    +
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
    +    @Mock
    +    private ImplicitParametersExtractor paramsExtractor;
    +    @Mock
    +    private IpAddressHelper ipAddressHelper;
    +    @Mock
    +    private IdGenerator idGenerator;
    +
    +    private Ortb2ImplicitParametersResolver target;
    +
    +    @Mock
    +    private TimeoutResolver timeoutResolver;
    +
    +    private BidRequest defaultBidRequest;
    +    private HttpRequestContext httpRequest;
    +
    +    @Before
    +    public void setUp() {
    +        defaultBidRequest = BidRequest.builder().build();
    +
    +        httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.empty())
    +                .build();
    +
    +        given(idGenerator.generateId()).willReturn(null);
    +        given(timeoutResolver.resolve(any())).willReturn(2000L);
    +
    +        target = new Ortb2ImplicitParametersResolver(
    +                false,
    +                "USD",
    +                BLACKLISTED_APPS,
    +                paramsExtractor,
    +                ipAddressHelper,
    +                idGenerator,
    +                jacksonMapper);
    +    }
    +
    +    @Test
    +    public void shouldSetFieldsFromHeadersIfBodyFieldsEmptyForIpv4() {
    +        // given
    +        givenImplicitParams("http://example.com", "example.com", "192.168.244.1", IpAddress.IP.v4, "UnitTest");
    +
    +        // when
    +        final BidRequest result = target.resolve(defaultBidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite()).isEqualTo(Site.builder()
    +                .page("http://example.com")
    +                .domain("example.com")
    +                .publisher(Publisher.builder().domain("example.com").build())
    +                .ext(ExtSite.of(0, null))
    +                .build());
    +        assertThat(result.getDevice())
    +                .isEqualTo(Device.builder().ip("192.168.244.1").ua("UnitTest").build());
    +    }
    +
    +    @Test
    +    public void shouldSetFieldsFromHeadersIfBodyFieldsInvalidForIpv4() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ip("127.0.0.1").build())
    +                .build();
    +
    +        given(ipAddressHelper.toIpAddress(eq("127.0.0.1"))).willReturn(null);
    +
    +        givenImplicitParams("http://example.com", "example.com", "192.168.244.1", IpAddress.IP.v4, "UnitTest");
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite()).isEqualTo(Site.builder()
    +                .page("http://example.com")
    +                .domain("example.com")
    +                .publisher(Publisher.builder().domain("example.com").build())
    +                .ext(ExtSite.of(0, null))
    +                .build());
    +        assertThat(result.getDevice())
    +                .isEqualTo(Device.builder().ip("192.168.244.1").ua("UnitTest").build());
    +    }
    +
    +    @Test
    +    public void shouldSetFieldsFromHeadersIfBodyFieldsEmptyForIpv6() {
    +        // given
    +        givenImplicitParams(
    +                "http://example.com",
    +                "example.com",
    +                "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
    +                IpAddress.IP.v6,
    +                "UnitTest");
    +
    +        // when
    +        final BidRequest result = target.resolve(defaultBidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite()).isEqualTo(Site.builder()
    +                .page("http://example.com")
    +                .domain("example.com")
    +                .publisher(Publisher.builder().domain("example.com").build())
    +                .ext(ExtSite.of(0, null))
    +                .build());
    +        assertThat(result.getDevice())
    +                .isEqualTo(Device.builder().ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334").ua("UnitTest").build());
    +    }
    +
    +    @Test
    +    public void shouldSetFieldsFromHeadersIfBodyFieldsInvalidForIpv6() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ipv6("::1").build())
    +                .build();
    +
    +        given(ipAddressHelper.toIpAddress(eq("::1"))).willReturn(null);
    +
    +        givenImplicitParams(
    +                "http://example.com",
    +                "example.com",
    +                "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
    +                IpAddress.IP.v6,
    +                "UnitTest");
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite()).isEqualTo(Site.builder()
    +                .page("http://example.com")
    +                .domain("example.com")
    +                .publisher(Publisher.builder().domain("example.com").build())
    +                .ext(ExtSite.of(0, null))
    +                .build());
    +        assertThat(result.getDevice())
    +                .isEqualTo(Device.builder().ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334").ua("UnitTest").build());
    +    }
    +
    +    @Test
    +    public void shouldSetAnonymizedIpv6FromField() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334").build())
    +                .build();
    +
    +        given(ipAddressHelper.toIpAddress(eq("2001:0db8:85a3:0000:0000:8a2e:0370:7334")))
    +                .willReturn(IpAddress.of("2001:0db8:85a3:0000::", IpAddress.IP.v6));
    +
    +        givenImplicitParams(
    +                "http://example.com",
    +                "example.com",
    +                "1111:2222:3333:4444:5555:6666:7777:8888",
    +                IpAddress.IP.v6,
    +                "UnitTest");
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite()).isEqualTo(Site.builder()
    +                .page("http://example.com")
    +                .domain("example.com")
    +                .publisher(Publisher.builder().domain("example.com").build())
    +                .ext(ExtSite.of(0, null))
    +                .build());
    +        assertThat(result.getDevice())
    +                .isEqualTo(Device.builder().ipv6("2001:0db8:85a3:0000::").ua("UnitTest").build());
    +    }
    +
    +    @Test
    +    public void shouldNotImplicitlyResolveIpIfIpv6IsPassed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334").build())
    +                .build();
    +
    +        given(ipAddressHelper.toIpAddress(eq("2001:0db8:85a3:0000:0000:8a2e:0370:7334")))
    +                .willReturn(IpAddress.of("2001:0db8:85a3:0000::", IpAddress.IP.v6));
    +
    +        // when
    +        target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        verify(paramsExtractor, never()).ipFrom(any(CaseInsensitiveMultiMap.class), any());
    +    }
    +
    +    @Test
    +    public void shouldNotSetDeviceDntIfHeaderHasInvalidValue() {
    +        // given
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.builder()
    +                        .add("DNT", "invalid")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(defaultBidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getDnt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceDntIfHeaderExists() {
    +        // given
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.builder()
    +                        .add("DNT", "1")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(defaultBidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getDnt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldOverrideDeviceDntIfHeaderExists() {
    +        // given
    +        final HttpRequestContext httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.builder()
    +                        .add("DNT", "0")
    +                        .build())
    +                .build();
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().dnt(1).build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getDnt()).isZero();
    +    }
    +
    +    @Test
    +    public void shouldNotSetDeviceLmtForIos14IfNoApp() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.0")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldNotSetDeviceLmtForNonIos() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("plan9")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldNotSetDeviceLmtForIosInvalidVersion() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("invalid-version")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldNotSetDeviceLmtForIosInvalidVersionMajor() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("invalid-major.0")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldNotSetDeviceLmtForIosInvalidVersionMinor() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.invalid-minor")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldNotSetDeviceLmtForIosMissingVersionMinor() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldNotSetDeviceLmtForIosLowerThan14() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("13.4")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtOneForIos14WithPatchVersion() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.0.patch-version")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtOneForIos14Minor0() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.0")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtOneForIos14Minor0AndEmptyIfa() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.0")
    +                        .ifa("")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldOverrideDeviceLmtForIos14Minor0AndEmptyIfa() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .lmt(0)
    +                        .os("iOS")
    +                        .osv("14.0")
    +                        .ifa("")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtOneForIos14Minor0AndZerosIfa() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.0")
    +                        .ifa("00000000-0000-0000-0000-000000000000")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtZeroForIos14Minor0AndNonZerosIfa() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.1")
    +                        .ifa("12345")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isZero();
    +    }
    +
    +    @Test
    +    public void shouldNotOverrideDeviceLmtForIos14Minor0AndNonZerosIfaWhenLmtAlreadySet() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .lmt(1)
    +                        .os("iOS")
    +                        .osv("14.0")
    +                        .ifa("12345")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtOneForIos14Minor1() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.1")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtOneForIos14Minor1AndEmptyIfa() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.1")
    +                        .ifa("")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldOverrideDeviceLmtForIos14Minor1AndEmptyIfa() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .lmt(0)
    +                        .os("iOS")
    +                        .osv("14.1")
    +                        .ifa("")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtOneForIos14Minor1AndZerosIfa() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.1")
    +                        .ifa("00000000-0000-0000-0000-000000000000")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtZeroForIos14Minor1AndNonZerosIfa() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.1")
    +                        .ifa("12345")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isZero();
    +    }
    +
    +    @Test
    +    public void shouldNotOverrideDeviceLmtForIos14Minor1AndNonZerosIfaWhenLmtAlreadySet() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .lmt(1)
    +                        .os("iOS")
    +                        .osv("14.1")
    +                        .ifa("12345")
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtZeroForIos14Minor2AndAtts0() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.2")
    +                        .ext(ExtDevice.of(0, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isZero();
    +    }
    +
    +    @Test
    +    public void shouldNotOverrideDeviceLmtForIos14Minor2AndAtts0() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .lmt(1)
    +                        .os("iOS")
    +                        .osv("14.2")
    +                        .ext(ExtDevice.of(0, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtOneForIos14Minor2AndAtts1() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.2")
    +                        .ext(ExtDevice.of(1, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldNotOverrideDeviceLmtForIos14Minor2AndAtts1() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .lmt(0)
    +                        .os("iOS")
    +                        .osv("14.2")
    +                        .ext(ExtDevice.of(1, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isZero();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtOneForIos15Minor0AndAtts1() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("15.0")
    +                        .ext(ExtDevice.of(1, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtOneForIos15Minor0AndAtts2() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("15.0")
    +                        .ext(ExtDevice.of(2, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldNotOverrideDeviceLmtForIos14Minor2AndAtts2() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .lmt(0)
    +                        .os("iOS")
    +                        .osv("14.2")
    +                        .ext(ExtDevice.of(2, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isZero();
    +    }
    +
    +    @Test
    +    public void shouldSetDeviceLmtZeroForIos14Minor2AndAtts3() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.2")
    +                        .ext(ExtDevice.of(3, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isZero();
    +    }
    +
    +    @Test
    +    public void shouldNotOverrideDeviceLmtForIos14Minor2AndAtts3() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .lmt(1)
    +                        .os("iOS")
    +                        .osv("14.2")
    +                        .ext(ExtDevice.of(3, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isOne();
    +    }
    +
    +    @Test
    +    public void shouldNotSetDeviceLmtForIos14Minor3AndAtts4() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.3")
    +                        .ext(ExtDevice.of(4, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldNotSetDeviceLmtForIos14Minor3AndAttsNull() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .device(Device.builder()
    +                        .os("iOS")
    +                        .osv("14.3")
    +                        .ext(ExtDevice.of(null, null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getDevice().getLmt()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldUpdateImpsWithSecurityOneIfRequestIsSecuredAndImpSecurityNotDefined() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().build())).build();
    +        given(paramsExtractor.secureFrom(any())).willReturn(1);
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getImp()).flatExtracting(Imp::getSecure).containsOnly(1);
    +    }
    +
    +    @Test
    +    public void shouldNotUpdateImpsWithSecurityOneIfRequestIsSecureAndImpSecurityIsZero() {
    +        // given
    +        final List imps = singletonList(Imp.builder().secure(0).build());
    +
    +        final BidRequest bidRequest = BidRequest.builder().imp(imps).build();
    +
    +        given(paramsExtractor.secureFrom(any())).willReturn(1);
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getImp()).isSameAs(imps);
    +    }
    +
    +    @Test
    +    public void shouldUpdateImpsOnlyWithNotDefinedSecurityWithSecurityOneIfRequestIsSecure() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(Arrays.asList(Imp.builder().build(), Imp.builder().secure(0).build()))
    +                .build();
    +        given(paramsExtractor.secureFrom(any())).willReturn(1);
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getImp()).extracting(Imp::getSecure).containsOnly(1, 0);
    +    }
    +
    +    @Test
    +    public void shouldNotUpdateImpsWithSecurityOneIfRequestIsNotSecureAndImpSecurityIsNotDefined() {
    +        // given
    +        final List imps = singletonList(Imp.builder().build());
    +
    +        final BidRequest bidRequest = BidRequest.builder().imp(imps).build();
    +
    +        given(paramsExtractor.secureFrom(any())).willReturn(0);
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getImp()).isSameAs(imps);
    +    }
    +
    +    @Test
    +    public void shouldMoveBidderParametersToImpExtPrebidBidderAndMergeWithExisting() {
    +        // given
    +        final List imps = singletonList(
    +                Imp.builder()
    +                        .ext(mapper.createObjectNode()
    +                                .set("bidder1", mapper.createObjectNode().put("param1", "value1"))
    +                                .set("bidder2", mapper.createObjectNode().put("param2", "value2"))
    +                                .set("context", mapper.createObjectNode().put("data", "datavalue"))
    +                                .set("all", mapper.createObjectNode().put("all-data", "all-value"))
    +                                .set("general", mapper.createObjectNode()
    +                                        .put("general-data", "general-value"))
    +                                .set("skadn", mapper.createObjectNode()
    +                                        .put("skadn-data", "skadn-value"))
    +                                .set("data", mapper.createObjectNode()
    +                                        .put("data-data", "data-value"))
    +                                .set("prebid", mapper.createObjectNode()
    +                                        .set("bidder", mapper.createObjectNode()
    +                                                .set("bidder2", mapper.createObjectNode().put("param22", "value22")))
    +                                        .set("storedresult", mapper.createObjectNode().put("id", "storedreq1"))))
    +                        .build());
    +
    +        final BidRequest bidRequest = BidRequest.builder().imp(imps).build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        final Imp expectedImp = Imp.builder()
    +                .ext(mapper.createObjectNode()
    +                        .set("context", mapper.createObjectNode().put("data", "datavalue"))
    +                        .set("all", mapper.createObjectNode().put("all-data", "all-value"))
    +                        .set("general", mapper.createObjectNode()
    +                                .put("general-data", "general-value"))
    +                        .set("skadn", mapper.createObjectNode()
    +                                .put("skadn-data", "skadn-value"))
    +                        .set("data", mapper.createObjectNode()
    +                                .put("data-data", "data-value"))
    +                        .set("prebid", mapper.createObjectNode()
    +                                .set("bidder", mapper.createObjectNode()
    +                                        .set(
    +                                                "bidder1", mapper.createObjectNode().put("param1", "value1"))
    +                                        .set(
    +                                                "bidder2", mapper.createObjectNode()
    +                                                        .put("param2", "value2")
    +                                                        .put("param22", "value22")))
    +                                .set("storedresult", mapper.createObjectNode().put("id", "storedreq1"))))
    +                .build();
    +
    +        assertThat(result.getImp()).isEqualTo(singletonList(expectedImp));
    +    }
    +
    +    @Test
    +    public void shouldMoveBidderParametersToImpExtPrebidBidderWhenImpExtPrebidAbsent() {
    +        // given
    +        final List imps = singletonList(
    +                Imp.builder()
    +                        .ext(mapper.createObjectNode()
    +                                .set("bidder1", mapper.createObjectNode().put("param1", "value1"))
    +                                .set("bidder2", mapper.createObjectNode().put("param2", "value2")))
    +                        .build());
    +
    +        final BidRequest bidRequest = BidRequest.builder().imp(imps).build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        final Imp expectedImp = Imp.builder()
    +                .ext(mapper.createObjectNode()
    +                        .set("prebid", mapper.createObjectNode()
    +                                .set("bidder", mapper.createObjectNode()
    +                                        .set(
    +                                                "bidder1", mapper.createObjectNode().put("param1", "value1"))
    +                                        .set(
    +                                                "bidder2", mapper.createObjectNode().put("param2", "value2")))))
    +                .build();
    +        assertThat(result.getImp()).isEqualTo(singletonList(expectedImp));
    +    }
    +
    +    @Test
    +    public void shouldNotChangeImpExtWhenBidderParametersAreAtImpExtPrebidBidderOnly() {
    +        // given
    +        final List imps = singletonList(
    +                Imp.builder()
    +                        .ext(mapper.createObjectNode()
    +                                .set("prebid", mapper.createObjectNode()
    +                                        .set("bidder", mapper.createObjectNode()
    +                                                .set(
    +                                                        "bidder1", mapper.createObjectNode().put("param1", "value1"))
    +                                                .set(
    +                                                        "bidder2", mapper.createObjectNode().put("param2", "value2")))))
    +                        .build());
    +
    +        final BidRequest bidRequest = BidRequest.builder().imp(imps).build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getImp()).isSameAs(imps);
    +    }
    +
    +    @Test
    +    public void shouldNotSetFieldsFromHeadersIfRequestFieldsNotEmpty() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .site(Site.builder()
    +                        .page("http://test.com")
    +                        .domain("test.com")
    +                        .publisher(Publisher.builder().domain("test.com").build())
    +                        .ext(ExtSite.of(0, null))
    +                        .build())
    +                .device(Device.builder().ua("UnitTestUA").ip("56.76.12.3").build())
    +                .user(User.builder().id("userId").build())
    +                .cur(singletonList("USD"))
    +                .tmax(2000L)
    +                .at(1)
    +                .build();
    +
    +        given(ipAddressHelper.toIpAddress(eq("56.76.12.3")))
    +                .willReturn(IpAddress.of("56.76.12.3", IpAddress.IP.v4));
    +
    +        givenImplicitParams(
    +                "http://anotherexample.com", "anotherexample.com", "192.168.244.2", IpAddress.IP.v4, "UnitTest2");
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result).isSameAs(bidRequest);
    +    }
    +
    +    @Test
    +    public void shouldSetSiteExtIfNoReferer() {
    +        // when
    +        final BidRequest result = target.resolve(defaultBidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite())
    +                .extracting(Site::getExt)
    +                .containsOnly(ExtSite.of(0, null));
    +    }
    +
    +    @Test
    +    public void shouldNotSetSitePageIfNoReferer() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .site(Site.builder().domain("home.com").build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite()).isEqualTo(
    +                Site.builder().domain("home.com").ext(ExtSite.of(0, null)).build());
    +    }
    +
    +    @Test
    +    public void shouldNotSetSitePageIfDomainCouldNotBeDerived() {
    +        // given
    +        given(paramsExtractor.refererFrom(any())).willReturn("http://not-valid-site");
    +        given(paramsExtractor.domainFrom(anyString())).willThrow(new PreBidException("Couldn't derive domain"));
    +
    +        // when
    +        final BidRequest result = target.resolve(defaultBidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite().getPage()).isNull();
    +    }
    +
    +    @Test
    +    public void shouldSetDomainFromPageInsteadOfReferer() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .site(Site.builder().page("http://page.site.com/page1.html").build())
    +                .build();
    +
    +        given(paramsExtractor.refererFrom(any())).willReturn("http://any-site/referer.html");
    +        given(paramsExtractor.domainFrom(anyString())).willReturn("site.com");
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        verify(paramsExtractor).domainFrom(eq("page.site.com"));
    +
    +        assertThat(singleton(result.getSite()))
    +                .extracting(Site::getPage, Site::getDomain, site -> site.getPublisher().getDomain())
    +                .containsOnly(tuple("http://page.site.com/page1.html", "page.site.com", "site.com"));
    +    }
    +
    +    @Test
    +    public void shouldSetSiteExtAmpIfSiteHasNoExt() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .site(Site.builder()
    +                        .page("http://test.com")
    +                        .domain("test.com")
    +                        .publisher(Publisher.builder().domain("test.com").build())
    +                        .build())
    +                .build();
    +        givenImplicitParams(
    +                "http://anotherexample.com", "anotherexample.com", "192.168.244.2", IpAddress.IP.v4, "UnitTest2");
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite()).isEqualTo(
    +                Site.builder()
    +                        .page("http://test.com")
    +                        .domain("test.com")
    +                        .publisher(Publisher.builder().domain("test.com").build())
    +                        .ext(ExtSite.of(0, null))
    +                        .build());
    +    }
    +
    +    @Test
    +    public void shouldSetSiteExtAmpIfSiteExtHasNoAmp() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .site(Site.builder()
    +                        .page("http://test.com")
    +                        .domain("test.com")
    +                        .publisher(Publisher.builder().domain("test.com").build())
    +                        .ext(ExtSite.of(null, null))
    +                        .build())
    +                .build();
    +        givenImplicitParams(
    +                "http://anotherexample.com", "anotherexample.com", "192.168.244.2", IpAddress.IP.v4, "UnitTest2");
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite()).isEqualTo(Site.builder()
    +                .page("http://test.com")
    +                .domain("test.com")
    +                .publisher(Publisher.builder().domain("test.com").build())
    +                .ext(ExtSite.of(0, null))
    +                .build());
    +    }
    +
    +    @Test
    +    public void shouldSetSiteExtAmpIfNoReferer() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .site(Site.builder().domain("test.com").page("http://test.com").build())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSite()).isEqualTo(
    +                Site.builder().domain("test.com").page("http://test.com")
    +                        .ext(ExtSite.of(0, null)).build());
    +    }
    +
    +    @Test
    +    public void shouldSetSourceTidIfNotDefined() {
    +        // given
    +        given(idGenerator.generateId()).willReturn("f6965ea7-f281-4eb9-9de2-560a52d954a3");
    +
    +        final BidRequest bidRequest = BidRequest.builder().build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getSource())
    +                .isEqualTo(Source.builder().tid("f6965ea7-f281-4eb9-9de2-560a52d954a3").build());
    +    }
    +
    +    @Test
    +    public void shouldSetDefaultAtIfInitialValueIsEqualsToZero() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder().at(0).build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getAt()).isEqualTo(1);
    +    }
    +
    +    @Test
    +    public void shouldSetDefaultAtIfInitialValueIsEqualsToNull() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder().at(null).build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getAt()).isEqualTo(1);
    +    }
    +
    +    @Test
    +    public void shouldSetCurrencyIfMissedInRequestAndPresentInAdServerCurrencyConfig() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder().cur(null).build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getCur()).isEqualTo(singletonList("USD"));
    +    }
    +
    +    @Test
    +    public void shouldSetTimeoutFromTimeoutResolver() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder().build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getTmax()).isEqualTo(2000L);
    +    }
    +
    +    @Test
    +    public void shouldConvertStringPriceGranularityViewToCustom() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .targeting(ExtRequestTargeting.builder().pricegranularity(new TextNode("low")).build())
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        // result was wrapped to list because extracting method works different on iterable and not iterable objects,
    +        // which force to make type casting or exception handling in lambdas
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getPricegranularity)
    +                .containsOnly(mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of(
    +                        BigDecimal.valueOf(5), BigDecimal.valueOf(0.5))))));
    +    }
    +
    +    @Test
    +    public void shouldNotUpdateExtTargetingIfImpressionsAreMissed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .containsNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureWithInvalidRequestExceptionWhenStringPriceGranularityInvalid() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .targeting(ExtRequestTargeting.builder().pricegranularity(new TextNode("invalid")).build())
    +                        .build()))
    +                .build();
    +
    +        // when and then
    +        assertThatThrownBy(() -> target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT))
    +                .isInstanceOf(InvalidRequestException.class)
    +                .hasMessage("Invalid string price granularity with value: invalid");
    +    }
    +
    +    @Test
    +    public void shouldSetDefaultPriceGranularityIfPriceGranularityAndMediaTypePriceGranularityIsMissing() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().video(Video.builder().build()).ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .targeting(ExtRequestTargeting.builder().build())
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getPricegranularity)
    +                .containsOnly(mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of(
    +                        BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))));
    +    }
    +
    +    @Test
    +    public void shouldNotSetDefaultPriceGranularityIfThereIsAMediaTypePriceGranularityForImpType() {
    +        // given
    +        final ExtMediaTypePriceGranularity mediaTypePriceGranularity = ExtMediaTypePriceGranularity.of(
    +                mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of(
    +                        BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))), null, null);
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().banner(Banner.builder().build())
    +                        .ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .targeting(ExtRequestTargeting.builder()
    +                                .mediatypepricegranularity(mediaTypePriceGranularity)
    +                                .build())
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getPricegranularity)
    +                .containsOnly((JsonNode) null);
    +    }
    +
    +    @Test
    +    public void shouldSetDefaultIncludeWinnersIfIncludeWinnersIsMissed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .targeting(ExtRequestTargeting.builder().build())
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludewinners)
    +                .containsOnly(true);
    +    }
    +
    +    @Test
    +    public void shouldSetDefaultIncludeBidderKeysIfIncludeBidderKeysIsMissed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .targeting(ExtRequestTargeting.builder().build())
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludebidderkeys)
    +                .containsOnly(true);
    +    }
    +
    +    @Test
    +    public void shouldSetDefaultIncludeBidderKeysToFalseIfIncludeBidderKeysIsMissedAndWinningonlyIsTrue() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .targeting(ExtRequestTargeting.builder().build())
    +                        .cache(ExtRequestPrebidCache.of(null, null, true))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludebidderkeys)
    +                .containsOnly(false);
    +    }
    +
    +    @Test
    +    public void shouldSetDefaultIncludeBidderKeysToFalseIfIncludeBidderKeysIsMissedAndWinningonlyIsTrueInConfig() {
    +        // given
    +        target = new Ortb2ImplicitParametersResolver(
    +                true,
    +                "USD",
    +                BLACKLISTED_APPS,
    +                paramsExtractor,
    +                ipAddressHelper,
    +                idGenerator,
    +                jacksonMapper);
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .targeting(ExtRequestTargeting.builder().build())
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(result.getExt())
    +                .extracting(extBidRequest -> extBidRequest.getPrebid().getTargeting().getIncludebidderkeys())
    +                .containsOnly(false);
    +    }
    +
    +    @Test
    +    public void shouldSetCacheWinningonlyFromConfigWhenExtRequestPrebidIsNull() {
    +        // given
    +        target = new Ortb2ImplicitParametersResolver(
    +                true,
    +                "USD",
    +                BLACKLISTED_APPS,
    +                paramsExtractor,
    +                ipAddressHelper,
    +                idGenerator,
    +                jacksonMapper);
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.empty())
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getCache)
    +                .extracting(ExtRequestPrebidCache::getWinningonly)
    +                .containsOnly(true);
    +    }
    +
    +    @Test
    +    public void shouldSetCacheWinningonlyFromConfigWhenExtRequestPrebidCacheIsNull() {
    +        // given
    +        target = new Ortb2ImplicitParametersResolver(
    +                true,
    +                "USD",
    +                BLACKLISTED_APPS,
    +                paramsExtractor,
    +                ipAddressHelper,
    +                idGenerator,
    +                jacksonMapper);
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getCache)
    +                .extracting(ExtRequestPrebidCache::getWinningonly)
    +                .containsOnly(true);
    +    }
    +
    +    @Test
    +    public void shouldSetCacheWinningonlyFromConfigWhenCacheWinningonlyIsNull() {
    +        // given
    +        target = new Ortb2ImplicitParametersResolver(
    +                true,
    +                "USD",
    +                BLACKLISTED_APPS,
    +                paramsExtractor,
    +                ipAddressHelper,
    +                idGenerator,
    +                jacksonMapper);
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .cache(ExtRequestPrebidCache.of(null, null, null))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getCache)
    +                .extracting(ExtRequestPrebidCache::getWinningonly)
    +                .containsOnly(true);
    +    }
    +
    +    @Test
    +    public void shouldNotChangeAnyOtherExtRequestPrebidCacheFields() {
    +        // given
    +        final ExtRequestPrebidCacheBids cacheBids = ExtRequestPrebidCacheBids.of(100, true);
    +        final ExtRequestPrebidCacheVastxml cacheVastxml = ExtRequestPrebidCacheVastxml.of(100, true);
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .cache(ExtRequestPrebidCache.of(cacheBids, cacheVastxml, null))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getCache)
    +                .extracting(ExtRequestPrebidCache::getBids, ExtRequestPrebidCache::getVastxml)
    +                .containsOnly(tuple(cacheBids, cacheVastxml));
    +    }
    +
    +    @Test
    +    public void shouldNotChangeAnyOtherExtRequestPrebidTargetingFields() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .targeting(ExtRequestTargeting.builder()
    +                                .includebrandcategory(ExtIncludeBrandCategory.of(1, "publisher", true))
    +                                .truncateattrchars(10)
    +                                .build())
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getTargeting)
    +                .extracting(ExtRequestTargeting::getIncludebrandcategory, ExtRequestTargeting::getTruncateattrchars)
    +                .containsOnly(tuple(ExtIncludeBrandCategory.of(1, "publisher", true), 10));
    +    }
    +
    +    @Test
    +    public void shouldSetCacheWinningonlyFromRequestWhenCacheWinningonlyIsPresent() {
    +        // given
    +        target = new Ortb2ImplicitParametersResolver(
    +                true,
    +                "USD",
    +                BLACKLISTED_APPS,
    +                paramsExtractor,
    +                ipAddressHelper,
    +                idGenerator,
    +                jacksonMapper);
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .cache(ExtRequestPrebidCache.of(null, null, false))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getCache)
    +                .extracting(ExtRequestPrebidCache::getWinningonly)
    +                .containsOnly(false);
    +    }
    +
    +    @Test
    +    public void shouldNotSetCacheWinningonlyFromConfigWhenCacheWinningonlyIsNullAndConfigValueIsFalse() {
    +        // given
    +        final ExtRequest extBidRequest = ExtRequest.of(ExtRequestPrebid.builder()
    +                .cache(ExtRequestPrebidCache.of(null, null, null))
    +                .build());
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(extBidRequest)
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        final ExtRequest expectedExtBidRequest = ExtRequest.of(ExtRequestPrebid.builder()
    +                .cache(ExtRequestPrebidCache.of(null, null, null))
    +                .pbs(ExtRequestPrebidPbs.of(ENDPOINT))
    +                .build());
    +        assertThat(result.getExt()).isEqualTo(expectedExtBidRequest);
    +    }
    +
    +    @Test
    +    public void shouldCreateExtBidRequestPbsFromGivenEndpoint() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        final ExtRequest expectedExtBidRequest = ExtRequest.of(ExtRequestPrebid.builder()
    +                .pbs(ExtRequestPrebidPbs.of(ENDPOINT))
    +                .build());
    +        assertThat(result.getExt()).isEqualTo(expectedExtBidRequest);
    +    }
    +
    +    @Test
    +    public void shouldSetRequestPrebidChannelWhenMissingInRequestAndSite() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .site(Site.builder().build())
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getChannel)
    +                .containsOnly(ExtRequestPrebidChannel.of("web"));
    +    }
    +
    +    @Test
    +    public void shouldSetRequestPrebidChannelWhenMissingInRequestAndApp() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getChannel)
    +                .containsOnly(ExtRequestPrebidChannel.of("app"));
    +    }
    +
    +    @Test
    +    public void shouldNotSetRequestPrebidChannelWhenMissingInRequestAndNotSiteOrApp() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getChannel)
    +                .containsOnly((ExtRequestPrebidChannel) null);
    +    }
    +
    +    @Test
    +    public void shouldNotSetRequestPrebidChannelWhenPresentInRequestAndApp() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .channel(ExtRequestPrebidChannel.of("custom"))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getChannel)
    +                .containsOnly(ExtRequestPrebidChannel.of("custom"));
    +    }
    +
    +    @Test
    +    public void shouldSetRequestPrebidChannelToAppWhenMissingInRequestAndBothAppAndSitePresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .site(Site.builder().build())
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().build()))
    +                .build();
    +
    +        // when
    +        final BidRequest request = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(request))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getChannel)
    +                .containsOnly(ExtRequestPrebidChannel.of("app"));
    +    }
    +
    +    @Test
    +    public void shouldPassExtPrebidDebugFlagIfPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build()))
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .debug(1)
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT);
    +
    +        // then
    +        assertThat(singletonList(result))
    +                .extracting(BidRequest::getExt)
    +                .extracting(ExtRequest::getPrebid)
    +                .extracting(ExtRequestPrebid::getDebug)
    +                .containsOnly(1);
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureWhenAppIdIsBlacklisted() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().id("bad_app").build())
    +                .build();
    +
    +        // when and then
    +        assertThatExceptionOfType(BlacklistedAppException.class)
    +                .isThrownBy(() -> target.resolve(bidRequest, httpRequest, timeoutResolver, ENDPOINT))
    +                .withMessage("Prebid-server does not process requests from App ID: bad_app");
    +    }
    +
    +    private void givenImplicitParams(String referer, String domain, String ip, IpAddress.IP ipVersion, String ua) {
    +        given(paramsExtractor.refererFrom(any())).willReturn(referer);
    +        given(paramsExtractor.domainFrom(anyString())).willReturn(domain);
    +        given(paramsExtractor.ipFrom(any(CaseInsensitiveMultiMap.class), any())).willReturn(singletonList(ip));
    +        given(ipAddressHelper.toIpAddress(eq(ip))).willReturn(IpAddress.of(ip, ipVersion));
    +        given(paramsExtractor.uaFrom(any())).willReturn(ua);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java
    new file mode 100644
    index 00000000000..76ea22589cb
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java
    @@ -0,0 +1,1119 @@
    +package org.prebid.server.auction.requestfactory;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.App;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Geo;
    +import com.iab.openrtb.request.Publisher;
    +import com.iab.openrtb.request.Site;
    +import io.vertx.core.Future;
    +import io.vertx.core.MultiMap;
    +import io.vertx.core.http.HttpServerRequest;
    +import io.vertx.core.net.impl.SocketAddressImpl;
    +import io.vertx.ext.web.RoutingContext;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.auction.IpAddressHelper;
    +import org.prebid.server.auction.StoredRequestProcessor;
    +import org.prebid.server.auction.TimeoutResolver;
    +import org.prebid.server.auction.model.AuctionContext;
    +import org.prebid.server.auction.model.DebugContext;
    +import org.prebid.server.auction.model.IpAddress;
    +import org.prebid.server.cookie.UidsCookie;
    +import org.prebid.server.cookie.UidsCookieService;
    +import org.prebid.server.cookie.proto.Uids;
    +import org.prebid.server.deals.DealsProcessor;
    +import org.prebid.server.deals.model.DeepDebugLog;
    +import org.prebid.server.deals.model.TxnLog;
    +import org.prebid.server.exception.BlacklistedAccountException;
    +import org.prebid.server.exception.InvalidRequestException;
    +import org.prebid.server.exception.PreBidException;
    +import org.prebid.server.exception.UnauthorizedAccountException;
    +import org.prebid.server.execution.Timeout;
    +import org.prebid.server.execution.TimeoutFactory;
    +import org.prebid.server.geolocation.model.GeoInfo;
    +import org.prebid.server.hooks.execution.HookStageExecutor;
    +import org.prebid.server.hooks.execution.model.HookExecutionContext;
    +import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
    +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
    +import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl;
    +import org.prebid.server.metric.MetricName;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
    +import org.prebid.server.model.Endpoint;
    +import org.prebid.server.model.HttpRequestContext;
    +import org.prebid.server.privacy.ccpa.Ccpa;
    +import org.prebid.server.privacy.gdpr.model.TcfContext;
    +import org.prebid.server.privacy.model.Privacy;
    +import org.prebid.server.privacy.model.PrivacyContext;
    +import org.prebid.server.proto.openrtb.ext.request.ExtPublisher;
    +import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.TraceLevel;
    +import org.prebid.server.settings.ApplicationSettings;
    +import org.prebid.server.settings.model.Account;
    +import org.prebid.server.settings.model.AccountAuctionConfig;
    +import org.prebid.server.settings.model.AccountStatus;
    +import org.prebid.server.validation.RequestValidator;
    +import org.prebid.server.validation.model.ValidationResult;
    +
    +import java.time.Clock;
    +import java.util.ArrayList;
    +import java.util.Collections;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.function.UnaryOperator;
    +
    +import static java.util.Collections.emptyMap;
    +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.assertThatExceptionOfType;
    +import static org.assertj.core.groups.Tuple.tuple;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyLong;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.ArgumentMatchers.eq;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.never;
    +import static org.mockito.Mockito.verify;
    +import static org.mockito.Mockito.verifyZeroInteractions;
    +import static org.prebid.server.assertion.FutureAssertion.assertThat;
    +
    +public class Ortb2RequestFactoryTest extends VertxTest {
    +
    +    private static final List BLACKLISTED_ACCOUNTS = singletonList("bad_acc");
    +
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
    +    @Mock
    +    private UidsCookieService uidsCookieService;
    +    @Mock
    +    private RequestValidator requestValidator;
    +    @Mock
    +    private TimeoutResolver timeoutResolver;
    +    @Mock
    +    private TimeoutFactory timeoutFactory;
    +    @Mock
    +    private StoredRequestProcessor storedRequestProcessor;
    +    @Mock
    +    private ApplicationSettings applicationSettings;
    +    @Mock
    +    private IpAddressHelper ipAddressHelper;
    +    @Mock
    +    private HookStageExecutor hookStageExecutor;
    +    @Mock
    +    private DealsProcessor dealsProcessor;
    +
    +    private final Clock clock = Clock.systemDefaultZone();
    +
    +    private Ortb2RequestFactory target;
    +
    +    @Mock
    +    private Timeout timeout;
    +
    +    private BidRequest defaultBidRequest;
    +    private HttpRequestContext httpRequest;
    +    private HookExecutionContext hookExecutionContext;
    +
    +    @Before
    +    public void setUp() {
    +        defaultBidRequest = BidRequest.builder().build();
    +
    +        httpRequest = HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.empty())
    +                .build();
    +        hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
    +
    +        given(timeoutResolver.resolve(any())).willReturn(2000L);
    +        given(timeoutResolver.adjustTimeout(anyLong())).willReturn(1900L);
    +
    +        given(hookStageExecutor.executeEntrypointStage(any(), any(), any(), any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                        false,
    +                        EntrypointPayloadImpl.of(
    +                                invocation.getArgument(0),
    +                                invocation.getArgument(1),
    +                                invocation.getArgument(2)))));
    +
    +        given(hookStageExecutor.executeRawAuctionRequestStage(any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                        false,
    +                        AuctionRequestPayloadImpl.of(invocation.getArgument(0)))));
    +
    +        given(hookStageExecutor.executeProcessedAuctionRequestStage(any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                        false,
    +                        AuctionRequestPayloadImpl.of(invocation.getArgument(0)))));
    +
    +        given(dealsProcessor.populateDealsInfo(any()))
    +                .willAnswer(invocationOnMock -> Future.succeededFuture(invocationOnMock.getArgument(0)));
    +
    +        target = new Ortb2RequestFactory(
    +                false,
    +                BLACKLISTED_ACCOUNTS,
    +                uidsCookieService,
    +                requestValidator,
    +                timeoutResolver,
    +                timeoutFactory,
    +                storedRequestProcessor,
    +                applicationSettings,
    +                ipAddressHelper,
    +                hookStageExecutor,
    +                dealsProcessor,
    +                clock);
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnFailedFutureIfAccountIsEnforcedAndIdIsNotProvided() {
    +        // given
    +        target = new Ortb2RequestFactory(
    +                true,
    +                BLACKLISTED_ACCOUNTS,
    +                uidsCookieService,
    +                requestValidator,
    +                timeoutResolver,
    +                timeoutFactory,
    +                storedRequestProcessor,
    +                applicationSettings,
    +                ipAddressHelper,
    +                hookStageExecutor,
    +                dealsProcessor,
    +                clock);
    +
    +        given(storedRequestProcessor.processStoredRequests(any(), any()))
    +                .willReturn(Future.succeededFuture(givenBidRequest(identity())));
    +
    +        // when
    +        final Future future = target.fetchAccount(
    +                AuctionContext.builder()
    +                        .httpRequest(httpRequest)
    +                        .bidRequest(defaultBidRequest)
    +                        .build());
    +
    +        // then
    +        verify(applicationSettings, never()).getAccountById(any(), any());
    +
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause())
    +                .isInstanceOf(UnauthorizedAccountException.class)
    +                .hasMessage("Unauthorized account id: ");
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnFailedFutureIfAccountIsEnforcedAndFailedGetAccountById() {
    +        // given
    +        target = new Ortb2RequestFactory(
    +                true,
    +                BLACKLISTED_ACCOUNTS,
    +                uidsCookieService,
    +                requestValidator,
    +                timeoutResolver,
    +                timeoutFactory,
    +                storedRequestProcessor,
    +                applicationSettings,
    +                ipAddressHelper,
    +                hookStageExecutor,
    +                dealsProcessor,
    +                clock);
    +
    +        given(applicationSettings.getAccountById(any(), any()))
    +                .willReturn(Future.failedFuture(new PreBidException("Not found")));
    +
    +        final String accountId = "absentId";
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .app(App.builder()
    +                        .publisher(Publisher.builder().id(accountId).build())
    +                        .build()));
    +
    +        // when
    +        final Future future = target.fetchAccount(
    +                AuctionContext.builder()
    +                        .httpRequest(httpRequest)
    +                        .bidRequest(bidRequest)
    +                        .build());
    +
    +        // then
    +        verify(applicationSettings).getAccountById(eq(accountId), any());
    +
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause())
    +                .isInstanceOf(UnauthorizedAccountException.class)
    +                .hasMessage("Unauthorized account id: absentId");
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnFailedFutureIfAccountIsInactive() {
    +        // given
    +        final String accountId = "accountId";
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .app(App.builder()
    +                        .publisher(Publisher.builder().id(accountId).build())
    +                        .build()));
    +
    +        given(applicationSettings.getAccountById(any(), any()))
    +                .willReturn(Future.succeededFuture(Account.builder()
    +                        .id(accountId)
    +                        .status(AccountStatus.inactive)
    +                        .build()));
    +
    +        // when
    +        final Future future = target.fetchAccount(
    +                AuctionContext.builder().bidRequest(bidRequest).build());
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause())
    +                .isInstanceOf(UnauthorizedAccountException.class)
    +                .hasMessage("Account accountId is inactive");
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnFailedFutureWhenAccountIdIsBlacklisted() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .site(Site.builder()
    +                        .publisher(Publisher.builder().id("bad_acc").build())
    +                        .build()));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder().bidRequest(bidRequest).build());
    +
    +        // then
    +        assertThat(result.failed()).isTrue();
    +        assertThat(result.cause())
    +                .isInstanceOf(BlacklistedAccountException.class)
    +                .hasMessage("Prebid-server has blacklisted Account ID: bad_acc, please reach out to the prebid "
    +                        + "server host.");
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnAccountWithAccountIdTakenFromPublisherExt() {
    +        // given
    +        final String parentAccount = "parentAccount";
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .site(Site.builder()
    +                        .publisher(Publisher.builder().id("accountId")
    +                                .ext(ExtPublisher.of(ExtPublisherPrebid.of(parentAccount)))
    +                                .build())
    +                        .build()));
    +
    +        final Account account = Account.builder().id(parentAccount).build();
    +        given(applicationSettings.getAccountById(any(), any()))
    +                .willReturn(Future.succeededFuture(account));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder().bidRequest(bidRequest).build());
    +
    +        // then
    +        verify(applicationSettings).getAccountById(eq(parentAccount), any());
    +
    +        assertThat(result.result()).isSameAs(account);
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnAccountWithAccountIdTakenFromPublisherIdWhenExtIsNull() {
    +        // given
    +        final String accountId = "accountId";
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .site(Site.builder()
    +                        .publisher(Publisher.builder().id(accountId).ext(null).build())
    +                        .build()));
    +
    +        final Account account = Account.builder().id(accountId).build();
    +        given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder().bidRequest(bidRequest).build());
    +
    +        // then
    +        verify(applicationSettings).getAccountById(eq(accountId), any());
    +
    +        assertThat(result.result()).isSameAs(account);
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnAccountWithAccountIdTakenFromPublisherIdWhenExtPublisherPrebidIsNull() {
    +        // given
    +        final String accountId = "accountId";
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .site(Site.builder()
    +                        .publisher(Publisher.builder().id(accountId)
    +                                .ext(ExtPublisher.empty())
    +                                .build())
    +                        .build()));
    +
    +        final Account account = Account.builder().id(accountId).build();
    +        given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder().bidRequest(bidRequest).build());
    +
    +        // then
    +        verify(applicationSettings).getAccountById(eq(accountId), any());
    +
    +        assertThat(result.result()).isSameAs(account);
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnAccountWithAccountIdTakenFromPublisherIdWhenExtParentIsEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .site(Site.builder()
    +                        .publisher(Publisher.builder().id("accountId")
    +                                .ext(ExtPublisher.of(ExtPublisherPrebid.of("")))
    +                                .build())
    +                        .build()));
    +
    +        final Account account = Account.builder().id("accountId").build();
    +        given(applicationSettings.getAccountById(any(), any()))
    +                .willReturn(Future.succeededFuture(account));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder().bidRequest(bidRequest).build());
    +
    +        // then
    +        verify(applicationSettings).getAccountById(eq("accountId"), any());
    +
    +        assertThat(result.result()).isSameAs(account);
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnAccountWithAccountIdTakenFromAppPublisherId() {
    +        // given
    +        final String accountId = "accountId";
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder()
    +                        .publisher(Publisher.builder().id(accountId).build())
    +                        .build())
    +                .build();
    +
    +        final Account account = Account.builder().id(accountId).build();
    +        given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder().bidRequest(bidRequest).build());
    +
    +        // then
    +        verify(applicationSettings).getAccountById(eq(accountId), any());
    +
    +        assertThat(result.result()).isSameAs(account);
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnEmptyAccountIfNotFound() {
    +        // given
    +        final String parentAccount = "parentAccount";
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .site(Site.builder()
    +                        .publisher(Publisher.builder().id("accountId")
    +                                .ext(ExtPublisher.of(ExtPublisherPrebid.of(parentAccount)))
    +                                .build())
    +                        .build()));
    +
    +        given(applicationSettings.getAccountById(any(), any()))
    +                .willReturn(Future.failedFuture(new PreBidException("not found")));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder()
    +                        .httpRequest(httpRequest)
    +                        .bidRequest(bidRequest)
    +                        .build());
    +
    +        // then
    +        verify(applicationSettings).getAccountById(eq(parentAccount), any());
    +
    +        assertThat(result.result()).isEqualTo(Account.empty(parentAccount));
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnEmptyAccountIfExceptionOccurred() {
    +        // given
    +        final String accountId = "accountId";
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .site(Site.builder()
    +                        .publisher(Publisher.builder().id(accountId).build())
    +                        .build()));
    +
    +        given(applicationSettings.getAccountById(any(), any()))
    +                .willReturn(Future.failedFuture(new RuntimeException("error")));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder().bidRequest(bidRequest).build());
    +
    +        // then
    +        verify(applicationSettings).getAccountById(eq(accountId), any());
    +
    +        assertThat(result.result()).isEqualTo(Account.empty(accountId));
    +    }
    +
    +    @Test
    +    public void fetchAccountShouldReturnEmptyAccountIfItIsMissingInRequest() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        given(storedRequestProcessor.processStoredRequests(any(), any()))
    +                .willReturn(Future.succeededFuture(bidRequest));
    +
    +        given(applicationSettings.getAccountById(any(), any()))
    +                .willReturn(Future.failedFuture(new RuntimeException("error")));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder()
    +                        .httpRequest(httpRequest)
    +                        .bidRequest(bidRequest)
    +                        .build());
    +
    +        // then
    +        verifyZeroInteractions(applicationSettings);
    +
    +        assertThat(result.result()).isEqualTo(Account.empty(""));
    +    }
    +
    +    @Test
    +    public void shouldFetchAccountFromStoredIfStoredLookupIsTrueAndAccountIsNotFoundPreviously() {
    +        // given
    +        final BidRequest receivedBidRequest = givenBidRequest(identity());
    +
    +        final String accountId = "accountId";
    +        final BidRequest mergedBidRequest = givenBidRequest(builder -> builder
    +                .site(Site.builder()
    +                        .publisher(Publisher.builder().id(accountId).build())
    +                        .build()));
    +
    +        given(storedRequestProcessor.processStoredRequests(any(), any()))
    +                .willReturn(Future.succeededFuture(mergedBidRequest));
    +
    +        final Account fetchedAccount = Account.builder().id(accountId).status(AccountStatus.active).build();
    +        given(applicationSettings.getAccountById(any(), any()))
    +                .willReturn(Future.succeededFuture(fetchedAccount));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder()
    +                        .httpRequest(httpRequest)
    +                        .bidRequest(receivedBidRequest)
    +                        .build());
    +
    +        // then
    +        verify(storedRequestProcessor).processStoredRequests("", receivedBidRequest);
    +        verify(applicationSettings).getAccountById(eq(accountId), any());
    +
    +        assertThat(result.result()).isEqualTo(fetchedAccount);
    +    }
    +
    +    @Test
    +    public void shouldFetchAccountFromStoredAndReturnFailedFutureWhenAccountIdIsBlacklisted() {
    +        // given
    +        final BidRequest receivedBidRequest = givenBidRequest(identity());
    +
    +        final BidRequest mergedBidRequest = givenBidRequest(builder -> builder
    +                .site(Site.builder()
    +                        .publisher(Publisher.builder().id("bad_acc").build()).build()));
    +
    +        given(storedRequestProcessor.processStoredRequests(any(), any()))
    +                .willReturn(Future.succeededFuture(mergedBidRequest));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder()
    +                        .httpRequest(httpRequest)
    +                        .bidRequest(receivedBidRequest)
    +                        .build());
    +
    +        // then
    +        verify(storedRequestProcessor).processStoredRequests("", receivedBidRequest);
    +        verifyZeroInteractions(applicationSettings);
    +
    +        assertThat(result.failed()).isTrue();
    +        assertThat(result.cause())
    +                .isInstanceOf(BlacklistedAccountException.class)
    +                .hasMessage("Prebid-server has blacklisted Account ID: bad_acc, please reach out to the prebid "
    +                        + "server host.");
    +    }
    +
    +    @Test
    +    public void shouldFetchAccountFromStoredAndReturnFailedFutureIfValidIsEnforcedAndStoredLookupIsFailed() {
    +        // given
    +        target = new Ortb2RequestFactory(
    +                true,
    +                BLACKLISTED_ACCOUNTS,
    +                uidsCookieService,
    +                requestValidator,
    +                timeoutResolver,
    +                timeoutFactory,
    +                storedRequestProcessor,
    +                applicationSettings,
    +                ipAddressHelper,
    +                hookStageExecutor,
    +                dealsProcessor,
    +                clock);
    +
    +        final BidRequest receivedBidRequest = givenBidRequest(identity());
    +        given(storedRequestProcessor.processStoredRequests(any(), any()))
    +                .willReturn(Future.failedFuture(new RuntimeException("error")));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder()
    +                        .httpRequest(httpRequest)
    +                        .bidRequest(receivedBidRequest)
    +                        .build());
    +
    +        // then
    +        verify(storedRequestProcessor).processStoredRequests("", receivedBidRequest);
    +        verifyZeroInteractions(applicationSettings);
    +
    +        assertThat(result.failed()).isTrue();
    +        assertThat(result.cause()).hasMessage("error");
    +    }
    +
    +    @Test
    +    public void shouldFetchAccountFromStoredAndReturnEmptyAccountIfStoredLookupIsFailed() {
    +        // given
    +        final BidRequest receivedBidRequest = givenBidRequest(identity());
    +        given(storedRequestProcessor.processStoredRequests(any(), any()))
    +                .willReturn(Future.failedFuture(new RuntimeException("error")));
    +
    +        // when
    +        final Future result = target.fetchAccount(
    +                AuctionContext.builder()
    +                        .httpRequest(httpRequest)
    +                        .bidRequest(receivedBidRequest)
    +                        .build());
    +
    +        // then
    +        verify(storedRequestProcessor).processStoredRequests("", receivedBidRequest);
    +        verifyZeroInteractions(applicationSettings);
    +
    +        assertThat(result.failed()).isTrue();
    +        assertThat(result.cause()).hasMessage("error");
    +    }
    +
    +    @Test
    +    public void fetchAccountWithoutStoredRequestLookupShouldNeverCallStoredProcessor() {
    +        // when
    +        target.fetchAccountWithoutStoredRequestLookup(
    +                AuctionContext.builder()
    +                        .httpRequest(httpRequest)
    +                        .bidRequest(givenBidRequest(identity()))
    +                        .build());
    +
    +        // then
    +        verifyZeroInteractions(storedRequestProcessor);
    +    }
    +
    +    @Test
    +    public void createAuctionContextShouldReturnExpectedAuctionContext() {
    +        // when
    +        final AuctionContext result = target.createAuctionContext(Endpoint.openrtb2_auction, MetricName.openrtb2app);
    +
    +        // then
    +        assertThat(result).isEqualTo(AuctionContext.builder()
    +                .requestTypeMetric(MetricName.openrtb2app)
    +                .prebidErrors(new ArrayList<>())
    +                .debugWarnings(new ArrayList<>())
    +                .hookExecutionContext(hookExecutionContext)
    +                .debugContext(DebugContext.empty())
    +                .requestRejected(false)
    +                .txnLog(TxnLog.create())
    +                .debugHttpCalls(emptyMap())
    +                .build());
    +    }
    +
    +    @Test
    +    public void enrichAuctionContextShouldReturnExpectedAuctionContext() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .tmax(1000L)
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .debug(1)
    +                        .trace(TraceLevel.basic)
    +                        .build()))
    +                .build();
    +
    +        final long resolvedTimeout = 200L;
    +        final long adjustedTimeout = 250L;
    +        given(timeoutResolver.resolve(anyLong())).willReturn(resolvedTimeout);
    +        given(timeoutResolver.adjustTimeout(anyLong())).willReturn(adjustedTimeout);
    +        given(timeoutFactory.create(anyLong(), anyLong())).willReturn(timeout);
    +
    +        final UidsCookie uidsCookie = new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper);
    +        given(uidsCookieService.parseFromRequest(any(HttpRequestContext.class))).willReturn(uidsCookie);
    +
    +        // when
    +        final AuctionContext result = target.enrichAuctionContext(
    +                AuctionContext.builder()
    +                        .requestTypeMetric(MetricName.openrtb2app)
    +                        .prebidErrors(new ArrayList<>())
    +                        .debugWarnings(new ArrayList<>())
    +                        .hookExecutionContext(hookExecutionContext)
    +                        .debugContext(DebugContext.empty())
    +                        .txnLog(TxnLog.create())
    +                        .debugHttpCalls(emptyMap())
    +                        .build(),
    +                httpRequest,
    +                bidRequest,
    +                100);
    +
    +        // then
    +        verify(timeoutResolver).resolve(1000L);
    +        verify(timeoutResolver).adjustTimeout(resolvedTimeout);
    +        verify(timeoutFactory).create(100, adjustedTimeout);
    +
    +        verify(uidsCookieService).parseFromRequest(httpRequest);
    +
    +        assertThat(result).isEqualToComparingFieldByFieldRecursively(AuctionContext.builder()
    +                .httpRequest(httpRequest)
    +                .uidsCookie(uidsCookie)
    +                .bidRequest(bidRequest)
    +                .requestTypeMetric(MetricName.openrtb2app)
    +                .timeout(timeout)
    +                .prebidErrors(new ArrayList<>())
    +                .debugWarnings(new ArrayList<>())
    +                .hookExecutionContext(hookExecutionContext)
    +                .debugContext(DebugContext.of(true, TraceLevel.basic))
    +                .txnLog(TxnLog.create())
    +                .deepDebugLog(DeepDebugLog.create(false, clock))
    +                .debugHttpCalls(new HashMap<>())
    +                .build());
    +    }
    +
    +    @Test
    +    public void enrichAuctionContextShouldSetDebugOnWhenTestIsOne() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .test(1)
    +                .build();
    +
    +        // when
    +        final AuctionContext result = target.enrichAuctionContext(
    +                AuctionContext.builder()
    +                        .debugContext(DebugContext.empty())
    +                        .build(),
    +                httpRequest,
    +                bidRequest,
    +                100);
    +
    +        // then
    +        assertThat(result.getDebugContext()).isEqualTo(DebugContext.of(true, null));
    +    }
    +
    +    @Test
    +    public void enrichAuctionContextShouldSetDebugOff() {
    +        // when
    +        final AuctionContext result = target.enrichAuctionContext(
    +                AuctionContext.builder()
    +                        .debugContext(DebugContext.empty())
    +                        .build(),
    +                httpRequest,
    +                BidRequest.builder().build(),
    +                100);
    +
    +        // then
    +        assertThat(result.getDebugContext()).isEqualTo(DebugContext.empty());
    +    }
    +
    +    @Test
    +    public void enrichAuctionContextShouldReturnAuctionContextWithDeepDebugLogWhenDeepDebugIsOff() {
    +        // when
    +        final AuctionContext auctionContext = target.enrichAuctionContext(
    +                AuctionContext.builder().build(),
    +                httpRequest,
    +                BidRequest.builder().build(),
    +                100);
    +
    +        // then
    +        assertThat(auctionContext.getDeepDebugLog()).isNotNull().returns(false, DeepDebugLog::isDeepDebugEnabled);
    +    }
    +
    +    @Test
    +    public void enrichAuctionContextShouldReturnAuctionContextWithDeepDebugLogWhenDeepDebugIsOn() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .ext(ExtRequest.of(
    +                        ExtRequestPrebid.builder().trace(TraceLevel.verbose).build()))
    +                .build();
    +
    +        // when
    +        final AuctionContext auctionContext = target.enrichAuctionContext(
    +                AuctionContext.builder().build(),
    +                httpRequest,
    +                bidRequest,
    +                100);
    +
    +        // then
    +        assertThat(auctionContext.getDeepDebugLog()).isNotNull().returns(true, DeepDebugLog::isDeepDebugEnabled);
    +    }
    +
    +    @Test
    +    public void validateRequestShouldThrowInvalidRequestExceptionIfRequestIsInvalid() {
    +        // given
    +        given(requestValidator.validate(any())).willReturn(ValidationResult.error("error"));
    +
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when and then
    +        assertThatExceptionOfType(InvalidRequestException.class)
    +                .isThrownBy(() -> target.validateRequest(bidRequest))
    +                .withMessage("error");
    +
    +        verify(requestValidator).validate(bidRequest);
    +    }
    +
    +    @Test
    +    public void validateRequestShouldReturnSameBidRequest() {
    +        // given
    +        given(requestValidator.validate(any())).willReturn(ValidationResult.success());
    +
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final BidRequest result = target.validateRequest(bidRequest);
    +
    +        // then
    +        verify(requestValidator).validate(bidRequest);
    +
    +        assertThat(result).isSameAs(bidRequest);
    +    }
    +
    +    @Test
    +    public void enrichBidRequestWithAccountAndPrivacyDataShouldReturnIntegrationFromAccount() {
    +        // given
    +        final String accountId = "accId";
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder
    +                .imp(new ArrayList<>())
    +                .site(Site.builder()
    +                        .publisher(Publisher.builder().id(accountId).build())
    +                        .build())
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder().build())));
    +
    +        final PrivacyContext privacyContext = PrivacyContext.of(
    +                Privacy.of("", "", Ccpa.EMPTY, 0),
    +                TcfContext.empty(),
    +                "ip");
    +
    +        final String integration = "integration";
    +        final Account account = Account.builder()
    +                .id(accountId)
    +                .auction(AccountAuctionConfig.builder()
    +                        .defaultIntegration(integration)
    +                        .build())
    +                .build();
    +        given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account));
    +
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .bidRequest(bidRequest)
    +                .account(account)
    +                .privacyContext(privacyContext)
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(auctionContext);
    +
    +        // then
    +        assertThat(result)
    +                .extracting(auctionBidRequest -> auctionBidRequest.getExt().getPrebid().getIntegration())
    +                .containsOnly(integration);
    +    }
    +
    +    @Test
    +    public void enrichBidRequestWithAccountAndPrivacyDataShouldAddCountryFromPrivacy() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +        final PrivacyContext privacyContext = PrivacyContext.of(
    +                Privacy.of("", "", Ccpa.EMPTY, 0),
    +                TcfContext.builder()
    +                        .geoInfo(GeoInfo.builder().vendor("v").country("ua").build())
    +                        .build(),
    +                null);
    +
    +        final Account account = Account.empty("id");
    +
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .bidRequest(bidRequest)
    +                .account(account)
    +                .privacyContext(privacyContext)
    +                .build();
    +
    +        // when
    +        final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(auctionContext);
    +
    +        // then
    +        assertThat(Collections.singleton(result))
    +                .extracting(BidRequest::getDevice)
    +                .extracting(Device::getGeo)
    +                .extracting(Geo::getCountry)
    +                .containsOnly("ua");
    +    }
    +
    +    @Test
    +    public void enrichBidRequestWithAccountAndPrivacyDataShouldAddIpAddressV4FromPrivacy() {
    +        // given
    +        given(ipAddressHelper.toIpAddress(anyString())).willReturn(IpAddress.of("ignored", IpAddress.IP.v4));
    +
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +        final PrivacyContext privacyContext = PrivacyContext.of(
    +                Privacy.of("", "", Ccpa.EMPTY, 0),
    +                TcfContext.builder().build(),
    +                "ipv4");
    +
    +        final Account account = Account.empty("id");
    +
    +        // when
    +        final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(
    +                AuctionContext.builder()
    +                        .bidRequest(bidRequest)
    +                        .account(account)
    +                        .privacyContext(privacyContext)
    +                        .build());
    +
    +        // then
    +        assertThat(Collections.singleton(result))
    +                .extracting(BidRequest::getDevice)
    +                .extracting(Device::getIp, Device::getIpv6)
    +                .containsOnly(tuple("ipv4", null));
    +    }
    +
    +    @Test
    +    public void enrichBidRequestWithAccountAndPrivacyDataShouldAddIpAddressV6FromPrivacy() {
    +        // given
    +        given(ipAddressHelper.toIpAddress(anyString())).willReturn(IpAddress.of("ignored", IpAddress.IP.v6));
    +
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +        final PrivacyContext privacyContext = PrivacyContext.of(
    +                Privacy.of("", "", Ccpa.EMPTY, 0),
    +                TcfContext.builder().build(),
    +                "ipv6");
    +
    +        final Account account = Account.empty("id");
    +
    +        // when
    +        final BidRequest result = target.enrichBidRequestWithAccountAndPrivacyData(
    +                AuctionContext.builder()
    +                        .bidRequest(bidRequest)
    +                        .account(account)
    +                        .privacyContext(privacyContext)
    +                        .build());
    +
    +        // then
    +        assertThat(Collections.singleton(result))
    +                .extracting(BidRequest::getDevice)
    +                .extracting(Device::getIp, Device::getIpv6)
    +                .containsOnly(tuple(null, "ipv6"));
    +    }
    +
    +    @Test
    +    public void executeEntrypointHooksShouldReturnExpectedHttpRequest() {
    +        // given
    +        final RoutingContext routingContext = mock(RoutingContext.class);
    +        final HttpServerRequest httpServerRequest = mock(HttpServerRequest.class);
    +
    +        given(routingContext.request()).willReturn(httpServerRequest);
    +        given(routingContext.queryParams()).willReturn(MultiMap.caseInsensitiveMultiMap().add("test", "test"));
    +
    +        given(httpServerRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap());
    +        given(httpServerRequest.absoluteURI()).willReturn("absoluteUri");
    +        given(httpServerRequest.scheme()).willReturn("https");
    +        given(httpServerRequest.remoteAddress()).willReturn(new SocketAddressImpl(1234, "host"));
    +
    +        final CaseInsensitiveMultiMap updatedQueryParam = CaseInsensitiveMultiMap.builder()
    +                .add("urloverride", "overriddendomain.com")
    +                .build();
    +        final CaseInsensitiveMultiMap headerParams = CaseInsensitiveMultiMap.builder()
    +                .add("DHT", "1")
    +                .build();
    +        given(hookStageExecutor.executeEntrypointStage(any(), any(), any(), any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                        false,
    +                        EntrypointPayloadImpl.of(
    +                                updatedQueryParam,
    +                                headerParams,
    +                                bidRequestToString(BidRequest.builder()
    +                                        .app(App.builder().bundle("org.company.application").build())
    +                                        .build())))));
    +
    +        final AuctionContext auctionContext =
    +                AuctionContext.builder().hookExecutionContext(hookExecutionContext).build();
    +
    +        // when
    +        final Future result = target.executeEntrypointHooks(routingContext, "", auctionContext);
    +
    +        // then
    +        final HttpRequestContext httpRequest = result.result();
    +        assertThat(httpRequest.getAbsoluteUri()).isEqualTo("absoluteUri");
    +        assertThat(httpRequest.getQueryParams()).isSameAs(updatedQueryParam);
    +        assertThat(httpRequest.getHeaders()).isSameAs(headerParams);
    +        assertThat(httpRequest.getBody()).isEqualTo("{\"app\":{\"bundle\":\"org.company.application\"}}");
    +        assertThat(httpRequest.getScheme()).isEqualTo("https");
    +        assertThat(httpRequest.getRemoteHost()).isEqualTo("host");
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfEntrypointHooksRejectedRequest() {
    +        // given
    +        final RoutingContext routingContext = mock(RoutingContext.class);
    +        final HttpServerRequest httpServerRequest = mock(HttpServerRequest.class);
    +
    +        given(routingContext.request()).willReturn(httpServerRequest);
    +        given(routingContext.queryParams()).willReturn(MultiMap.caseInsensitiveMultiMap());
    +        given(httpServerRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap());
    +
    +        given(hookStageExecutor.executeEntrypointStage(any(), any(), any(), any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(true, null)));
    +
    +        final AuctionContext auctionContext =
    +                AuctionContext.builder().hookExecutionContext(hookExecutionContext).build();
    +
    +        // when
    +        final Future result = target.executeEntrypointHooks(routingContext, "", auctionContext);
    +
    +        // then
    +        assertThat(result).isFailed();
    +        assertThat(result.cause()).isInstanceOf(Ortb2RequestFactory.RejectedRequestException.class);
    +        assertThat(((Ortb2RequestFactory.RejectedRequestException) result.cause()).getAuctionContext())
    +                .isEqualTo(auctionContext);
    +    }
    +
    +    @Test
    +    public void shouldUseBidRequestModifiedByRawAuctionRequestHooks() {
    +        // given
    +        final BidRequest modifiedBidRequest = BidRequest.builder()
    +                .app(App.builder().bundle("org.company.application").build())
    +                .build();
    +        given(hookStageExecutor.executeRawAuctionRequestStage(any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                        false, AuctionRequestPayloadImpl.of(modifiedBidRequest))));
    +
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .bidRequest(BidRequest.builder().site(Site.builder().build()).build())
    +                .hookExecutionContext(hookExecutionContext)
    +                .build();
    +
    +        // when
    +        final Future result = target.executeRawAuctionRequestHooks(auctionContext);
    +
    +        // then
    +        assertThat(result.result()).isEqualTo(modifiedBidRequest);
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfRawAuctionRequestHookRejectedRequest() {
    +        // given
    +        given(hookStageExecutor.executeRawAuctionRequestStage(any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(true, null)));
    +
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .hookExecutionContext(hookExecutionContext)
    +                .build();
    +
    +        // when
    +        final Future result = target.executeRawAuctionRequestHooks(auctionContext);
    +
    +        // then
    +        assertThat(result).isFailed();
    +        assertThat(result.cause()).isInstanceOf(Ortb2RequestFactory.RejectedRequestException.class);
    +        assertThat(((Ortb2RequestFactory.RejectedRequestException) result.cause()).getAuctionContext())
    +                .isEqualTo(auctionContext);
    +    }
    +
    +    @Test
    +    public void shouldUseBidRequestModifiedByProcessedAuctionRequestHooks() {
    +        // given
    +        final BidRequest modifiedBidRequest = BidRequest.builder()
    +                .app(App.builder().bundle("org.company.application").build())
    +                .build();
    +        given(hookStageExecutor.executeProcessedAuctionRequestStage(any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
    +                        false, AuctionRequestPayloadImpl.of(modifiedBidRequest))));
    +
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .bidRequest(BidRequest.builder().site(Site.builder().build()).build())
    +                .hookExecutionContext(hookExecutionContext)
    +                .build();
    +
    +        // when
    +        final Future result = target.executeProcessedAuctionRequestHooks(auctionContext);
    +
    +        // then
    +        assertThat(result.result()).isEqualTo(modifiedBidRequest);
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfProcessedAuctionRequestHookRejectedRequest() {
    +        // given
    +        given(hookStageExecutor.executeProcessedAuctionRequestStage(any()))
    +                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(true, null)));
    +
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .hookExecutionContext(hookExecutionContext)
    +                .build();
    +
    +        // when
    +        final Future result = target.executeProcessedAuctionRequestHooks(auctionContext);
    +
    +        // then
    +        assertThat(result).isFailed();
    +        assertThat(result.cause()).isInstanceOf(Ortb2RequestFactory.RejectedRequestException.class);
    +        assertThat(((Ortb2RequestFactory.RejectedRequestException) result.cause()).getAuctionContext())
    +                .isEqualTo(auctionContext);
    +    }
    +
    +    @Test
    +    public void restoreResultFromRejectionShouldReturnSuccessfulFutureWhenRequestRejected() {
    +        // given
    +        final AuctionContext auctionContext = AuctionContext.builder()
    +                .requestRejected(false)
    +                .build();
    +
    +        // when
    +        final Future result =
    +                target.restoreResultFromRejection(new Ortb2RequestFactory.RejectedRequestException(auctionContext));
    +
    +        // then
    +        assertThat(result).succeededWith(AuctionContext.builder()
    +                .requestRejected(true)
    +                .build());
    +    }
    +
    +    @Test
    +    public void restoreResultFromRejectionShouldReturnFailedFutureWhenNotRejectionException() {
    +        // given
    +        final InvalidRequestException exception = new InvalidRequestException("Request is not really valid");
    +
    +        // when
    +        final Future result = target.restoreResultFromRejection(exception);
    +
    +        // then
    +        assertThat(result).isFailed().isSameAs(exception);
    +    }
    +
    +    private static String bidRequestToString(BidRequest bidRequest) {
    +        try {
    +            return mapper.writeValueAsString(bidRequest);
    +        } catch (JsonProcessingException e) {
    +            throw new RuntimeException(e);
    +        }
    +    }
    +
    +    private static BidRequest givenBidRequest(UnaryOperator requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()).build();
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java
    new file mode 100644
    index 00000000000..ee9e29b52f1
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java
    @@ -0,0 +1,387 @@
    +package org.prebid.server.auction.requestfactory;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Content;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.request.User;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.request.video.BidRequestVideo;
    +import com.iab.openrtb.request.video.PodError;
    +import io.vertx.core.Future;
    +import io.vertx.core.MultiMap;
    +import io.vertx.core.http.HttpServerRequest;
    +import io.vertx.core.net.impl.SocketAddressImpl;
    +import io.vertx.ext.web.RoutingContext;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
    +import org.mockito.stubbing.Answer;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.auction.PriceGranularity;
    +import org.prebid.server.auction.PrivacyEnforcementService;
    +import org.prebid.server.auction.TimeoutResolver;
    +import org.prebid.server.auction.VideoStoredRequestProcessor;
    +import org.prebid.server.auction.model.AuctionContext;
    +import org.prebid.server.auction.model.WithPodErrors;
    +import org.prebid.server.exception.InvalidRequestException;
    +import org.prebid.server.metric.MetricName;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
    +import org.prebid.server.model.Endpoint;
    +import org.prebid.server.model.HttpRequestContext;
    +import org.prebid.server.privacy.ccpa.Ccpa;
    +import org.prebid.server.privacy.gdpr.model.TcfContext;
    +import org.prebid.server.privacy.model.Privacy;
    +import org.prebid.server.privacy.model.PrivacyContext;
    +import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheVastxml;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.Objects;
    +
    +import static java.util.Collections.emptySet;
    +import static java.util.Collections.singletonList;
    +import static org.apache.commons.lang3.StringUtils.EMPTY;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyLong;
    +import static org.mockito.ArgumentMatchers.argThat;
    +import static org.mockito.ArgumentMatchers.eq;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.doAnswer;
    +import static org.mockito.Mockito.verify;
    +
    +public class VideoRequestFactoryTest extends VertxTest {
    +
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
    +    @Mock
    +    private Ortb2RequestFactory ortb2RequestFactory;
    +    @Mock
    +    private Ortb2ImplicitParametersResolver paramsResolver;
    +    @Mock
    +    private VideoStoredRequestProcessor videoStoredRequestProcessor;
    +    @Mock
    +    private PrivacyEnforcementService privacyEnforcementService;
    +
    +    private VideoRequestFactory target;
    +
    +    @Mock
    +    private RoutingContext routingContext;
    +    @Mock
    +    private HttpServerRequest httpServerRequest;
    +    @Mock
    +    private TimeoutResolver timeoutResolver;
    +
    +    @Before
    +    public void setUp() {
    +        given(ortb2RequestFactory.createAuctionContext(any(), eq(MetricName.video)))
    +                .willReturn(AuctionContext.builder().build());
    +        given(ortb2RequestFactory.executeEntrypointHooks(any(), any(), any()))
    +                .willAnswer(invocation -> toHttpRequest(invocation.getArgument(0), invocation.getArgument(1)));
    +        given(ortb2RequestFactory.restoreResultFromRejection(any()))
    +                .willAnswer(invocation -> Future.failedFuture((Throwable) invocation.getArgument(0)));
    +
    +        given(routingContext.request()).willReturn(httpServerRequest);
    +        given(routingContext.queryParams()).willReturn(MultiMap.caseInsensitiveMultiMap());
    +        given(httpServerRequest.remoteAddress()).willReturn(new SocketAddressImpl(1234, "host"));
    +        given(httpServerRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap());
    +
    +        final PrivacyContext defaultPrivacyContext = PrivacyContext.of(
    +                Privacy.of("0", EMPTY, Ccpa.EMPTY, 0),
    +                TcfContext.empty());
    +        given(privacyEnforcementService.contextFromBidRequest(any()))
    +                .willReturn(Future.succeededFuture(defaultPrivacyContext));
    +
    +        given(ortb2RequestFactory.populateDealsInfo(any()))
    +                .willAnswer(invocationOnMock -> Future.succeededFuture(invocationOnMock.getArgument(0)));
    +
    +        target = new VideoRequestFactory(
    +                Integer.MAX_VALUE,
    +                false,
    +                ortb2RequestFactory,
    +                paramsResolver,
    +                videoStoredRequestProcessor,
    +                privacyEnforcementService,
    +                timeoutResolver,
    +                jacksonMapper);
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfRequestBodyIsMissing() {
    +        // given
    +        given(routingContext.getBodyAsString()).willReturn(null);
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause())
    +                .isInstanceOf(InvalidRequestException.class)
    +                .hasMessage("Incoming request has no body");
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfStoredRequestIsEnforcedAndIdIsNotProvided() throws JsonProcessingException {
    +        // given
    +        given(routingContext.getBodyAsString())
    +                .willReturn(mapper.writeValueAsString(BidRequestVideo.builder().build()));
    +        given(routingContext.request().headers()).willReturn(MultiMap.caseInsensitiveMultiMap()
    +                .add(HttpUtil.USER_AGENT_HEADER, "123"));
    +        target = new VideoRequestFactory(
    +                Integer.MAX_VALUE,
    +                true,
    +                ortb2RequestFactory,
    +                paramsResolver,
    +                videoStoredRequestProcessor,
    +                privacyEnforcementService,
    +                timeoutResolver,
    +                jacksonMapper);
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause())
    +                .isInstanceOf(InvalidRequestException.class)
    +                .hasMessage("Unable to find required stored request id");
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() {
    +        // given
    +        target = new VideoRequestFactory(
    +                2,
    +                true,
    +                ortb2RequestFactory,
    +                paramsResolver,
    +                videoStoredRequestProcessor,
    +                privacyEnforcementService,
    +                timeoutResolver,
    +                jacksonMapper);
    +
    +        given(routingContext.getBodyAsString()).willReturn("body");
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause())
    +                .isInstanceOf(InvalidRequestException.class)
    +                .hasMessage("Request size exceeded max size of 2 bytes.");
    +    }
    +
    +    @Test
    +    public void shouldReturnFailedFutureIfRequestBodyCouldNotBeParsed() {
    +        // given
    +        given(routingContext.getBodyAsString()).willReturn("body");
    +
    +        // when
    +        final Future future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause()).isInstanceOf(InvalidRequestException.class);
    +        assertThat(((InvalidRequestException) future.cause()).getMessages()).hasSize(1)
    +                .element(0).asString().startsWith("Failed to decode:");
    +    }
    +
    +    @Test
    +    public void shouldUseHeadersModifiedByEntrypointHooks() throws JsonProcessingException {
    +        // given
    +        final BidRequestVideo requestVideo = BidRequestVideo.builder().build();
    +        final String body = mapper.writeValueAsString(requestVideo);
    +        given(routingContext.getBodyAsString()).willReturn(body);
    +
    +        given(routingContext.request().headers()).willReturn(MultiMap.caseInsensitiveMultiMap()
    +                .add(HttpUtil.USER_AGENT_HEADER, "user-agent-123"));
    +
    +        doAnswer(invocation -> Future.succeededFuture(HttpRequestContext.builder()
    +                .headers(CaseInsensitiveMultiMap.builder()
    +                        .add(HttpUtil.USER_AGENT_HEADER, "user-agent-456")
    +                        .build())
    +                .body(body)
    +                .build()))
    +                .when(ortb2RequestFactory)
    +                .executeEntrypointHooks(any(), any(), any());
    +
    +        final WithPodErrors emptyMergeObject = WithPodErrors.of(null, null);
    +        given(videoStoredRequestProcessor.processVideoRequest(any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(emptyMergeObject));
    +
    +        // when
    +        target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        verify(videoStoredRequestProcessor).processVideoRequest(any(), any(), any(), eq(BidRequestVideo.builder()
    +                .device(Device.builder()
    +                        .ua("user-agent-456")
    +                        .build())
    +                .build()));
    +    }
    +
    +    @Test
    +    public void shouldReturnExpectedResultAndReturnErrors() throws JsonProcessingException {
    +        // given
    +        final Content content = Content.builder()
    +                .len(900)
    +                .livestream(0)
    +                .build();
    +        final Imp expectedImp1 = Imp.builder()
    +                .id("123_0")
    +                .video(Video.builder()
    +                        .mimes(singletonList("mime"))
    +                        .maxduration(100)
    +                        .protocols(singletonList(123))
    +                        .build())
    +                .build();
    +        final Imp expectedImp2 = Imp.builder()
    +                .id("123_1")
    +                .video(Video.builder()
    +                        .mimes(singletonList("mime"))
    +                        .maxduration(100)
    +                        .protocols(singletonList(123))
    +                        .build())
    +                .build();
    +        final ExtRequestPrebid ext = ExtRequestPrebid.builder()
    +                .cache(ExtRequestPrebidCache.of(null, ExtRequestPrebidCacheVastxml.of(null, null), null))
    +                .targeting(ExtRequestTargeting.builder()
    +                        .pricegranularity(mapper.valueToTree(PriceGranularity.createFromString("med")))
    +                        .includebidderkeys(true)
    +                        .includebrandcategory(ExtIncludeBrandCategory.of(null, null, false))
    +                        .build())
    +                .build();
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .id("bid_id")
    +                .imp(Arrays.asList(expectedImp1, expectedImp2))
    +                .user(User.builder().buyeruid("appnexus").yob(123).gender("gender").keywords("keywords").build())
    +                .site(Site.builder().id("siteId").content(content).build())
    +                .bcat(singletonList("bcat"))
    +                .badv(singletonList("badv"))
    +                .cur(singletonList("USD"))
    +                .tmax(0L)
    +                .ext(ExtRequest.of(ext))
    +                .build();
    +
    +        final BidRequestVideo requestVideo = BidRequestVideo.builder().device(
    +                Device.builder().ua("123").build()).build();
    +        given(routingContext.getBodyAsString()).willReturn(mapper.writeValueAsString(requestVideo));
    +
    +        final List podErrors = singletonList(PodError.of(1, 1, singletonList("TEST")));
    +        givenBidRequest(bidRequest, podErrors);
    +
    +        // when
    +        final Future> result = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        verify(routingContext).getBodyAsString();
    +        verify(videoStoredRequestProcessor).processVideoRequest("", null, emptySet(), requestVideo);
    +        verify(ortb2RequestFactory).createAuctionContext(any(), eq(MetricName.video));
    +        verify(ortb2RequestFactory).enrichAuctionContext(any(), any(), eq(bidRequest), eq(0L));
    +        verify(ortb2RequestFactory).fetchAccountWithoutStoredRequestLookup(
    +                eq(AuctionContext.builder().bidRequest(bidRequest).build()));
    +        verify(ortb2RequestFactory).validateRequest(bidRequest);
    +        verify(paramsResolver).resolve(eq(bidRequest), any(), eq(timeoutResolver), eq(Endpoint.openrtb2_video.value()));
    +        verify(ortb2RequestFactory).enrichBidRequestWithAccountAndPrivacyData(
    +                argThat(context -> Objects.equals(context.getBidRequest(), bidRequest)));
    +        assertThat(result.result().getData().getBidRequest()).isEqualTo(bidRequest);
    +        assertThat(result.result().getPodErrors()).isEqualTo(podErrors);
    +    }
    +
    +    @Test
    +    public void shouldReplaceDeviceUaWithUserAgentHeaderIfPresented() throws JsonProcessingException {
    +        // given
    +        final BidRequestVideo requestVideo = BidRequestVideo.builder().build();
    +        given(routingContext.getBodyAsString()).willReturn(mapper.writeValueAsString(requestVideo));
    +        given(routingContext.request().headers()).willReturn(MultiMap.caseInsensitiveMultiMap()
    +                .add(HttpUtil.USER_AGENT_HEADER, "user-agent-123"));
    +
    +        final WithPodErrors emptyMergeObject = WithPodErrors.of(null, null);
    +        given(videoStoredRequestProcessor.processVideoRequest(any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(emptyMergeObject));
    +
    +        // when
    +        target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        verify(videoStoredRequestProcessor).processVideoRequest(any(), any(), any(), eq(BidRequestVideo.builder()
    +                .device(Device.builder()
    +                        .ua("user-agent-123")
    +                        .build())
    +                .build()));
    +    }
    +
    +    @Test
    +    public void shouldReturnErrorIfDeviceUaAndUserAgentHeaderIsEmpty() throws JsonProcessingException {
    +        // given
    +        final BidRequestVideo requestVideo = BidRequestVideo.builder().build();
    +        given(routingContext.getBodyAsString()).willReturn(mapper.writeValueAsString(requestVideo));
    +        given(httpServerRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap());
    +
    +        // when
    +        Future> future = target.fromRequest(routingContext, 0L);
    +
    +        // then
    +        assertThat(future.failed()).isTrue();
    +        assertThat(future.cause())
    +                .isInstanceOf(InvalidRequestException.class)
    +                .hasMessage("Device.UA and User-Agent Header is not presented");
    +    }
    +
    +    private void givenBidRequest(BidRequest bidRequest, List podErrors) {
    +        given(videoStoredRequestProcessor.processVideoRequest(any(), any(), any(), any()))
    +                .willReturn(Future.succeededFuture(WithPodErrors.of(bidRequest, podErrors)));
    +        given(ortb2RequestFactory.enrichAuctionContext(any(), any(), any(), anyLong()))
    +                .willAnswer(invocationOnMock -> AuctionContext.builder()
    +                        .bidRequest((BidRequest) invocationOnMock.getArguments()[2])
    +                        .build());
    +        given(ortb2RequestFactory.fetchAccountWithoutStoredRequestLookup(any())).willReturn(Future.succeededFuture());
    +
    +        given(ortb2RequestFactory.validateRequest(any())).willAnswer(answerWithFirstArgument());
    +        given(paramsResolver.resolve(any(), any(), any(), any()))
    +                .willAnswer(answerWithFirstArgument());
    +
    +        given(ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(any()))
    +                .willAnswer(invocation -> ((AuctionContext) invocation.getArgument(0)).getBidRequest());
    +        given(ortb2RequestFactory.executeProcessedAuctionRequestHooks(any()))
    +                .willAnswer(invocation -> Future.succeededFuture(
    +                        ((AuctionContext) invocation.getArgument(0)).getBidRequest()));
    +    }
    +
    +    private Answer answerWithFirstArgument() {
    +        return invocationOnMock -> invocationOnMock.getArguments()[0];
    +    }
    +
    +    private static Future toHttpRequest(RoutingContext routingContext, String body) {
    +        return Future.succeededFuture(HttpRequestContext.builder()
    +                .absoluteUri(routingContext.request().absoluteURI())
    +                .queryParams(toCaseInsensitiveMultiMap(routingContext.queryParams()))
    +                .headers(toCaseInsensitiveMultiMap(routingContext.request().headers()))
    +                .body(body)
    +                .scheme(routingContext.request().scheme())
    +                .remoteHost(routingContext.request().remoteAddress().host())
    +                .build());
    +    }
    +
    +    private static CaseInsensitiveMultiMap toCaseInsensitiveMultiMap(MultiMap originalMap) {
    +        final CaseInsensitiveMultiMap.Builder mapBuilder = CaseInsensitiveMultiMap.builder();
    +        originalMap.entries().forEach(entry -> mapBuilder.add(entry.getKey(), entry.getValue()));
    +
    +        return mapBuilder.build();
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java
    index 8307e4d4c10..0bfe687c68f 100644
    --- a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java
    +++ b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java
    @@ -18,24 +18,18 @@ public class BidderCatalogTest {
         @Rule
         public final MockitoRule mockitoRule = MockitoJUnit.rule();
     
    -    @Mock
    -    private Usersyncer usersyncer;
         @Mock
         private Bidder bidder;
    -    @Mock
    -    private Adapter adapter;
     
    -    private BidderDeps bidderDeps;
         private BidderCatalog bidderCatalog;
     
         @Test
         public void isValidNameShouldReturnTrueForKnownBidder() {
             // given
    -        bidderDeps = BidderDeps.builder()
    +        final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder()
                     .name(BIDDER)
                     .deprecatedNames(emptyList())
    -                .aliases(emptyList())
    -                .build();
    +                .build()));
             bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
     
             // when and then
    @@ -54,11 +48,10 @@ public void isValidNameShouldReturnFalseForUnknownBidder() {
         @Test
         public void isDeprecatedNameShouldReturnTrueForDeprecatedBidder() {
             // given
    -        bidderDeps = BidderDeps.builder()
    +        final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder()
                     .name(BIDDER)
                     .deprecatedNames(singletonList("deprecated"))
    -                .aliases(emptyList())
    -                .build();
    +                .build()));
             bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
     
             // when and then
    @@ -77,11 +70,10 @@ public void isDeprecatedNameShouldReturnFalseForUnknownBidder() {
         @Test
         public void errorForDeprecatedNameShouldReturnErrorForDeprecatedBidder() {
             // given
    -        bidderDeps = BidderDeps.builder()
    +        final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder()
                     .name(BIDDER)
                     .deprecatedNames(singletonList("deprecated"))
    -                .aliases(emptyList())
    -                .build();
    +                .build()));
             bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
     
             // when and then
    @@ -89,69 +81,28 @@ public void errorForDeprecatedNameShouldReturnErrorForDeprecatedBidder() {
                     .isEqualTo("deprecated has been deprecated and is no longer available. Use rubicon instead.");
         }
     
    -    @Test
    -    public void aliasesShouldReturnConfiguredAliases() {
    -        // given
    -        bidderDeps = BidderDeps.builder()
    -                .name(BIDDER)
    -                .deprecatedNames(emptyList())
    -                .aliases(singletonList("alias"))
    -                .build();
    -        bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
    -
    -        // when and then
    -        assertThat(bidderCatalog.aliases()).containsOnly("alias");
    -    }
    -
    -    @Test
    -    public void isAliasShouldReturnTrueForBidderAlias() {
    -        // given
    -        bidderDeps = BidderDeps.builder()
    -                .name(BIDDER)
    -                .deprecatedNames(emptyList())
    -                .aliases(singletonList("alias"))
    -                .build();
    -        bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
    -
    -        // when and then
    -        assertThat(bidderCatalog.isAlias("alias")).isTrue();
    -    }
    -
    -    @Test
    -    public void isAliasShouldReturnFalseForUnknownBidderAlias() {
    -        // given
    -        bidderCatalog = new BidderCatalog(emptyList());
    -
    -        // when and then
    -        assertThat(bidderCatalog.isAlias("alias")).isFalse();
    -    }
    -
    -    @Test
    -    public void nameByAliasShouldReturnBidderName() {
    -        // given
    -        bidderDeps = BidderDeps.builder()
    -                .name(BIDDER)
    -                .deprecatedNames(emptyList())
    -                .aliases(singletonList("alias"))
    -                .build();
    -        bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
    -
    -        // when and then
    -        assertThat(bidderCatalog.nameByAlias("alias")).isEqualTo(BIDDER);
    -    }
    -
         @Test
         public void metaInfoByNameShouldReturnMetaInfoForKnownBidder() {
             // given
    -        final BidderInfo bidderInfo = BidderInfo.create(true, "test@email.com",
    -                singletonList("banner"), singletonList("video"), null, 99, true, true, false);
    -
    -        bidderDeps = BidderDeps.builder()
    +        final BidderInfo bidderInfo = BidderInfo.create(
    +                true,
    +                null,
    +                null,
    +                "test@email.com",
    +                singletonList("banner"),
    +                singletonList("video"),
    +                null,
    +                99,
    +                true,
    +                true,
    +                false);
    +
    +        final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder()
                     .name(BIDDER)
                     .deprecatedNames(emptyList())
    -                .aliases(emptyList())
                     .bidderInfo(bidderInfo)
    -                .build();
    +                .build()));
    +
             bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
     
             // when and then
    @@ -170,12 +121,12 @@ public void metaInfoByNameShouldReturnNullForUnknownBidder() {
         @Test
         public void usersyncerByNameShouldReturnUsersyncerForKnownBidder() {
             // given
    -        bidderDeps = BidderDeps.builder()
    +        final Usersyncer usersyncer = Usersyncer.of(null, null, null);
    +        final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder()
                     .name(BIDDER)
                     .deprecatedNames(emptyList())
    -                .aliases(emptyList())
                     .usersyncer(usersyncer)
    -                .build();
    +                .build()));
             bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
     
             // when and then
    @@ -194,12 +145,11 @@ public void usersyncerByNameShouldReturnNullForUnknownBidder() {
         @Test
         public void bidderByNameShouldReturnBidderForKnownBidder() {
             // given
    -        bidderDeps = BidderDeps.builder()
    +        final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder()
                     .name(BIDDER)
                     .deprecatedNames(emptyList())
    -                .aliases(emptyList())
                     .bidder(bidder)
    -                .build();
    +                .build()));
             bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
     
             // when and then
    @@ -209,17 +159,25 @@ public void bidderByNameShouldReturnBidderForKnownBidder() {
         @Test
         public void nameByVendorIdShouldReturnBidderNameForVendorId() {
             // given
    -        final BidderInfo bidderInfo = BidderInfo.create(true, "test@email.com",
    -                singletonList("banner"), singletonList("video"), null, 99, true, true, false);
    -
    -        bidderDeps = BidderDeps.builder()
    +        final BidderInfo bidderInfo = BidderInfo.create(
    +                true,
    +                null,
    +                null,
    +                "test@email.com",
    +                singletonList("banner"),
    +                singletonList("video"),
    +                null,
    +                99,
    +                true,
    +                true,
    +                false);
    +
    +        final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder()
                     .name(BIDDER)
                     .deprecatedNames(emptyList())
    -                .aliases(emptyList())
                     .bidder(bidder)
                     .bidderInfo(bidderInfo)
    -                .build();
    -
    +                .build()));
             bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
     
             // when and then
    @@ -234,72 +192,4 @@ public void bidderByNameShouldReturnNullForUnknownBidder() {
             // when and then
             assertThat(bidderCatalog.bidderByName("unknown_bidder")).isNull();
         }
    -
    -    @Test
    -    public void adapterByNameShouldReturnAdapterForKnownBidder() {
    -        // given
    -        bidderDeps = BidderDeps.builder()
    -                .name(BIDDER)
    -                .deprecatedNames(emptyList())
    -                .aliases(emptyList())
    -                .adapter(adapter)
    -                .build();
    -        bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
    -
    -        // when and then
    -        assertThat(bidderCatalog.adapterByName(BIDDER)).isSameAs(adapter);
    -    }
    -
    -    @Test
    -    public void adapterByNameShouldReturnNullForUnknownBidder() {
    -        // given
    -        bidderCatalog = new BidderCatalog(emptyList());
    -
    -        // when and then
    -        assertThat(bidderCatalog.adapterByName("unknown_bidder")).isNull();
    -    }
    -
    -    @Test
    -    public void isValidAdapterNameShouldReturnTrueIfNameIsValidAndAdapterIsDefined() {
    -        // given
    -        bidderDeps = BidderDeps.builder()
    -                .name(BIDDER)
    -                .deprecatedNames(emptyList())
    -                .aliases(emptyList())
    -                .adapter(adapter)
    -                .build();
    -        bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
    -
    -        // when and then
    -        assertThat(bidderCatalog.isValidAdapterName(BIDDER)).isTrue();
    -    }
    -
    -    @Test
    -    public void isValidAdapterNameShouldReturnFalseIfNameIsInvalid() {
    -        // given
    -        bidderDeps = BidderDeps.builder()
    -                .name("invalid")
    -                .deprecatedNames(emptyList())
    -                .aliases(emptyList())
    -                .adapter(adapter)
    -                .build();
    -        bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
    -
    -        // when and then
    -        assertThat(bidderCatalog.isValidAdapterName(BIDDER)).isFalse();
    -    }
    -
    -    @Test
    -    public void isValidAdapterNameShouldReturnFalseIfAdapterIsNotDefined() {
    -        // given
    -        bidderDeps = BidderDeps.builder()
    -                .name(BIDDER)
    -                .deprecatedNames(emptyList())
    -                .aliases(emptyList())
    -                .build();
    -        bidderCatalog = new BidderCatalog(singletonList(bidderDeps));
    -
    -        // when and then
    -        assertThat(bidderCatalog.isValidAdapterName(BIDDER)).isFalse();
    -    }
     }
    diff --git a/src/test/java/org/prebid/server/bidder/BidderErrorNotifierTest.java b/src/test/java/org/prebid/server/bidder/BidderErrorNotifierTest.java
    new file mode 100644
    index 00000000000..d71f2b608b4
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/BidderErrorNotifierTest.java
    @@ -0,0 +1,137 @@
    +package org.prebid.server.bidder;
    +
    +import com.iab.openrtb.request.BidRequest;
    +import io.vertx.core.Future;
    +import io.vertx.core.http.HttpMethod;
    +import org.assertj.core.api.Assertions;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.metric.Metrics;
    +import org.prebid.server.vertx.http.HttpClient;
    +import org.prebid.server.vertx.http.model.HttpClientResponse;
    +
    +import java.util.concurrent.TimeoutException;
    +
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyLong;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.ArgumentMatchers.eq;
    +import static org.mockito.ArgumentMatchers.isNull;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.verify;
    +import static org.mockito.Mockito.verifyZeroInteractions;
    +
    +public class BidderErrorNotifierTest {
    +
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
    +    @Mock
    +    private HttpClient httpClient;
    +    @Mock
    +    private Metrics metrics;
    +
    +    private BidderErrorNotifier bidderErrorNotifier;
    +
    +    @Mock
    +    private Bidder bidder;
    +
    +    @Before
    +    public void setUp() {
    +        bidderErrorNotifier = new BidderErrorNotifier(200, true, false, 1d, httpClient, metrics);
    +    }
    +
    +    @Test
    +    public void shouldNotSendTimeoutNotificationWhenBidderDoesNotCreateRequest() {
    +        // given
    +        final HttpRequest bidderRequest = HttpRequest.builder().build();
    +
    +        given(bidder.makeTimeoutNotification(any())).willReturn(null);
    +
    +        // when
    +        bidderErrorNotifier.processTimeout(HttpCall.failure(bidderRequest, BidderError.timeout("Timeout")), bidder);
    +
    +        // then
    +        verify(bidder).makeTimeoutNotification(eq(bidderRequest));
    +        verifyZeroInteractions(httpClient);
    +    }
    +
    +    @Test
    +    public void shouldSendTimeoutNotificationAndUpdateSuccessMetric() {
    +        // given
    +        final HttpRequest bidderRequest = HttpRequest.builder().build();
    +        final HttpCall bidderCall = HttpCall.failure(bidderRequest, BidderError.timeout("Timeout"));
    +
    +        given(bidder.makeTimeoutNotification(any())).willReturn(HttpRequest.builder()
    +                .uri("url")
    +                .method(HttpMethod.POST)
    +                .body("{}")
    +                .build());
    +
    +        given(httpClient.request(any(), anyString(), any(), anyString(), anyLong()))
    +                .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, null)));
    +
    +        // when
    +        final HttpCall result = bidderErrorNotifier.processTimeout(bidderCall, bidder);
    +
    +        // then
    +        Assertions.assertThat(result).isSameAs(bidderCall);
    +
    +        verify(bidder).makeTimeoutNotification(eq(bidderRequest));
    +        verify(httpClient).request(eq(HttpMethod.POST), eq("url"), isNull(), eq("{}"), eq(200L));
    +        verify(metrics).updateTimeoutNotificationMetric(eq(true));
    +    }
    +
    +    @Test
    +    public void shouldSendTimeoutNotificationAndUpdateFailedMetricWhenResponseCodeNonSuccess() {
    +        // given
    +        final HttpRequest bidderRequest = HttpRequest.builder().build();
    +
    +        given(bidder.makeTimeoutNotification(any())).willReturn(HttpRequest.builder()
    +                .uri("url")
    +                .method(HttpMethod.POST)
    +                .body("{}")
    +                .build());
    +
    +        given(httpClient.request(any(), anyString(), any(), anyString(), anyLong()))
    +                .willReturn(Future.succeededFuture(HttpClientResponse.of(404, null, null)));
    +
    +        // when
    +        bidderErrorNotifier.processTimeout(HttpCall.failure(bidderRequest, BidderError.timeout("Timeout")), bidder);
    +
    +        // then
    +        verify(bidder).makeTimeoutNotification(eq(bidderRequest));
    +        verify(httpClient).request(eq(HttpMethod.POST), eq("url"), isNull(), eq("{}"), eq(200L));
    +        verify(metrics).updateTimeoutNotificationMetric(eq(false));
    +    }
    +
    +    @Test
    +    public void shouldSendTimeoutNotificationAndUpdateFailedMetricWhenResponseTimedOut() {
    +        // given
    +        final HttpRequest bidderRequest = HttpRequest.builder().build();
    +
    +        given(bidder.makeTimeoutNotification(any())).willReturn(HttpRequest.builder()
    +                .uri("url")
    +                .method(HttpMethod.POST)
    +                .body("{}")
    +                .build());
    +
    +        given(httpClient.request(any(), anyString(), any(), anyString(), anyLong()))
    +                .willReturn(Future.failedFuture(new TimeoutException("Timeout exception")));
    +
    +        // when
    +        bidderErrorNotifier.processTimeout(HttpCall.failure(bidderRequest, BidderError.timeout("Timeout")), bidder);
    +
    +        // then
    +        verify(bidder).makeTimeoutNotification(eq(bidderRequest));
    +        verify(httpClient).request(eq(HttpMethod.POST), eq("url"), isNull(), eq("{}"), eq(200L));
    +        verify(metrics).updateTimeoutNotificationMetric(eq(false));
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/DisabledAdapterTest.java b/src/test/java/org/prebid/server/bidder/DisabledAdapterTest.java
    deleted file mode 100644
    index d97b9200ed6..00000000000
    --- a/src/test/java/org/prebid/server/bidder/DisabledAdapterTest.java
    +++ /dev/null
    @@ -1,42 +0,0 @@
    -package org.prebid.server.bidder;
    -
    -import org.junit.Before;
    -import org.junit.Test;
    -import org.prebid.server.exception.PreBidException;
    -
    -import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
    -
    -public class DisabledAdapterTest {
    -
    -    private DisabledAdapter disabledAdapter;
    -
    -    @Before
    -    public void setUp() {
    -        disabledAdapter = new DisabledAdapter("error message");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldRespondWithExpectedError() {
    -        assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> disabledAdapter.makeHttpRequests(null, null))
    -                .withMessage("error message");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldBeUnsupported() {
    -        assertThatExceptionOfType(UnsupportedOperationException.class)
    -                .isThrownBy(() -> disabledAdapter.extractBids(null, null));
    -    }
    -
    -    @Test
    -    public void tolerateErrorsShouldBeUnsupported() {
    -        assertThatExceptionOfType(UnsupportedOperationException.class)
    -                .isThrownBy(() -> disabledAdapter.tolerateErrors());
    -    }
    -
    -    @Test
    -    public void responseTypeReferenceShouldBeUnsupported() {
    -        assertThatExceptionOfType(UnsupportedOperationException.class)
    -                .isThrownBy(() -> disabledAdapter.responseTypeReference());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/HttpAdapterConnectorTest.java b/src/test/java/org/prebid/server/bidder/HttpAdapterConnectorTest.java
    deleted file mode 100644
    index 04f1f742a1a..00000000000
    --- a/src/test/java/org/prebid/server/bidder/HttpAdapterConnectorTest.java
    +++ /dev/null
    @@ -1,828 +0,0 @@
    -package org.prebid.server.bidder;
    -
    -import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.fasterxml.jackson.core.type.TypeReference;
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.BidRequest.BidRequestBuilder;
    -import com.iab.openrtb.request.Format;
    -import com.iab.openrtb.request.Regs;
    -import com.iab.openrtb.request.User;
    -import com.iab.openrtb.response.Bid;
    -import com.iab.openrtb.response.Bid.BidBuilder;
    -import com.iab.openrtb.response.BidResponse;
    -import com.iab.openrtb.response.BidResponse.BidResponseBuilder;
    -import com.iab.openrtb.response.SeatBid;
    -import com.iab.openrtb.response.SeatBid.SeatBidBuilder;
    -import io.netty.channel.ConnectTimeoutException;
    -import io.vertx.core.Future;
    -import io.vertx.core.MultiMap;
    -import io.vertx.core.http.HttpMethod;
    -import lombok.AllArgsConstructor;
    -import lombok.Value;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.ArgumentCaptor;
    -import org.mockito.BDDMockito;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.auction.model.AdUnitBid.AdUnitBidBuilder;
    -import org.prebid.server.auction.model.AdapterRequest;
    -import org.prebid.server.auction.model.AdapterResponse;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    -import org.prebid.server.auction.model.PreBidRequestContext.PreBidRequestContextBuilder;
    -import org.prebid.server.bidder.model.AdapterHttpRequest;
    -import org.prebid.server.bidder.model.BidderError;
    -import org.prebid.server.bidder.model.ExchangeCall;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.execution.Timeout;
    -import org.prebid.server.execution.TimeoutFactory;
    -import org.prebid.server.privacy.PrivacyExtractor;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.request.PreBidRequest;
    -import org.prebid.server.proto.request.PreBidRequest.PreBidRequestBuilder;
    -import org.prebid.server.proto.response.BidderDebug;
    -import org.prebid.server.proto.response.MediaType;
    -import org.prebid.server.proto.response.UsersyncInfo;
    -import org.prebid.server.vertx.http.HttpClient;
    -import org.prebid.server.vertx.http.model.HttpClientResponse;
    -
    -import java.io.IOException;
    -import java.math.BigDecimal;
    -import java.time.Clock;
    -import java.time.Instant;
    -import java.time.ZoneId;
    -import java.util.List;
    -import java.util.concurrent.TimeoutException;
    -import java.util.function.Function;
    -import java.util.stream.Collectors;
    -
    -import static io.vertx.core.http.HttpMethod.GET;
    -import static io.vertx.core.http.HttpMethod.POST;
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.singleton;
    -import static java.util.Collections.singletonList;
    -import static java.util.function.Function.identity;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.mockito.ArgumentMatchers.any;
    -import static org.mockito.ArgumentMatchers.anyLong;
    -import static org.mockito.ArgumentMatchers.anyString;
    -import static org.mockito.BDDMockito.given;
    -import static org.mockito.BDDMockito.willReturn;
    -import static org.mockito.Mockito.eq;
    -import static org.mockito.Mockito.isNull;
    -import static org.mockito.Mockito.mock;
    -import static org.mockito.Mockito.verify;
    -import static org.mockito.Mockito.verifyZeroInteractions;
    -
    -public class HttpAdapterConnectorTest extends VertxTest {
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private HttpClient httpClient;
    -
    -    private Clock clock;
    -
    -    private HttpAdapterConnector httpAdapterConnector;
    -    @Mock
    -    private Adapter adapter;
    -
    -    private Usersyncer usersyncer;
    -    @Mock
    -    private UidsCookie uidsCookie;
    -
    -    private AdapterRequest adapterRequest;
    -
    -    private PreBidRequestContext preBidRequestContext;
    -
    -    @Before
    -    public void setUp() {
    -        willReturn(singletonList(givenHttpRequest())).given(adapter).makeHttpRequests(any(), any());
    -        willReturn(new TypeReference() {
    -        }).given(adapter).responseTypeReference();
    -
    -        clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
    -        adapterRequest = AdapterRequest.of(null, null);
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -
    -        httpAdapterConnector = new HttpAdapterConnector(
    -                httpClient, new PrivacyExtractor(), clock, jacksonMapper);
    -
    -        usersyncer = new Usersyncer(null, "", "", null, null, false);
    -    }
    -
    -    @Test
    -    public void callShouldPerformHttpRequestsWithExpectedMethod() {
    -        // given
    -        givenHttpClientReturnsResponse(200, null);
    -
    -        willReturn(singletonList(givenHttpRequest(GET)))
    -                .given(adapter).makeHttpRequests(any(), any());
    -
    -        // when
    -        httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        verify(httpClient).request(eq(GET), anyString(), any(), isNull(), anyLong());
    -    }
    -
    -    @Test
    -    public void callShouldPerformHttpRequestsWithExpectedHeaders() {
    -        // given
    -        givenHttpClientReturnsResponse(200, null);
    -
    -        final MultiMap headers = MultiMap.caseInsensitiveMultiMap().add("key1", "value1");
    -        willReturn(singletonList(AdapterHttpRequest.of(POST, "uri", null, headers)))
    -                .given(adapter).makeHttpRequests(any(), any());
    -
    -        // when
    -        httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        verify(httpClient).request(any(), anyString(), eq(headers), any(), anyLong());
    -    }
    -
    -    @Test
    -    public void callShouldPerformHttpRequestsWithoutAdditionalHeadersIfTheyAreNull() {
    -        // given
    -        givenHttpClientReturnsResponse(200, null);
    -
    -        willReturn(singletonList(givenHttpRequest(POST))).given(adapter).makeHttpRequests(any(), any());
    -
    -        // when
    -        httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        verify(httpClient).request(any(), anyString(), isNull(), any(), anyLong());
    -    }
    -
    -    @Test
    -    public void callShouldPerformHttpRequestsWithExpectedTimeout() {
    -        // given
    -        givenHttpClientReturnsResponse(200, null);
    -
    -        // when
    -        httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        verify(httpClient).request(any(), anyString(), any(), any(), eq(500L));
    -    }
    -
    -    @Test
    -    public void callShouldPerformHttpRequestsWithExpectedBody() throws IOException {
    -        // given
    -        givenHttpClientReturnsResponse(200, null);
    -
    -        willReturn(singletonList(AdapterHttpRequest.of(POST, "uri", givenBidRequest(b -> b.id("bidRequest1")), null)))
    -                .given(adapter).makeHttpRequests(any(), any());
    -
    -        // when
    -        httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final BidRequest bidRequest = captureBidRequest();
    -        assertThat(bidRequest).isNotNull();
    -        assertThat(bidRequest.getId()).isEqualTo("bidRequest1");
    -    }
    -
    -    @Test
    -    public void callShouldPerformHttpRequestsWithoutBodyIfItIsNull() {
    -        // given
    -        givenHttpClientReturnsResponse(200, null);
    -
    -        willReturn(singletonList(givenHttpRequest(POST))).given(adapter).makeHttpRequests(any(), any());
    -
    -        // when
    -        httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        verify(httpClient).request(any(), anyString(), any(), isNull(), anyLong());
    -    }
    -
    -    @Test
    -    public void callShouldNotPerformHttpRequestsIfAdapterReturnsEmptyHttpRequests() {
    -        // given
    -        given(adapter.makeHttpRequests(any(), any())).willReturn(emptyList());
    -
    -        // when
    -        httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        verifyZeroInteractions(httpClient);
    -    }
    -
    -    @Test
    -    public void callShouldSubmitErrorToAdapterIfMakeHttpRequestsFails() {
    -        // given
    -        given(adapter.makeHttpRequests(any(), any())).willThrow(new PreBidException("Make http requests exception"));
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isEqualTo(BidderError.badInput("Make http requests exception"));
    -        assertThat(adapterResponse.getBidderStatus().getError()).isEqualTo("Make http requests exception");
    -    }
    -
    -    @Test
    -    public void callShouldSubmitTimeOutErrorToAdapterIfGlobalTimeoutAlreadyExpired() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(
    -                builder -> builder.timeout(expiredTimeout()),
    -                identity());
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isEqualTo(BidderError.timeout("Timed out"));
    -        assertThat(adapterResponse.getBidderStatus().getError()).isEqualTo("Timed out");
    -        verifyZeroInteractions(httpClient);
    -    }
    -
    -    @Test
    -    public void callShouldSubmitTimeOutErrorToAdapterIfConnectTimeoutOccurs() {
    -        // given
    -        givenHttpClientProducesException(new ConnectTimeoutException());
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isEqualTo(BidderError.timeout("Timed out"));
    -        assertThat(adapterResponse.getBidderStatus().getError()).isEqualTo("Timed out");
    -    }
    -
    -    @Test
    -    public void callShouldSubmitTimeOutErrorToAdapterIfTimeoutOccurs() {
    -        // given
    -        givenHttpClientProducesException(new TimeoutException());
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isEqualTo(BidderError.timeout("Timed out"));
    -        assertThat(adapterResponse.getBidderStatus().getError()).isEqualTo("Timed out");
    -    }
    -
    -    @Test
    -    public void callShouldSubmitErrorToAdapterIfReadingHttpResponseFails() {
    -        // given
    -        givenHttpClientProducesException(new RuntimeException("Response exception"));
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isEqualTo(BidderError.generic("Response exception"));
    -        assertThat(adapterResponse.getBidderStatus().getError()).isEqualTo("Response exception");
    -    }
    -
    -    @Test
    -    public void callShouldNotSubmitErrorToAdapterIfHttpResponseStatusCodeIs204() {
    -        // given
    -        givenHttpClientReturnsResponse(204, "response");
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isNull();
    -        assertThat(adapterResponse.getBidderStatus().getError()).isNull();
    -        assertThat(adapterResponse.getBids()).isEmpty();
    -    }
    -
    -    @Test
    -    public void callShouldSubmitErrorToAdapterIfHttpResponseStatusCodeIsNot200Or204() {
    -        // given
    -        givenHttpClientReturnsResponse(503, "response");
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError())
    -                .isEqualTo(BidderError.badServerResponse("HTTP status 503; body: response"));
    -        assertThat(adapterResponse.getBidderStatus().getError()).isEqualTo("HTTP status 503; body: response");
    -    }
    -
    -    @Test
    -    public void callShouldSubmitErrorToAdapterIfHttpResponseStatusCodeIs400() {
    -        // given
    -        givenHttpClientReturnsResponse(400, "response");
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError())
    -                .isEqualTo(BidderError.badInput("HTTP status 400; body: response"));
    -        assertThat(adapterResponse.getBidderStatus().getError()).isEqualTo("HTTP status 400; body: response");
    -    }
    -
    -    @Test
    -    public void callShouldSubmitErrorToAdapterIfHttpResponseBodyCouldNotBeParsed() {
    -        // given
    -        givenHttpClientReturnsResponse(200, "response");
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isNotNull();
    -        assertThat(adapterResponse.getError().getMessage()).startsWith("Failed to decode");
    -        assertThat(adapterResponse.getError().getType()).isEqualTo(BidderError.Type.bad_server_response);
    -        assertThat(adapterResponse.getBidderStatus().getError()).startsWith("Failed to decode");
    -    }
    -
    -    @SuppressWarnings("unchecked")
    -    @Test
    -    public void callShouldUnmarshalBodyToAdapterResponseClass() throws JsonProcessingException {
    -        // given
    -        adapterRequest = AdapterRequest.of("bidderCode1", singletonList(givenAdUnitBid(identity())));
    -
    -        final Adapter anotherAdapter = (Adapter) mock(Adapter.class);
    -        willReturn(singletonList(givenHttpRequest())).given(anotherAdapter).makeHttpRequests(any(), any());
    -        willReturn(new TypeReference() {
    -        }).given(anotherAdapter).responseTypeReference();
    -        given(anotherAdapter.extractBids(any(), any()))
    -                .willReturn(singletonList(org.prebid.server.proto.response.Bid.builder()));
    -
    -        final String bidResponse = mapper.writeValueAsString(CustomResponse.of("url", BigDecimal.ONE));
    -        givenHttpClientReturnsResponse(200, bidResponse);
    -
    -        // when
    -        httpAdapterConnector.call(anotherAdapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final ArgumentCaptor> exchangeCallCaptor =
    -                ArgumentCaptor.forClass(ExchangeCall.class);
    -        verify(anotherAdapter).extractBids(any(), exchangeCallCaptor.capture());
    -        assertThat(exchangeCallCaptor.getValue().getResponse()).isInstanceOf(CustomResponse.class);
    -    }
    -
    -    @Test
    -    public void callShouldReturnBidderResultWithoutErrorIfBidsArePresent() throws JsonProcessingException {
    -        // given
    -        final AdUnitBid adUnitBid = givenAdUnitBid(identity());
    -        adapterRequest = AdapterRequest.of("bidderCode1", asList(adUnitBid, adUnitBid));
    -
    -        given(adapter.extractBids(any(), any()))
    -                .willReturn(singletonList(org.prebid.server.proto.response.Bid.builder()));
    -
    -        final String bidResponse = givenBidResponse(identity(), identity(), singletonList(identity()));
    -        givenHttpClientReturnsResponse(200, bidResponse);
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isNull();
    -        assertThat(adapterResponse.getBidderStatus().getError()).isNull();
    -        assertThat(adapterResponse.getBids()).hasSize(1);
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithErrorIfErrorsOccurWhileHttpRequestForNotToleratedErrorsAdapter()
    -            throws JsonProcessingException {
    -        // given
    -        givenHttpClientReturnsResponse(200, null);
    -
    -        willReturn(asList(givenHttpRequest(), givenHttpRequest())).given(adapter).makeHttpRequests(any(), any());
    -
    -        final AdUnitBid adUnitBid = givenAdUnitBid(identity());
    -        adapterRequest = AdapterRequest.of("bidderCode1", asList(adUnitBid, adUnitBid));
    -
    -        given(adapter.extractBids(any(), any()))
    -                .willReturn(singletonList(org.prebid.server.proto.response.Bid.builder()))
    -                .willReturn(singletonList(org.prebid.server.proto.response.Bid.builder()));
    -
    -        final String bidResponse = givenBidResponse(identity(), identity(), singletonList(identity()));
    -        givenHttpClientReturnsResponses(
    -                HttpClientResponse.of(200, null, bidResponse),
    -                HttpClientResponse.of(503, null, "error response"));
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isNotNull();
    -        assertThat(adapterResponse.getError().getMessage()).startsWith("HTTP status 503; body:");
    -        assertThat(adapterResponse.getError().getType()).isEqualTo(BidderError.Type.bad_server_response);
    -        assertThat(adapterResponse.getBidderStatus().getError()).startsWith("HTTP status 503; body:");
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithoutErrorIfBidsArePresentWhileHttpRequestForToleratedErrorsAdapter()
    -            throws JsonProcessingException {
    -        // given
    -        willReturn(asList(givenHttpRequest(), givenHttpRequest())).given(adapter).makeHttpRequests(any(), any());
    -
    -        given(adapter.tolerateErrors()).willReturn(true);
    -
    -        final AdUnitBid adUnitBid = givenAdUnitBid(identity());
    -        adapterRequest = AdapterRequest.of("bidderCode1", asList(adUnitBid, adUnitBid));
    -
    -        given(adapter.extractBids(any(), any()))
    -                .willReturn(singletonList(org.prebid.server.proto.response.Bid.builder()))
    -                .willReturn(null);
    -
    -        final String bidResponse = givenBidResponse(identity(), identity(), singletonList(identity()));
    -        givenHttpClientReturnsResponses(
    -                HttpClientResponse.of(200, null, bidResponse),
    -                HttpClientResponse.of(503, null, "error response"));
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isNull();
    -        assertThat(adapterResponse.getBidderStatus().getError()).isNull();
    -        assertThat(adapterResponse.getBids()).hasSize(1);
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithErrorIfErrorsOccurWhileExtractingForNotToleratedErrorsAdapter()
    -            throws JsonProcessingException {
    -        // given
    -        willReturn(asList(givenHttpRequest(), givenHttpRequest())).given(adapter).makeHttpRequests(any(), any());
    -
    -        final AdUnitBid adUnitBid = givenAdUnitBid(identity());
    -        adapterRequest = AdapterRequest.of("bidderCode1", asList(adUnitBid, adUnitBid));
    -
    -        given(adapter.extractBids(any(), any()))
    -                .willReturn(singletonList(org.prebid.server.proto.response.Bid.builder()))
    -                .willThrow(new PreBidException("adapter extractBids exception"));
    -
    -        final String bidResponse = givenBidResponse(identity(), identity(), singletonList(identity()));
    -        givenHttpClientReturnsResponse(200, bidResponse);
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError())
    -                .isEqualTo(BidderError.badServerResponse("adapter extractBids exception"));
    -        assertThat(adapterResponse.getBidderStatus().getError()).isEqualTo("adapter extractBids exception");
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithoutErrorIfBidsArePresentWhileExtractingForToleratedErrorsAdapter()
    -            throws JsonProcessingException {
    -        // given
    -        willReturn(asList(givenHttpRequest(), givenHttpRequest())).given(adapter).makeHttpRequests(any(), any());
    -
    -        given(adapter.tolerateErrors()).willReturn(true);
    -
    -        final AdUnitBid adUnitBid = givenAdUnitBid(identity());
    -        adapterRequest = AdapterRequest.of("bidderCode1", asList(adUnitBid, adUnitBid));
    -
    -        given(adapter.extractBids(any(), any()))
    -                .willReturn(singletonList(org.prebid.server.proto.response.Bid.builder()))
    -                .willThrow(new PreBidException("adapter extractBids exception"));
    -
    -        final String bidResponse = givenBidResponse(identity(), identity(), singletonList(identity()));
    -        givenHttpClientReturnsResponse(200, bidResponse);
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getError()).isNull();
    -        assertThat(adapterResponse.getBidderStatus().getError()).isNull();
    -        assertThat(adapterResponse.getBids()).hasSize(1);
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithEmptyBidsIfAdUnitBidIsBannerAndSizesLengthMoreThanOne()
    -            throws JsonProcessingException {
    -        // given
    -        adapterRequest = AdapterRequest.of("bidderCode1", singletonList(
    -                givenAdUnitBid(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .sizes(asList(Format.builder().w(100).h(200).build(), Format.builder().w(100).h(200).build()))
    -                        .bidId("bidId1"))));
    -
    -        given(adapter.extractBids(any(), any()))
    -                .willReturn(singletonList(org.prebid.server.proto.response.Bid.builder()
    -                        .code("adUnitCode1")
    -                        .bidId("bidId1")
    -                        .mediaType(MediaType.banner)));
    -
    -        givenHttpClientReturnsResponse(200,
    -                givenBidResponse(identity(), identity(), singletonList(identity())));
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getBids()).isEmpty();
    -        assertThat(adapterResponse.getBidderStatus().getNumBids()).isEqualTo(0);
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithNoCookieIfNoAdapterUidInCookieAndNoAppInPreBidRequest()
    -            throws IOException {
    -        // given
    -        givenHttpClientReturnsResponse(200,
    -                givenBidResponse(identity(), identity(), singletonList(identity())));
    -        usersyncer = new Usersyncer(null, "url1", null, null, null, false);
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getBidderStatus().getNoCookie()).isTrue();
    -        assertThat(adapterResponse.getBidderStatus().getUsersync()).isNotNull();
    -        assertThat(adapterResponse.getBidderStatus().getUsersync()).isEqualTo(UsersyncInfo.of("url1", null, false));
    -    }
    -
    -    @Test
    -    public void callShouldReturnGdprAwareAdapterResponseWithNoCookieIfNoAdapterUidInCookieAndNoAppInPreBidRequest()
    -            throws IOException {
    -        // given
    -        final Regs regs = Regs.of(0, ExtRegs.of(1, "1---"));
    -        final User user = User.builder()
    -                .ext(ExtUser.builder().consent("consent$1").build())
    -                .build();
    -        preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder.regs(regs).user(user));
    -
    -        givenHttpClientReturnsResponse(200,
    -                givenBidResponse(identity(), identity(), singletonList(identity())));
    -
    -        usersyncer = new Usersyncer(null, "http://url?redir=%26gdpr%3D{{gdpr}}"
    -                + "%26gdpr_consent%3D{{gdpr_consent}}"
    -                + "%26us_privacy={{us_privacy}}",
    -                null, null, null, false);
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getBidderStatus().getNoCookie()).isTrue();
    -        assertThat(adapterResponse.getBidderStatus().getUsersync()).isNotNull();
    -        assertThat(adapterResponse.getBidderStatus().getUsersync()).isEqualTo(UsersyncInfo.of(
    -                "http://url?redir=%26gdpr%3D1%26gdpr_consent%3Dconsent%241%26us_privacy=1---", null, false));
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithoutNoCookieIfNoAdapterUidInCookieAndAppPresentInPreBidRequest()
    -            throws IOException {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(identity(),
    -                builder -> builder.app(App.builder().build()).user(User.builder().build()));
    -
    -        givenHttpClientReturnsResponse(200,
    -                givenBidResponse(identity(), identity(), singletonList(identity())));
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getBidderStatus().getNoCookie()).isNull();
    -        assertThat(adapterResponse.getBidderStatus().getUsersync()).isNull();
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithDebugIfFlagIsTrue() throws JsonProcessingException {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(builder -> builder.isDebug(true), identity());
    -
    -        adapterRequest = AdapterRequest.of("bidderCode1", asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1")),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"))));
    -
    -        final String bidResponse = givenBidResponse(builder -> builder.id("bidResponseId1"),
    -                identity(),
    -                asList(bidBuilder -> bidBuilder.impid("adUnitCode1"), bidBuilder -> bidBuilder.impid("adUnitCode2")));
    -        givenHttpClientReturnsResponse(200, bidResponse);
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(String.class);
    -        verify(httpClient).request(any(), anyString(), any(), bidRequestCaptor.capture(), anyLong());
    -        final List bidRequests = bidRequestCaptor.getAllValues();
    -
    -        assertThat(adapterResponse.getBidderStatus().getDebug()).hasSize(1).containsOnly(
    -                BidderDebug.builder()
    -                        .requestUri("uri")
    -                        .requestBody(bidRequests.get(0))
    -                        .responseBody(bidResponse)
    -                        .statusCode(200)
    -                        .build());
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithoutDebugIfFlagIsFalse() throws JsonProcessingException {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(builder -> builder.isDebug(false), identity());
    -
    -        givenHttpClientReturnsResponse(200,
    -                givenBidResponse(identity(), identity(), singletonList(identity())));
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        assertThat(adapterResponseFuture.result().getBidderStatus().getDebug()).isNull();
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithDebugIfFlagIsTrueAndGlobalTimeoutAlreadyExpired() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(
    -                builder -> builder
    -                        .timeout(expiredTimeout())
    -                        .isDebug(true),
    -                identity());
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getBidderStatus().getDebug()).hasSize(1);
    -
    -        final BidderDebug bidderDebug = adapterResponse.getBidderStatus().getDebug().get(0);
    -        assertThat(bidderDebug.getRequestUri()).isNotBlank();
    -        assertThat(bidderDebug.getRequestBody()).isNotBlank();
    -    }
    -
    -    @Test
    -    public void callShouldReturnAdapterResponseWithDebugIfFlagIsTrueAndResponseIsNotSuccessful() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(builder -> builder.isDebug(true), identity());
    -
    -        givenHttpClientReturnsResponse(503, "response");
    -
    -        // when
    -        final Future adapterResponseFuture =
    -                httpAdapterConnector.call(adapter, usersyncer, adapterRequest, preBidRequestContext);
    -
    -        // then
    -        final AdapterResponse adapterResponse = adapterResponseFuture.result();
    -        assertThat(adapterResponse.getBidderStatus().getDebug()).hasSize(1);
    -
    -        final BidderDebug bidderDebug = adapterResponse.getBidderStatus().getDebug().get(0);
    -        assertThat(bidderDebug.getRequestUri()).isNotBlank();
    -        assertThat(bidderDebug.getRequestBody()).isNotBlank();
    -        assertThat(bidderDebug.getResponseBody()).isNotBlank();
    -        assertThat(bidderDebug.getStatusCode()).isPositive();
    -    }
    -
    -    private BidRequest captureBidRequest() throws IOException {
    -        final ArgumentCaptor bidRequestCaptor = ArgumentCaptor.forClass(String.class);
    -        verify(httpClient).request(any(), anyString(), any(), bidRequestCaptor.capture(), anyLong());
    -        return mapper.readValue(bidRequestCaptor.getValue(), BidRequest.class);
    -    }
    -
    -    private AdapterHttpRequest givenHttpRequest(HttpMethod method) {
    -        return AdapterHttpRequest.of(method, "uri", null, null);
    -    }
    -
    -    private static AdapterHttpRequest givenHttpRequest() {
    -        return AdapterHttpRequest.of(POST, "uri", givenBidRequest(identity()), null);
    -    }
    -
    -    private static BidRequest givenBidRequest(
    -            Function bidRequestBuilderCustomizer) {
    -        return bidRequestBuilderCustomizer.apply(BidRequest.builder()).build();
    -    }
    -
    -    private static AdUnitBid givenAdUnitBid(Function adUnitBidBuilderCustomizer) {
    -        final AdUnitBidBuilder adUnitBidBuilderMinimal = AdUnitBid.builder()
    -                .adUnitCode("adUnitCode1")
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .mediaTypes(singleton(MediaType.banner));
    -        return adUnitBidBuilderCustomizer.apply(adUnitBidBuilderMinimal).build();
    -    }
    -
    -    private PreBidRequestContext givenPreBidRequestContext(
    -            Function preBidRequestContextBuilderCustomizer,
    -            Function preBidRequestBuilderCustomizer) {
    -
    -        final PreBidRequestBuilder preBidRequestBuilderMinimal = PreBidRequest.builder()
    -                .accountId("accountId");
    -        final PreBidRequest preBidRequest = preBidRequestBuilderCustomizer.apply(preBidRequestBuilderMinimal).build();
    -
    -        final PreBidRequestContextBuilder preBidRequestContextBuilderMinimal =
    -                PreBidRequestContext.builder()
    -                        .preBidRequest(preBidRequest)
    -                        .uidsCookie(uidsCookie)
    -                        .timeout(timeout());
    -        return preBidRequestContextBuilderCustomizer.apply(preBidRequestContextBuilderMinimal).build();
    -    }
    -
    -    private static String givenBidResponse(
    -            Function bidResponseBuilderCustomizer,
    -            Function seatBidBuilderCustomizer,
    -            List> bidBuilderCustomizers) throws JsonProcessingException {
    -
    -        // bid
    -        final BidBuilder bidBuilderMinimal = com.iab.openrtb.response.Bid.builder();
    -        final List bids = bidBuilderCustomizers.stream()
    -                .map(bidBuilderBidBuilderFunction -> bidBuilderBidBuilderFunction.apply(bidBuilderMinimal).build())
    -                .collect(Collectors.toList());
    -
    -        // seatBid
    -        final SeatBidBuilder seatBidBuilderMinimal = SeatBid.builder().bid(bids);
    -        final SeatBid seatBid = seatBidBuilderCustomizer.apply(seatBidBuilderMinimal).build();
    -
    -        // bidResponse
    -        final BidResponseBuilder bidResponseBuilderMinimal = BidResponse.builder()
    -                .seatbid(singletonList(seatBid));
    -        final BidResponse bidResponse = bidResponseBuilderCustomizer.apply(bidResponseBuilderMinimal).build();
    -
    -        return mapper.writeValueAsString(bidResponse);
    -    }
    -
    -    private void givenHttpClientReturnsResponse(int statusCode, String response) {
    -        given(httpClient.request(any(), anyString(), any(), any(), anyLong()))
    -                .willReturn(Future.succeededFuture(HttpClientResponse.of(statusCode, null, response)));
    -    }
    -
    -    private void givenHttpClientProducesException(Throwable throwable) {
    -        given(httpClient.request(any(), anyString(), any(), any(), anyLong()))
    -                .willReturn(Future.failedFuture(throwable));
    -    }
    -
    -    private void givenHttpClientReturnsResponses(HttpClientResponse... httpClientResponses) {
    -        BDDMockito.BDDMyOngoingStubbing> stubbing =
    -                given(httpClient.request(any(), anyString(), any(), any(), anyLong()));
    -
    -        // setup multiple answers
    -        for (HttpClientResponse httpClientResponse : httpClientResponses) {
    -            stubbing = stubbing.willReturn(Future.succeededFuture(httpClientResponse));
    -        }
    -    }
    -
    -    private Timeout timeout() {
    -        return new TimeoutFactory(clock).create(500L);
    -    }
    -
    -    private Timeout expiredTimeout() {
    -        return new TimeoutFactory(clock).create(clock.instant().minusMillis(1500L).toEpochMilli(), 1000L);
    -    }
    -
    -    @AllArgsConstructor(staticName = "of")
    -    @Value
    -    private static class CustomResponse {
    -
    -        String url;
    -
    -        BigDecimal price;
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java
    new file mode 100644
    index 00000000000..aa48fe0bcb3
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java
    @@ -0,0 +1,108 @@
    +package org.prebid.server.bidder;
    +
    +import com.iab.openrtb.request.App;
    +import com.iab.openrtb.request.BidRequest;
    +import io.vertx.core.MultiMap;
    +import io.vertx.core.http.CaseInsensitiveHeaders;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
    +import org.prebid.server.proto.openrtb.ext.request.ExtApp;
    +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +
    +public class HttpBidderRequestEnricherTest {
    +
    +    private HttpBidderRequestEnricher requestEnricher;
    +
    +    @Before
    +    public void setUp() {
    +
    +        requestEnricher = new HttpBidderRequestEnricher("1.00");
    +    }
    +
    +    @Test
    +    public void shouldSendPopulatedPostRequest() {
    +        // given
    +        final MultiMap headers = new CaseInsensitiveHeaders();
    +        headers.add("header1", "value1");
    +        headers.add("header2", "value2");
    +
    +        // when
    +        final MultiMap resultHeaders =
    +                requestEnricher.enrichHeaders(headers, CaseInsensitiveMultiMap.empty(), BidRequest.builder().build());
    +
    +        // then
    +        final MultiMap expectedHeaders = new CaseInsensitiveHeaders();
    +        expectedHeaders.addAll(headers);
    +        expectedHeaders.add("x-prebid", "pbs-java/1.00");
    +        assertThat(resultHeaders).hasSize(3);
    +        assertThat(isEqualsMultiMaps(resultHeaders, expectedHeaders)).isTrue();
    +    }
    +
    +    @Test
    +    public void shouldAddSecGpcHeaderFromOriginalRequest() {
    +        // given
    +        final CaseInsensitiveMultiMap originalHeaders = CaseInsensitiveMultiMap.builder()
    +                .add("Sec-GPC", "1")
    +                .build();
    +
    +        // when
    +        final MultiMap resultHeaders =
    +                requestEnricher.enrichHeaders(new CaseInsensitiveHeaders(),
    +                        originalHeaders, BidRequest.builder().build());
    +
    +        // then
    +        assertThat(resultHeaders.contains("Sec-GPC")).isTrue();
    +        assertThat(resultHeaders.get("Sec-GPC")).isEqualTo("1");
    +    }
    +
    +    @Test
    +    public void shouldNotOverrideHeadersFromBidRequest() {
    +        // given
    +        final CaseInsensitiveMultiMap originalHeaders = CaseInsensitiveMultiMap.builder()
    +                .add("Sec-GPC", "1")
    +                .build();
    +        final MultiMap bidderRequestHeaders = new CaseInsensitiveHeaders().add("Sec-GPC", "0");
    +
    +        // when
    +        final MultiMap resultHeaders = requestEnricher.enrichHeaders(bidderRequestHeaders,
    +                originalHeaders, BidRequest.builder().build());
    +
    +        // then
    +        assertThat(resultHeaders.contains("Sec-GPC")).isTrue();
    +        assertThat(resultHeaders.getAll("Sec-GPC")).hasSize(1);
    +        assertThat(resultHeaders.get("Sec-GPC")).isEqualTo("0");
    +    }
    +
    +    @Test
    +    public void shouldCreateXPrebidHeaderForOutgoingRequest() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .channel(ExtRequestPrebidChannel.of("pbjs", "4.39"))
    +                        .build()))
    +                .app(App.builder()
    +                        .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null))
    +                        .build())
    +                .build();
    +
    +        // when
    +        final MultiMap resultHeaders = requestEnricher.enrichHeaders(new CaseInsensitiveHeaders(),
    +                CaseInsensitiveMultiMap.empty(), bidRequest);
    +
    +        // then
    +        final MultiMap expectedHeaders = new CaseInsensitiveHeaders();
    +        expectedHeaders.add("x-prebid", "pbjs/4.39,prebid-mobile/1.2.3,pbs-java/1.00");
    +        assertThat(isEqualsMultiMaps(resultHeaders, expectedHeaders)).isTrue();
    +    }
    +
    +    private static boolean isEqualsMultiMaps(MultiMap left, MultiMap right) {
    +        return left.size() == right.size() && left.entries().stream()
    +                .allMatch(entry -> right.contains(entry.getKey(), entry.getValue(), true));
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java
    index abacefbf616..fa714c45ab4 100644
    --- a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java
    +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java
    @@ -1,25 +1,39 @@
     package org.prebid.server.bidder;
     
     import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Deal;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Pmp;
    +import com.iab.openrtb.response.Bid;
     import io.vertx.core.Future;
     import io.vertx.core.MultiMap;
    +import io.vertx.core.Promise;
     import io.vertx.core.http.CaseInsensitiveHeaders;
     import io.vertx.core.http.HttpMethod;
    +import io.vertx.core.http.HttpServerRequest;
    +import io.vertx.ext.web.RoutingContext;
    +import lombok.AllArgsConstructor;
     import org.junit.Before;
     import org.junit.Rule;
     import org.junit.Test;
    +import org.mockito.ArgumentCaptor;
    +import org.mockito.ArgumentMatcher;
     import org.mockito.BDDMockito;
     import org.mockito.Mock;
     import org.mockito.junit.MockitoJUnit;
     import org.mockito.junit.MockitoRule;
     import org.prebid.server.VertxTest;
    +import org.prebid.server.auction.model.BidderRequest;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderError;
     import org.prebid.server.bidder.model.BidderSeatBid;
    +import org.prebid.server.bidder.model.HttpCall;
     import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
     import org.prebid.server.bidder.model.Result;
     import org.prebid.server.execution.Timeout;
     import org.prebid.server.execution.TimeoutFactory;
    +import org.prebid.server.model.CaseInsensitiveMultiMap;
     import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall;
     import org.prebid.server.vertx.http.HttpClient;
     import org.prebid.server.vertx.http.model.HttpClientResponse;
    @@ -27,24 +41,30 @@
     import java.time.Clock;
     import java.time.Instant;
     import java.time.ZoneId;
    +import java.util.Arrays;
     import java.util.List;
    +import java.util.Map;
     import java.util.concurrent.TimeoutException;
    +import java.util.stream.Collectors;
     
     import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
     import static java.util.Collections.singletonList;
    +import static java.util.Collections.singletonMap;
     import static org.apache.commons.lang3.StringUtils.EMPTY;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.mockito.ArgumentMatchers.any;
    -import static org.mockito.ArgumentMatchers.argThat;
     import static org.mockito.ArgumentMatchers.eq;
    +import static org.mockito.ArgumentMatchers.same;
     import static org.mockito.BDDMockito.given;
     import static org.mockito.Mockito.anyLong;
     import static org.mockito.Mockito.anyString;
     import static org.mockito.Mockito.isNull;
    +import static org.mockito.Mockito.never;
     import static org.mockito.Mockito.times;
     import static org.mockito.Mockito.verify;
     import static org.mockito.Mockito.verifyZeroInteractions;
    +import static org.mockito.Mockito.when;
     
     public class HttpBidderRequesterTest extends VertxTest {
     
    @@ -54,23 +74,34 @@ public class HttpBidderRequesterTest extends VertxTest {
         @Mock
         private Bidder bidder;
         @Mock
    -    private TimeoutBidder timeoutBidder;
    -    @Mock
         private HttpClient httpClient;
    +    @Mock
    +    private BidderErrorNotifier bidderErrorNotifier;
    +    @Mock
    +    private HttpBidderRequestEnricher requestEnricher;
    +    @Mock
    +    private RoutingContext routingContext;
    +    @Mock
    +    private HttpServerRequest httpServerRequest;
     
    -    private HttpBidderRequester bidderHttpConnector;
    +    private HttpBidderRequester httpBidderRequester;
     
         private Timeout timeout;
         private Timeout expiredTimeout;
     
         @Before
         public void setUp() {
    +        given(bidderErrorNotifier.processTimeout(any(), any())).will(invocation -> invocation.getArgument(0));
    +        given(routingContext.request()).willReturn(httpServerRequest);
    +        given(httpServerRequest.headers()).willReturn(new CaseInsensitiveHeaders());
    +        given(requestEnricher.enrichHeaders(any(), any(), any())).willReturn(new CaseInsensitiveHeaders());
    +
             final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
             final TimeoutFactory timeoutFactory = new TimeoutFactory(clock);
             timeout = timeoutFactory.create(500L);
             expiredTimeout = timeoutFactory.create(clock.instant().minusMillis(1500L).toEpochMilli(), 1000L);
     
    -        bidderHttpConnector = new HttpBidderRequester(httpClient, null);
    +        httpBidderRequester = new HttpBidderRequester(httpClient, null, bidderErrorNotifier, requestEnricher);
         }
     
         @Test
    @@ -78,9 +109,12 @@ public void shouldReturnFailedToRequestBidsErrorWhenBidderReturnsEmptyHttpReques
             // given
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(emptyList(), emptyList()));
     
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
             // when
             final BidderSeatBid bidderSeatBid =
    -                bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), timeout, false).result();
    +                httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false)
    +                        .result();
     
             // then
             assertThat(bidderSeatBid.getBids()).isEmpty();
    @@ -96,9 +130,12 @@ public void shouldTolerateBidderReturningErrorsAndNoHttpRequests() {
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(emptyList(),
                     asList(BidderError.badInput("error1"), BidderError.badInput("error2"))));
     
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
             // when
             final BidderSeatBid bidderSeatBid =
    -                bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), timeout, false).result();
    +                httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false)
    +                        .result();
     
             // then
             assertThat(bidderSeatBid.getBids()).isEmpty();
    @@ -108,27 +145,69 @@ public void shouldTolerateBidderReturningErrorsAndNoHttpRequests() {
         }
     
         @Test
    -    public void shouldSendPopulatedPostRequest() {
    +    public void shouldPassStoredResponseToBidderMakeBidsMethodAndReturnSeatBids() {
             // given
    -        givenHttpClientReturnsResponse(200, null);
    -
             final MultiMap headers = new CaseInsensitiveHeaders();
    +        headers.add("header1", "value1");
    +        headers.add("header2", "value2");
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri("uri")
    -                        .body("requestBody")
    -                        .headers(headers)
    -                        .build()),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri("uri")
    +                                .body("requestBody")
    +                                .headers(headers)
    +                                .build()),
                     emptyList()));
    +
    +        final List bids = asList(BidderBid.of(null, null, null), BidderBid.of(null, null, null));
    +        given(bidder.makeBids(any(), any())).willReturn(Result.of(bids, emptyList()));
    +
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", "storedResponse", BidRequest.builder().build());
    +
    +        // when
    +        final BidderSeatBid bidderSeatBid = httpBidderRequester
    +                .requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false)
    +                .result();
    +
    +        // then
    +        verifyZeroInteractions(httpClient);
    +        final ArgumentCaptor> httpCallArgumentCaptor = ArgumentCaptor.forClass(HttpCall.class);
    +        verify(bidder).makeBids(httpCallArgumentCaptor.capture(), any());
    +        assertThat(httpCallArgumentCaptor.getValue().getResponse())
    +                .extracting(HttpResponse::getBody)
    +                .containsOnly("storedResponse");
    +        assertThat(bidderSeatBid.getBids()).containsOnlyElementsOf(bids);
    +    }
    +
    +    @Test
    +    public void shouldMakeRequestToBidderWhenStoredResponseDefinedButBidderCreatesMoreThanOneRequest() {
    +        // given
    +        givenHttpClientReturnsResponse(200, null);
    +        final MultiMap headers = new CaseInsensitiveHeaders();
             headers.add("header1", "value1");
             headers.add("header2", "value2");
    +        given(bidder.makeHttpRequests(any())).willReturn(Result.of(asList(
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri("uri")
    +                                .body("requestBody")
    +                                .headers(headers)
    +                                .build(),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri("uri")
    +                                .body("requestBody2")
    +                                .headers(headers)
    +                                .build()),
    +                emptyList()));
    +
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", "storedResponse", BidRequest.builder().build());
     
             // when
    -        bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), timeout, false);
    +        httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false);
     
             // then
    -        verify(httpClient).request(eq(HttpMethod.POST), eq("uri"), eq(headers), eq("requestBody"), eq(500L));
    +        verify(httpClient, times(2)).request(any(), anyString(), any(), anyString(), anyLong());
         }
     
         @Test
    @@ -137,17 +216,19 @@ public void shouldSendPopulatedGetRequestWithoutBody() {
             givenHttpClientReturnsResponse(200, null);
     
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    -                HttpRequest.builder()
    -                        .method(HttpMethod.GET)
    -                        .uri("uri")
    -                        .build()),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.GET)
    +                                .uri("uri")
    +                                .build()),
                     emptyList()));
     
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
             // when
    -        bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), timeout, false);
    +        httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false);
     
             // then
    -        verify(httpClient).request(any(), anyString(), any(), isNull(), anyLong());
    +        verify(httpClient).request(any(), anyString(), any(), (String) isNull(), anyLong());
         }
     
         @Test
    @@ -156,174 +237,376 @@ public void shouldSendMultipleRequests() {
             givenHttpClientReturnsResponse(200, null);
     
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(asList(
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build(),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build()),
    +                emptyList()));
    +
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
    +        // when
    +        httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false);
    +
    +        // then
    +        verify(httpClient, times(2)).request(any(), anyString(), any(), anyString(), anyLong());
    +    }
    +
    +    @Test
    +    public void shouldReturnBidsCreatedByBidder() {
    +        // given
    +        given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build()),
    +                emptyList()));
    +
    +        givenHttpClientReturnsResponse(200, "responseBody");
    +
    +        final List bids = asList(BidderBid.of(null, null, null), BidderBid.of(null, null, null));
    +        given(bidder.makeBids(any(), any())).willReturn(Result.of(bids, emptyList()));
    +
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
    +        // when
    +        final BidderSeatBid bidderSeatBid =
    +                httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false)
    +                        .result();
    +
    +        // then
    +        assertThat(bidderSeatBid.getBids()).containsOnlyElementsOf(bids);
    +    }
    +
    +    @Test
    +    public void shouldNotWaitForResponsesWhenAllDealsIsGathered() {
    +        // given
    +        httpBidderRequester = new HttpBidderRequester(httpClient, new DealsBidderRequestCompletionTrackerFactory(),
    +                bidderErrorNotifier, requestEnricher);
    +
    +        final BidRequest bidRequest = bidRequestWithDeals("deal1", "deal2");
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, bidRequest);
    +
    +        given(bidder.makeHttpRequests(any())).willReturn(Result.of(Arrays.asList(
    +                HttpRequest.builder()
    +                        .method(HttpMethod.POST)
    +                        .uri(EMPTY)
    +                        .body("r1")
    +                        .headers(new CaseInsensitiveHeaders())
    +                        .payload(bidRequestWithDeals("deal1"))
    +                        .build(),
    +                HttpRequest.builder()
    +                        .method(HttpMethod.POST)
    +                        .uri(EMPTY)
    +                        .body("r2")
    +                        .headers(new CaseInsensitiveHeaders())
    +                        .payload(bidRequestWithDeals("deal1"))
    +                        .build(),
                     HttpRequest.builder()
                             .method(HttpMethod.POST)
                             .uri(EMPTY)
    -                        .body(EMPTY)
    +                        .body("r3")
                             .headers(new CaseInsensitiveHeaders())
    +                        .payload(bidRequestWithDeals("deal2"))
                             .build(),
                     HttpRequest.builder()
                             .method(HttpMethod.POST)
                             .uri(EMPTY)
    -                        .body(EMPTY)
    +                        .body("r4")
                             .headers(new CaseInsensitiveHeaders())
    +                        .payload(bidRequestWithDeals("deal1"))
                             .build()),
                     emptyList()));
     
    +        final HttpClientResponse respWithDeal1 = HttpClientResponse.of(200, null,
    +                "{\"seatbid\":[{\"bid\":[{\"dealid\":\"deal1\"}]}]}");
    +        final HttpClientResponse respWithDeal2 = HttpClientResponse.of(200, null,
    +                "{\"seatbid\":[{\"bid\":[{\"dealid\":\"deal2\"}]}]}");
    +
    +        given(httpClient.request(any(), anyString(), any(), eq("r1"), anyLong()))
    +                .willReturn(Future.succeededFuture(respWithDeal1));
    +        given(httpClient.request(any(), anyString(), any(), eq("r2"), anyLong()))
    +                .willReturn(Promise.promise().future());
    +        given(httpClient.request(any(), anyString(), any(), eq("r3"), anyLong()))
    +                .willReturn(Future.succeededFuture(respWithDeal2));
    +        given(httpClient.request(any(), anyString(), any(), eq("r4"), anyLong()))
    +                .willReturn(Promise.promise().future());
    +
    +        final BidderBid bidderBidDeal1 = BidderBid.of(Bid.builder().impid("deal1").dealid("deal1").build(), null, null);
    +        final BidderBid bidderBidDeal2 = BidderBid.of(Bid.builder().impid("deal2").dealid("deal2").build(), null, null);
    +        given(bidder.makeBids(any(), any())).willReturn(
    +                Result.of(singletonList(bidderBidDeal1), emptyList()),
    +                Result.of(singletonList(bidderBidDeal2), emptyList()));
    +
             // when
    -        bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), timeout, false);
    +        final BidderSeatBid bidderSeatBid =
    +                httpBidderRequester.requestBids(
    +                        bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false)
    +                        .result();
     
             // then
    -        verify(httpClient, times(2)).request(any(), anyString(), any(), any(), anyLong());
    +        verify(bidder, times(1)).makeHttpRequests(any());
    +        verify(httpClient, times(4)).request(any(), any(), any(), anyString(), anyLong());
    +        verify(bidder, times(2)).makeBids(any(), any());
    +
    +        assertThat(bidderSeatBid.getBids()).containsOnly(bidderBidDeal1, bidderBidDeal2);
         }
     
         @Test
    -    public void shouldReturnBidsCreatedByBidder() {
    +    public void shouldFinishWhenAllDealRequestsAreFinishedAndNoDealsProvided() {
             // given
    -        given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    +        final BidRequest bidRequest = bidRequestWithDeals("deal1", "deal2", "deal2");
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, bidRequest);
    +
    +        given(bidder.makeHttpRequests(any())).willReturn(Result.of(Arrays.asList(
    +                HttpRequest.builder()
    +                        .method(HttpMethod.POST)
    +                        .uri(EMPTY)
    +                        .body("r1")
    +                        .headers(new CaseInsensitiveHeaders())
    +                        .payload(bidRequestWithDeals("deal1"))
    +                        .build(),
                     HttpRequest.builder()
                             .method(HttpMethod.POST)
                             .uri(EMPTY)
    -                        .body(EMPTY)
    +                        .body("r2")
                             .headers(new CaseInsensitiveHeaders())
    +                        .payload(bidRequestWithDeals("deal2"))
    +                        .build(),
    +                HttpRequest.builder()
    +                        .method(HttpMethod.POST)
    +                        .uri(EMPTY)
    +                        .body("r3")
    +                        .headers(new CaseInsensitiveHeaders())
    +                        .payload(bidRequestWithDeals("deal2"))
    +                        .build(),
    +                HttpRequest.builder()
    +                        .method(HttpMethod.POST)
    +                        .uri(EMPTY)
    +                        .body("r4")
    +                        .headers(new CaseInsensitiveHeaders())
    +                        .payload(bidRequestWithDeals("deal2"))
                             .build()),
                     emptyList()));
     
             givenHttpClientReturnsResponse(200, "responseBody");
     
    -        final List bids = asList(BidderBid.of(null, null, null), BidderBid.of(null, null, null));
    -        given(bidder.makeBids(any(), any())).willReturn(Result.of(bids, emptyList()));
    +        final BidderBid bidderBid = BidderBid.of(Bid.builder().dealid("deal2").build(), null, null);
    +        given(bidder.makeBids(any(), any())).willReturn(Result.of(singletonList(bidderBid), emptyList()));
     
             // when
             final BidderSeatBid bidderSeatBid =
    -                bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), timeout, false).result();
    +                httpBidderRequester.requestBids(
    +                        bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false)
    +                        .result();
     
             // then
    -        assertThat(bidderSeatBid.getBids()).containsOnlyElementsOf(bids);
    +        verify(bidder, times(1)).makeHttpRequests(any());
    +        verify(httpClient, times(4)).request(any(), any(), any(), anyString(), anyLong());
    +        verify(bidder, times(4)).makeBids(any(), any());
    +
    +        assertThat(bidderSeatBid.getBids()).contains(bidderBid, bidderBid, bidderBid, bidderBid);
         }
     
         @Test
         public void shouldReturnFullDebugInfoIfDebugEnabled() {
             // given
    +        final MultiMap headers = new CaseInsensitiveHeaders().add("headerKey", "headerValue");
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(asList(
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri("uri1")
    -                        .body("requestBody1")
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build(),
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri("uri2")
    -                        .body("requestBody2")
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build()),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri("uri1")
    +                                .body("requestBody1")
    +                                .headers(headers)
    +                                .build(),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri("uri2")
    +                                .body("requestBody2")
    +                                .headers(headers)
    +                                .build()),
                     emptyList()));
     
    +        given(requestEnricher.enrichHeaders(any(), any(), any())).willReturn(headers);
    +
             givenHttpClientReturnsResponses(
                     HttpClientResponse.of(200, null, "responseBody1"),
                     HttpClientResponse.of(200, null, "responseBody2"));
     
             given(bidder.makeBids(any(), any())).willReturn(Result.of(emptyList(), emptyList()));
     
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
             // when
             final BidderSeatBid bidderSeatBid =
    -                bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), timeout, true).result();
    +                httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), true)
    +                        .result();
     
             // then
             assertThat(bidderSeatBid.getHttpCalls()).hasSize(2).containsOnly(
                     ExtHttpCall.builder().uri("uri1").requestbody("requestBody1").responsebody("responseBody1")
    +                        .requestheaders(singletonMap("headerKey", singletonList("headerValue")))
                             .status(200).build(),
                     ExtHttpCall.builder().uri("uri2").requestbody("requestBody2").responsebody("responseBody2")
    +                        .requestheaders(singletonMap("headerKey", singletonList("headerValue")))
                             .status(200).build());
         }
     
    +    @Test
    +    public void shouldNotReturnSensitiveHeadersInFullDebugInfo() {
    +        // given
    +        final CaseInsensitiveHeaders headers = new CaseInsensitiveHeaders();
    +        headers.add("headerKey", "headerValue");
    +        headers.add("Authorization", "authorizationValue");
    +        given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri("uri1")
    +                                .body("requestBody1")
    +                                .headers(headers)
    +                                .build()),
    +                emptyList()));
    +        given(requestEnricher.enrichHeaders(any(), any(), any())).willReturn(headers);
    +
    +        givenHttpClientReturnsResponses(
    +                HttpClientResponse.of(200, null, "responseBody1"));
    +
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
    +        // when
    +        final BidderSeatBid bidderSeatBid =
    +                httpBidderRequester
    +                        .requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), true).result();
    +
    +        // then
    +        assertThat(bidderSeatBid.getHttpCalls())
    +                .extracting(ExtHttpCall::getRequestheaders)
    +                .flatExtracting(Map::keySet)
    +                .containsExactly("headerKey");
    +    }
    +
         @Test
         public void shouldReturnPartialDebugInfoIfDebugEnabledAndGlobalTimeoutAlreadyExpired() {
             // given
    +        final MultiMap headers = new CaseInsensitiveHeaders().add("headerKey", "headerValue");
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri("uri1")
    -                        .body("requestBody1")
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build()),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri("uri1")
    +                                .headers(headers)
    +                                .body("requestBody1")
    +                                .build()),
                     emptyList()));
     
    +        given(requestEnricher.enrichHeaders(any(), any(), any())).willReturn(headers);
    +
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
             // when
             final BidderSeatBid bidderSeatBid =
    -                bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), expiredTimeout, true).result();
    +                httpBidderRequester.requestBids(bidder, bidderRequest, expiredTimeout, CaseInsensitiveMultiMap.empty(),
    +                        true).result();
     
             // then
             assertThat(bidderSeatBid.getHttpCalls()).hasSize(1).containsOnly(
    -                ExtHttpCall.builder().uri("uri1").requestbody("requestBody1").build());
    +                ExtHttpCall.builder().uri("uri1").requestbody("requestBody1")
    +                        .requestheaders(singletonMap("headerKey", singletonList("headerValue")))
    +                        .build());
         }
     
         @Test
         public void shouldReturnPartialDebugInfoIfDebugEnabledAndHttpErrorOccurs() {
             // given
    +        final MultiMap headers = new CaseInsensitiveHeaders().add("headerKey", "headerValue");
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri("uri1")
    -                        .body("requestBody1")
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build()),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri("uri1")
    +                                .body("requestBody1")
    +                                .headers(headers)
    +                                .build()),
                     emptyList()));
     
    +        given(requestEnricher.enrichHeaders(any(), any(), any())).willReturn(headers);
    +
             givenHttpClientProducesException(new RuntimeException("Request exception"));
     
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
             // when
             final BidderSeatBid bidderSeatBid =
    -                bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), timeout, true).result();
    +                httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), true)
    +                        .result();
     
             // then
             assertThat(bidderSeatBid.getHttpCalls()).hasSize(1).containsOnly(
    -                ExtHttpCall.builder().uri("uri1").requestbody("requestBody1").build());
    +                ExtHttpCall.builder().uri("uri1").requestbody("requestBody1")
    +                        .requestheaders(singletonMap("headerKey", singletonList("headerValue")))
    +                        .build());
         }
     
         @Test
         public void shouldReturnFullDebugInfoIfDebugEnabledAndErrorStatus() {
             // given
    +        final MultiMap headers = new CaseInsensitiveHeaders().add("headerKey", "headerValue");
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri("uri1")
    -                        .body("requestBody1")
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build()),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri("uri1")
    +                                .body("requestBody1")
    +                                .headers(headers)
    +                                .build()),
                     emptyList()));
     
    +        given(requestEnricher.enrichHeaders(any(), any(), any())).willReturn(headers);
    +
             givenHttpClientReturnsResponses(HttpClientResponse.of(500, null, "responseBody1"));
     
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
             // when
             final BidderSeatBid bidderSeatBid =
    -                bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), timeout, true).result();
    +                httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), true)
    +                        .result();
     
             // then
             assertThat(bidderSeatBid.getHttpCalls()).hasSize(1).containsOnly(
                     ExtHttpCall.builder().uri("uri1").requestbody("requestBody1").responsebody("responseBody1")
    +                        .requestheaders(singletonMap("headerKey", singletonList("headerValue")))
                             .status(500).build());
             assertThat(bidderSeatBid.getErrors()).hasSize(1)
                     .extracting(BidderError::getMessage).containsOnly(
    -                "Unexpected status code: 500. Run with request.test = 1 for more info");
    +                        "Unexpected status code: 500. Run with request.test = 1 for more info");
         }
     
         @Test
         public void shouldTolerateAlreadyExpiredGlobalTimeout() {
             // given
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri(EMPTY)
    -                        .body(EMPTY)
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build()),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build()),
                     emptyList()));
     
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
             // when
             final BidderSeatBid bidderSeatBid =
    -                bidderHttpConnector.requestBids(bidder, BidRequest.builder().build(), expiredTimeout, false).result();
    +                httpBidderRequester.requestBids(bidder, bidderRequest, expiredTimeout, CaseInsensitiveMultiMap.empty(),
    +                        false).result();
     
             // then
             assertThat(bidderSeatBid.getErrors()).hasSize(1)
    @@ -333,75 +616,78 @@ public void shouldTolerateAlreadyExpiredGlobalTimeout() {
         }
     
         @Test
    -    public void shouldSendTimeoutNotificationIfTimeoutBidder() {
    +    public void shouldNotifyBidderOfTimeout() {
             // given
    -        given(timeoutBidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri("uri1")
    -                        .body("requestBody1")
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build()),
    -                emptyList()));
    +        final HttpRequest httpRequest = HttpRequest.builder()
    +                .method(HttpMethod.POST)
    +                .uri(EMPTY)
    +                .body(EMPTY)
    +                .build();
    +
    +        given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(httpRequest), null));
    +
    +        given(httpClient.request(any(), anyString(), any(), anyString(), anyLong()))
    +                // bidder request
    +                .willReturn(Future.failedFuture(new TimeoutException("Timeout exception")));
     
    -        givenHttpClientProducesException(new TimeoutException("Timeout error"));
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
     
             // when
    -        bidderHttpConnector.requestBids(timeoutBidder, BidRequest.builder().build(), timeout, false);
    +        httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false);
     
             // then
    -        verify(timeoutBidder).makeTimeoutNotification(any());
    +        verify(bidderErrorNotifier).processTimeout(any(), same(bidder));
         }
     
         @Test
         public void shouldTolerateMultipleErrors() {
             // given
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(asList(
    -                // this request will fail with response exception
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri(EMPTY)
    -                        .body(EMPTY)
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build(),
    -                // this request will fail with timeout
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri(EMPTY)
    -                        .body(EMPTY)
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build(),
    -                // this request will fail with 500 status
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri(EMPTY)
    -                        .body(EMPTY)
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build(),
    -                // this request will fail with 400 status
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri(EMPTY)
    -                        .body(EMPTY)
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build(),
    -                // this request will get 204 status
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri(EMPTY)
    -                        .body(EMPTY)
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build(),
    -                // finally this request will succeed
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri(EMPTY)
    -                        .body(EMPTY)
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build()),
    +                        // this request will fail with response exception
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build(),
    +                        // this request will fail with timeout
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build(),
    +                        // this request will fail with 500 status
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build(),
    +                        // this request will fail with 400 status
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build(),
    +                        // this request will get 204 status
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build(),
    +                        // finally this request will succeed
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build()),
                     singletonList(BidderError.badInput("makeHttpRequestsError"))));
    -
    -        given(httpClient.request(any(), anyString(), any(), any(), anyLong()))
    +        when(requestEnricher.enrichHeaders(any(), any(), any())).thenAnswer(invocation -> new CaseInsensitiveHeaders());
    +        given(httpClient.request(any(), anyString(), any(), anyString(), anyLong()))
                     // simulate response error for the first request
                     .willReturn(Future.failedFuture(new RuntimeException("Response exception")))
                     // simulate timeout for the second request
    @@ -416,18 +702,20 @@ public void shouldTolerateMultipleErrors() {
                     .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, EMPTY)));
     
             given(bidder.makeBids(any(), any())).willReturn(
    -                Result.of(singletonList(BidderBid.of(null, null, null)),
    +                Result.of(singletonList(BidderBid.of(Bid.builder().impid("123").build(), null, null)),
                             singletonList(BidderError.badServerResponse("makeBidsError"))));
     
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().build());
    +
             // when
    -        final BidderSeatBid bidderSeatBid = bidderHttpConnector
    -                .requestBids(bidder, BidRequest.builder().test(1).build(), timeout, false)
    +        final BidderSeatBid bidderSeatBid = httpBidderRequester
    +                .requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false)
                     .result();
     
             // then
    -        // only two calls are expected (200 and 204) since other requests have failed with errors.
    -        verify(bidder, times(2)).makeBids(any(), any());
    -        assertThat(bidderSeatBid.getBids()).hasSize(2);
    +        // only one calls is expected (200) since other requests have failed with errors.
    +        verify(bidder, times(1)).makeBids(any(), any());
    +        assertThat(bidderSeatBid.getBids()).hasSize(1);
             assertThat(bidderSeatBid.getErrors()).containsOnly(
                     BidderError.badInput("makeHttpRequestsError"),
                     BidderError.generic("Response exception"),
    @@ -438,43 +726,73 @@ public void shouldTolerateMultipleErrors() {
         }
     
         @Test
    -    public void shouldPassEmptyJsonResponseBodyToMakeBidsIfResponseStatusIs204() {
    +    public void shouldNotMakeBidsIfResponseStatusIs204() {
             // given
             given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList(
    -                HttpRequest.builder()
    -                        .method(HttpMethod.POST)
    -                        .uri(EMPTY)
    -                        .body(EMPTY)
    -                        .headers(new CaseInsensitiveHeaders())
    -                        .build()),
    +                        HttpRequest.builder()
    +                                .method(HttpMethod.POST)
    +                                .uri(EMPTY)
    +                                .body(EMPTY)
    +                                .headers(new CaseInsensitiveHeaders())
    +                                .build()),
                     emptyList()));
     
             givenHttpClientReturnsResponse(204, EMPTY);
     
    +        final BidderRequest bidderRequest = BidderRequest.of("bidder", null, BidRequest.builder().test(1).build());
    +
             // when
    -        bidderHttpConnector.requestBids(bidder, BidRequest.builder().test(1).build(), timeout, false);
    +        httpBidderRequester.requestBids(bidder, bidderRequest, timeout, CaseInsensitiveMultiMap.empty(), false);
     
             // then
    -        verify(bidder).makeBids(argThat(httpCall -> httpCall.getResponse().getBody().equals("{}")), any());
    +        verify(bidder, never()).makeBids(any(), any());
    +    }
    +
    +    private static BidRequest bidRequestWithDeals(String... ids) {
    +        final List impsWithDeals = Arrays.stream(ids)
    +                .map(HttpBidderRequesterTest::impWithDeal)
    +                .collect(Collectors.toList());
    +        return BidRequest.builder().imp(impsWithDeals).build();
    +    }
    +
    +    private static Imp impWithDeal(String dealId) {
    +        return Imp.builder()
    +                .id(dealId)
    +                .pmp(Pmp.builder()
    +                        .deals(singletonList(Deal.builder().id(dealId).build()))
    +                        .build())
    +                .build();
         }
     
         private void givenHttpClientReturnsResponse(int statusCode, String response) {
    -        given(httpClient.request(any(), anyString(), any(), any(), anyLong()))
    +        given(httpClient.request(any(), anyString(), any(), (String) any(), anyLong()))
                     .willReturn(Future.succeededFuture(HttpClientResponse.of(statusCode, null, response)));
         }
     
         private void givenHttpClientProducesException(Throwable throwable) {
    -        given(httpClient.request(any(), anyString(), any(), any(), anyLong()))
    +        given(httpClient.request(any(), anyString(), any(), anyString(), anyLong()))
                     .willReturn(Future.failedFuture(throwable));
         }
     
         private void givenHttpClientReturnsResponses(HttpClientResponse... httpClientResponses) {
             BDDMockito.BDDMyOngoingStubbing> stubbing =
    -                given(httpClient.request(any(), anyString(), any(), any(), anyLong()));
    +                given(httpClient.request(any(), anyString(), any(), anyString(), anyLong()));
     
             // setup multiple answers
             for (HttpClientResponse httpClientResponse : httpClientResponses) {
                 stubbing = stubbing.willReturn(Future.succeededFuture(httpClientResponse));
             }
         }
    +
    +    @AllArgsConstructor
    +    public static class MultiMapMatcher implements ArgumentMatcher {
    +
    +        private final MultiMap left;
    +
    +        @Override
    +        public boolean matches(MultiMap right) {
    +            return left.size() == right.size() && left.entries().stream()
    +                    .allMatch(entry -> right.contains(entry.getKey(), entry.getValue(), true));
    +        }
    +    }
     }
    diff --git a/src/test/java/org/prebid/server/bidder/OpenrtbAdapterTest.java b/src/test/java/org/prebid/server/bidder/OpenrtbAdapterTest.java
    deleted file mode 100644
    index d1da3a67b76..00000000000
    --- a/src/test/java/org/prebid/server/bidder/OpenrtbAdapterTest.java
    +++ /dev/null
    @@ -1,110 +0,0 @@
    -package org.prebid.server.bidder;
    -
    -import com.iab.openrtb.request.Imp;
    -import org.junit.Test;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.proto.request.Video;
    -import org.prebid.server.proto.response.MediaType;
    -
    -import java.util.Collections;
    -import java.util.EnumSet;
    -import java.util.List;
    -import java.util.Set;
    -import java.util.stream.Collectors;
    -import java.util.stream.Stream;
    -
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.singleton;
    -import static java.util.Collections.singletonList;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatCode;
    -import static org.assertj.core.api.Assertions.assertThatThrownBy;
    -
    -public class OpenrtbAdapterTest {
    -
    -    @Test
    -    public void validateAdUnitBidsMediaTypesShouldFailWhenMediaTypeIsVideoAndMimesListIsEmpty() {
    -        // given
    -        final List adUnitBids = singletonList(AdUnitBid.builder()
    -                .mediaTypes(singleton(MediaType.video))
    -                .video(Video.builder()
    -                        .mimes(emptyList())
    -                        .build())
    -                .build());
    -
    -        // when and then
    -        assertThatThrownBy(() -> OpenrtbAdapter.validateAdUnitBidsMediaTypes(adUnitBids,
    -                Collections.unmodifiableSet(EnumSet.of(MediaType.banner, MediaType.video))))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid AdUnit: VIDEO media type with no video data");
    -    }
    -
    -    @Test
    -    public void validateAdUnitBidsMediaTypesShouldNotThrowExceptionWhenVideoTypeIsNotValidAndNotAllowedMediaType() {
    -        // given
    -        final List adUnitBids = singletonList(AdUnitBid.builder()
    -                .mediaTypes(singleton(MediaType.video))
    -                .video(Video.builder()
    -                        .mimes(emptyList())
    -                        .build())
    -                .build());
    -
    -        // when and then
    -        assertThatCode(() -> OpenrtbAdapter.validateAdUnitBidsMediaTypes(adUnitBids, singleton(MediaType.banner)))
    -                .doesNotThrowAnyException();
    -    }
    -
    -    @Test
    -    public void allowedMediaTypesShouldReturnExpectedMediaTypes() {
    -        // given
    -        final AdUnitBid adUnitBid = AdUnitBid.builder()
    -                .mediaTypes(singleton(MediaType.video))
    -                .build();
    -        final Set mediaTypes = Stream.of(MediaType.banner, MediaType.video)
    -                .collect(Collectors.toSet());
    -
    -        // when
    -        final Set allowedMediaTypes = OpenrtbAdapter.allowedMediaTypes(adUnitBid, mediaTypes);
    -
    -        // then
    -        assertThat(allowedMediaTypes).containsOnly(MediaType.video);
    -    }
    -
    -    @Test
    -    public void validateImpsShouldFailOnNullOrEmptyArgument() {
    -        assertThatThrownBy(() -> OpenrtbAdapter.validateImps(null))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("openRTB bids need at least one Imp");
    -
    -        assertThatThrownBy(() -> OpenrtbAdapter.validateImps(emptyList()))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("openRTB bids need at least one Imp");
    -    }
    -
    -    @Test
    -    public void validateImpsShouldAllowListOfImps() {
    -        assertThatCode(() -> OpenrtbAdapter.validateImps(singletonList(Imp.builder().build())))
    -                .doesNotThrowAnyException();
    -    }
    -
    -    @Test
    -    public void lookupBidShouldFailWhenBidNotFound() {
    -        assertThatThrownBy(() -> OpenrtbAdapter.lookupBid(emptyList(), null))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Unknown ad unit code 'null'");
    -    }
    -
    -    @Test
    -    public void lookupBidShouldReturnExpectedValue() {
    -        // given
    -        final List adUnitBids = singletonList(AdUnitBid.builder().adUnitCode("adUnitCode1").build());
    -
    -        // when
    -        final AdUnitBid adUnitBid = OpenrtbAdapter.lookupBid(adUnitBids, "adUnitCode1");
    -
    -        // then
    -        assertThat(adUnitBid).isNotNull()
    -                .isEqualTo(AdUnitBid.builder().adUnitCode("adUnitCode1").build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/UsersyncInfoAssemblerTest.java b/src/test/java/org/prebid/server/bidder/UsersyncInfoAssemblerTest.java
    index 6b29fc60b55..ea90a395d58 100644
    --- a/src/test/java/org/prebid/server/bidder/UsersyncInfoAssemblerTest.java
    +++ b/src/test/java/org/prebid/server/bidder/UsersyncInfoAssemblerTest.java
    @@ -13,8 +13,8 @@ public class UsersyncInfoAssemblerTest {
         public void assembleUsersyncInfoShouldAppendRedirectUrlToUsersyncUrl() {
             // given and when
             final UsersyncInfo result = UsersyncInfoAssembler
    -                .from(new Usersyncer(null, "http://url/redirect=", "redirectUrl",
    -                        "http://localhost:8000", null, false)).assemble();
    +                .from(createUsersyncMethod("http://url/redirect=", "http://localhost:8000redirectUrl"))
    +                .assemble();
     
             // then
             assertThat(result.getUrl()).isEqualTo("http://url/redirect=http%3A%2F%2Flocalhost%3A8000redirectUrl");
    @@ -24,8 +24,10 @@ public void assembleUsersyncInfoShouldAppendRedirectUrlToUsersyncUrl() {
         public void assembleUsersyncInfoShouldAppendEncodedRedirectUrlAndNotEncodedQueryParamsToUsersyncUrl() {
             // given and when
             final UsersyncInfo result = UsersyncInfoAssembler
    -                .from(new Usersyncer(null, "http://url/redirect=", "/setuid?gdpr={{gdpr}}?gdpr={{gdpr}}",
    -                        "http://localhost:8000", null, false)).assemble();
    +                .from(createUsersyncMethod(
    +                        "http://url/redirect=",
    +                        "http://localhost:8000/setuid?gdpr={{gdpr}}?gdpr={{gdpr}}"))
    +                .assemble();
     
             // then
             assertThat(result.getUrl()).isEqualTo(
    @@ -36,7 +38,8 @@ public void assembleUsersyncInfoShouldAppendEncodedRedirectUrlAndNotEncodedQuery
         public void assembleUsersyncInfoShouldIgnoreRedirectUrlIfNotDefined() {
             // given and when
             final UsersyncInfo result = UsersyncInfoAssembler
    -                .from(new Usersyncer(null, "http://url/redirect=", null, null, null, false)).assemble();
    +                .from(createUsersyncMethod("http://url/redirect=", null))
    +                .assemble();
     
             // then
             assertThat(result.getUrl()).isEqualTo("http://url/redirect=");
    @@ -46,11 +49,12 @@ public void assembleUsersyncInfoShouldIgnoreRedirectUrlIfNotDefined() {
         public void assembleWithPrivacyShouldCreatePrivacyAwareUsersyncInfo() {
             // given and when
             final UsersyncInfo result = UsersyncInfoAssembler
    -                .from(new Usersyncer(null, "http://url?redir=%26gdpr%3D{{gdpr}}"
    -                        + "%26gdpr_consent%3D{{gdpr_consent}}"
    -                        + "%26us_privacy={{us_privacy}}",
    -                        null, null, null, false))
    -                .withPrivacy(Privacy.of("1", "consent$1", Ccpa.of("1YNN"), null)).assemble();
    +                .from(createUsersyncMethod(
    +                        "http://url?redir=%26gdpr%3D{{gdpr}}%26gdpr_consent%3D{{gdpr_consent}}"
    +                                + "%26us_privacy={{us_privacy}}",
    +                        null))
    +                .withPrivacy(Privacy.of("1", "consent$1", Ccpa.of("1YNN"), null))
    +                .assemble();
     
             // then
             assertThat(result.getUrl()).isEqualTo(
    @@ -61,11 +65,12 @@ public void assembleWithPrivacyShouldCreatePrivacyAwareUsersyncInfo() {
         public void assembleWithPrivacyShouldTolerateMissingPrivacyParamsUsersyncInfo() {
             // given and when
             final UsersyncInfo result = UsersyncInfoAssembler
    -                .from(new Usersyncer(null, "http://url?redir=%26gdpr%3D{{gdpr}}"
    -                        + "%26gdpr_consent%3D{{gdpr_consent}}"
    -                        + "%26us_privacy%3D{{us_privacy}}",
    -                        null, null, null, false))
    -                .withPrivacy(Privacy.of(null, null, Ccpa.EMPTY, null)).assemble();
    +                .from(createUsersyncMethod(
    +                        "http://url?redir=%26gdpr%3D{{gdpr}}%26gdpr_consent%3D{{gdpr_consent}}"
    +                                + "%26us_privacy%3D{{us_privacy}}",
    +                        null))
    +                .withPrivacy(Privacy.of(null, null, Ccpa.EMPTY, null))
    +                .assemble();
     
             // then
             assertThat(result.getUrl()).isEqualTo("http://url?redir=%26gdpr%3D%26gdpr_consent%3D%26us_privacy%3D");
    @@ -75,8 +80,9 @@ public void assembleWithPrivacyShouldTolerateMissingPrivacyParamsUsersyncInfo()
         public void assembleWithPrivacyShouldIgnorePrivacyParamsIfTheyAreMissingInUrl() {
             // given and when
             final UsersyncInfo result = UsersyncInfoAssembler
    -                .from(new Usersyncer(null, "http://url?redir=a%3Db", null, null, null, false))
    -                .withPrivacy(Privacy.of("1", "consent", Ccpa.of("YNN"), null)).assemble();
    +                .from(createUsersyncMethod("http://url?redir=a%3Db", null))
    +                .withPrivacy(Privacy.of("1", "consent", Ccpa.of("YNN"), null))
    +                .assemble();
     
             // then
             assertThat(result.getUrl()).isEqualTo("http://url?redir=a%3Db");
    @@ -86,12 +92,12 @@ public void assembleWithPrivacyShouldIgnorePrivacyParamsIfTheyAreMissingInUrl()
         public void assembleWithPrivacyUsersyncInfoShouldPopulateWithPrivacyRedirectAndUsersyncUrl() {
             // given and when
             final UsersyncInfo result = UsersyncInfoAssembler
    -                .from(new Usersyncer(null, "http://url/{{gdpr}}/{{gdpr_consent}}?redir=",
    -                        "/setuid?bidder=adnxs&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}"
    -                                + "&us_privacy={{us_privacy}}"
    -                                + "&uid=$UID",
    -                        "http://localhost:8000", null, false))
    -                .withPrivacy(Privacy.of("1", "consent$1", Ccpa.of("1YNN"), null)).assemble();
    +                .from(createUsersyncMethod(
    +                        "http://url/{{gdpr}}/{{gdpr_consent}}?redir=",
    +                        "http://localhost:8000/setuid?bidder=adnxs&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}"
    +                                + "&us_privacy={{us_privacy}}&uid=$UID"))
    +                .withPrivacy(Privacy.of("1", "consent$1", Ccpa.of("1YNN"), null))
    +                .assemble();
     
             // then
             assertThat(result.getUrl()).isEqualTo(
    @@ -103,10 +109,15 @@ public void assembleWithPrivacyUsersyncInfoShouldPopulateWithPrivacyRedirectAndU
         public void assembleWithUrlUsersyncInfoShouldUpdateUsersyncUrl() {
             // given and when
             final UsersyncInfo result = UsersyncInfoAssembler
    -                .from(new Usersyncer(null, "http://url", null, null, null, false))
    -                .withUrl("http://updated-url").assemble();
    +                .from(createUsersyncMethod("http://url", null))
    +                .withUrl("http://updated-url")
    +                .assemble();
     
             // then
             assertThat(result.getUrl()).isEqualTo("http://updated-url");
         }
    +
    +    private static Usersyncer.UsersyncMethod createUsersyncMethod(String usersyncUrl, String redirectUrl) {
    +        return Usersyncer.UsersyncMethod.of(null, usersyncUrl, redirectUrl, false);
    +    }
     }
    diff --git a/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java b/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java
    new file mode 100644
    index 00000000000..f4dfdf21c65
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java
    @@ -0,0 +1,367 @@
    +package org.prebid.server.bidder;
    +
    +import com.fasterxml.jackson.databind.node.IntNode;
    +import com.fasterxml.jackson.databind.node.TextNode;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.proto.request.CookieSyncRequest;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +
    +public class UsersyncMethodChooserTest extends VertxTest {
    +
    +    private static final String BIDDER = "bidder";
    +
    +    @Test
    +    public void shouldReturnPrimaryMethodWhenFilterIsNull() {
    +        // given
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(null).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(primaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnPrimaryMethodWhenFilterIsEmpty() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(null, null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(primaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnPrimaryMethodWhenMethodFilterTypeIsNull() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(null, null),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(primaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnSecondaryMethodWhenMethodFilterExcludeAndNullBidders() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        null,
    +                        CookieSyncRequest.FilterType.exclude),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer.UsersyncMethod secondaryMethod = createMethod("redirect", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod, secondaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(secondaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnPrimaryMethodWhenNotInMethodFilterExcludeList() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        mapper.createArrayNode().add("anotherbidder"),
    +                        CookieSyncRequest.FilterType.exclude),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(primaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnSecondaryMethodWhenInMethodFilterExcludeList() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        mapper.createArrayNode().add(BIDDER),
    +                        CookieSyncRequest.FilterType.exclude),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer.UsersyncMethod secondaryMethod = createMethod("redirect", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod, secondaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(secondaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnSecondaryMethodWhenMethodFilterExcludesAll() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        new TextNode("*"),
    +                        CookieSyncRequest.FilterType.exclude),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer.UsersyncMethod secondaryMethod = createMethod("redirect", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod, secondaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(secondaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnPrimaryMethodWhenMethodFilterExcludeListIsNotArray() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        new IntNode(1),
    +                        CookieSyncRequest.FilterType.exclude),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(primaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnPrimaryMethodWhenMethodFilterExcludeListIsNotStringArray() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        mapper.createArrayNode().add(1),
    +                        CookieSyncRequest.FilterType.exclude),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(primaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnPrimaryMethodWhenMethodFilterIncludeAndNullBidders() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        null,
    +                        CookieSyncRequest.FilterType.include),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(primaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnSecondaryMethodWhenNotInMethodFilterIncludeList() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        mapper.createArrayNode().add("anotherbidder"),
    +                        CookieSyncRequest.FilterType.include),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer.UsersyncMethod secondaryMethod = createMethod("redirect", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod, secondaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(secondaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnPrimaryMethodWhenInMethodFilterIncludeList() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        mapper.createArrayNode().add(BIDDER),
    +                        CookieSyncRequest.FilterType.include),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(primaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnPrimaryMethodWhenMethodFilterIncludesAll() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        new TextNode("*"),
    +                        CookieSyncRequest.FilterType.include),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(primaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnSecondaryMethodWhenMethodFilterIncludeListIsNotArray() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        new IntNode(1),
    +                        CookieSyncRequest.FilterType.include),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer.UsersyncMethod secondaryMethod = createMethod("redirect", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod, secondaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(secondaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnSecondaryMethodWhenMethodFilterIncludeListIsNotStringArray() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        mapper.createArrayNode().add(1),
    +                        CookieSyncRequest.FilterType.include),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer.UsersyncMethod secondaryMethod = createMethod("redirect", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod, secondaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(secondaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnSecondaryMethodWhenPrimaryIsFilteredOutAndSecondIsNot() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        new TextNode("*"),
    +                        CookieSyncRequest.FilterType.exclude),
    +                CookieSyncRequest.MethodFilter.of(
    +                        new TextNode("*"),
    +                        CookieSyncRequest.FilterType.include));
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer.UsersyncMethod secondaryMethod = createMethod("redirect", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod, secondaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isSameAs(secondaryMethod);
    +    }
    +
    +    @Test
    +    public void shouldReturnNullWhenPrimaryAndSecondaryAreFilteredOut() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        new TextNode("*"),
    +                        CookieSyncRequest.FilterType.exclude),
    +                CookieSyncRequest.MethodFilter.of(
    +                        new TextNode("*"),
    +                        CookieSyncRequest.FilterType.exclude));
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer.UsersyncMethod secondaryMethod = createMethod("redirect", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod, secondaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnNullWhenPrimaryHasNoUrl() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(null, null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", null);
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isNull();
    +    }
    +
    +    @Test
    +    public void shouldReturnNullWhenPrimaryIsFilteredOutAndNoSecondary() {
    +        // given
    +        final CookieSyncRequest.FilterSettings filter = CookieSyncRequest.FilterSettings.of(
    +                CookieSyncRequest.MethodFilter.of(
    +                        new TextNode("*"),
    +                        CookieSyncRequest.FilterType.exclude),
    +                null);
    +        final Usersyncer.UsersyncMethod primaryMethod = createMethod("iframe", "url");
    +        final Usersyncer usersyncer = createUsersyncer(primaryMethod);
    +
    +        // when
    +        final Usersyncer.UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER);
    +
    +        // then
    +        assertThat(chosenMethod).isNull();
    +    }
    +
    +    private Usersyncer createUsersyncer(Usersyncer.UsersyncMethod primaryMethod) {
    +        return createUsersyncer(primaryMethod, null);
    +    }
    +
    +    private Usersyncer createUsersyncer(Usersyncer.UsersyncMethod primaryMethod,
    +                                        Usersyncer.UsersyncMethod secondaryMethod) {
    +
    +        return Usersyncer.of(null, primaryMethod, secondaryMethod);
    +    }
    +
    +    private Usersyncer.UsersyncMethod createMethod(String type, String url) {
    +        return Usersyncer.UsersyncMethod.of(type, url, null, false);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/UsersyncUtilTest.java b/src/test/java/org/prebid/server/bidder/UsersyncUtilTest.java
    new file mode 100644
    index 00000000000..814d7d751ca
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/UsersyncUtilTest.java
    @@ -0,0 +1,72 @@
    +package org.prebid.server.bidder;
    +
    +import org.junit.Test;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +
    +public class UsersyncUtilTest {
    +
    +    @Test
    +    public void enrichUsersyncUrlWithFormatShouldNotChangeUrlIfMissing() {
    +        // given and when
    +        final String url = UsersyncUtil.enrichUsersyncUrlWithFormat(null, "iframe");
    +
    +        // then
    +        assertThat(url).isNull();
    +    }
    +
    +    @Test
    +    public void enrichUsersyncUrlWithFormatShouldNotChangeUrlIfEmpty() {
    +        // given and when
    +        final String url = UsersyncUtil.enrichUsersyncUrlWithFormat("", "iframe");
    +
    +        // then
    +        assertThat(url).isEmpty();
    +    }
    +
    +    @Test
    +    public void enrichUsersyncUrlWithFormatShouldNotChangeUrlIfTypeMissing() {
    +        // given and when
    +        final String url = UsersyncUtil.enrichUsersyncUrlWithFormat("", null);
    +
    +        // then
    +        assertThat(url).isEmpty();
    +    }
    +
    +    @Test
    +    public void enrichUsersyncUrlWithFormatShouldNotChangeUrlIfTypeEmpty() {
    +        // given and when
    +        final String url = UsersyncUtil.enrichUsersyncUrlWithFormat("", "");
    +
    +        // then
    +        assertThat(url).isEmpty();
    +    }
    +
    +    @Test
    +    public void enrichUsersyncUrlWithFormatShouldAddFormat() {
    +        // given and when
    +        final String url = UsersyncUtil.enrichUsersyncUrlWithFormat("//url", "iframe");
    +
    +        // then
    +        assertThat(url).isEqualTo("//url?f=b");
    +    }
    +
    +    @Test
    +    public void enrichUsersyncUrlWithFormatShouldAppendFormat() {
    +        // given and when
    +        final String url = UsersyncUtil.enrichUsersyncUrlWithFormat("http://url?param1=value1", "redirect");
    +
    +        // then
    +        assertThat(url).isEqualTo("http://url?param1=value1&f=i");
    +    }
    +
    +    @Test
    +    public void enrichUsersyncUrlWithFormatShouldInsertFormat() {
    +        // given and when
    +        final String url = UsersyncUtil.enrichUsersyncUrlWithFormat("http://url?param1=value1¶m2=value2",
    +                "redirect");
    +
    +        // then
    +        assertThat(url).isEqualTo("http://url?param1=value1&f=i¶m2=value2");
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/UsersyncerTest.java b/src/test/java/org/prebid/server/bidder/UsersyncerTest.java
    deleted file mode 100644
    index 85d02862836..00000000000
    --- a/src/test/java/org/prebid/server/bidder/UsersyncerTest.java
    +++ /dev/null
    @@ -1,36 +0,0 @@
    -package org.prebid.server.bidder;
    -
    -import org.junit.Test;
    -
    -import java.net.MalformedURLException;
    -
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatThrownBy;
    -
    -public class UsersyncerTest {
    -
    -    @Test
    -    public void newUsersyncerShouldReturnUsersyncerWithConcatenatedExternalAndRedirectUrl() {
    -        // given, when and then
    -        assertThat(
    -                new Usersyncer("rubicon", "//usersync-url", "/redicret-url", "http://localhost:8000", "redirect", true))
    -                .extracting(Usersyncer::getRedirectUrl)
    -                .containsOnly("http://localhost:8000/redicret-url");
    -    }
    -
    -    @Test
    -    public void newUsersyncerShouldReturnUsersyncerWithEmptyRedirectUrlWhenItWasNotDefined() {
    -        // given, when and then
    -        assertThat(new Usersyncer("rubicon", "//usersync-url", null, null, "redirect", true))
    -                .extracting(Usersyncer::getRedirectUrl)
    -                .containsOnly("");
    -    }
    -
    -    @Test
    -    public void newUsersyncerShouldValidateExtenalUrl() {
    -        // given, when and then
    -        assertThatThrownBy(() -> new Usersyncer(null, "//usersync-url", "not-valid-url", null, "redirect", true))
    -                .hasCauseExactlyInstanceOf(MalformedURLException.class)
    -                .hasMessage("URL supplied is not valid: null");
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/acuityads/AcuityadsBidderTest.java b/src/test/java/org/prebid/server/bidder/acuityads/AcuityadsBidderTest.java
    new file mode 100644
    index 00000000000..a88132700ce
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/acuityads/AcuityadsBidderTest.java
    @@ -0,0 +1,319 @@
    +package org.prebid.server.bidder.acuityads;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.acuity.ExtImpAcuityads;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class AcuityadsBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.com/prebid/bid?host={{Host}}&key={{AccountID}}";
    +
    +    private AcuityadsBidder acuityadsBidder;
    +
    +    @Before
    +    public void setUp() {
    +        acuityadsBidder = new AcuityadsBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new AcuityadsBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = acuityadsBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("ext.bidder not provided");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtIsNull() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, null))));
    +        // when
    +        final Result>> result = acuityadsBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("ext.bidder not provided");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtHostParamIsEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(
    +                        ExtPrebid.of(null, ExtImpAcuityads.of("", "someVal")))));
    +        // when
    +        final Result>> result = acuityadsBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Missed host param");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtAccountIdParamIsEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(
    +                        ExtPrebid.of(null, ExtImpAcuityads.of("someVal", "")))));
    +        // when
    +        final Result>> result = acuityadsBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Missed accountId param");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build()));
    +
    +        // when
    +        final Result>> result = acuityadsBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getUri)
    +                .containsOnly("https://test.com/prebid/bid?host=hostVal&key=accountIdVal");
    +    }
    +
    +    @Test
    +    public void shouldRemoveFirsImpExt() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build()));
    +
    +        // when
    +        final Result>> result = acuityadsBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsNull();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = acuityadsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = acuityadsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsOnly(BidderError.badServerResponse("Bad Server Response"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = acuityadsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsOnly(BidderError.badServerResponse("Empty SeatBid array"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfNoBidsFromSeatArePresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder()
    +                        .seatbid(singletonList(SeatBid.builder().build())).build()));
    +
    +        // when
    +        final Result> result = acuityadsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsOnly(BidderError.badServerResponse("Empty bids array"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = acuityadsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyBidderBidsFromSecondSeatBid() throws JsonProcessingException {
    +        // given
    +        final SeatBid firstSeatBId = SeatBid.builder()
    +                .bid(singletonList(Bid.builder()
    +                        .impid("123")
    +                        .build()))
    +                .build();
    +
    +        final SeatBid secondSeatBid = SeatBid.builder()
    +                .bid(singletonList(Bid.builder()
    +                        .impid("456")
    +                        .build()))
    +                .build();
    +
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(BidResponse.builder()
    +                        .seatbid(Arrays.asList(firstSeatBId, secondSeatBid))
    +                        .build()));
    +
    +        // when
    +        final Result> result = acuityadsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = acuityadsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = acuityadsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().id("banner_id").build()).ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpAcuityads.of("hostVal", "accountIdVal")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/adf/AdfBidderTest.java b/src/test/java/org/prebid/server/bidder/adf/AdfBidderTest.java
    new file mode 100644
    index 00000000000..71478685184
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/adf/AdfBidderTest.java
    @@ -0,0 +1,194 @@
    +package org.prebid.server.bidder.adf;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.adf.ExtImpAdf;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +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;
    +
    +public class AdfBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.endpoint.com/";
    +
    +    private AdfBidder adfBidder;
    +
    +    @Before
    +    public void setup() {
    +        adfBidder = new AdfBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnExpectedBidRequest() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = adfBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final BidRequest expectedRequest = bidRequest.toBuilder()
    +                .imp(singletonList(bidRequest.getImp().get(0).toBuilder().tagid("12345").build()))
    +                .build();
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .containsExactly(expectedRequest);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestForAllImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        Imp.builder()
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdf.of("1"))))
    +                                .build())));
    +
    +        // when
    +        final Result>> result = adfBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).hasSize(2)
    +                .extracting(Imp::getTagid)
    +                .containsExactly("12345", "1");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().imp(singletonList(Imp.builder().id("123").banner(Banner.builder()
    +                        .w(1)
    +                        .h(1)
    +                        .build()).build())).build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adfBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().imp(singletonList(Imp.builder().id("123").video(Video.builder()
    +                        .w(1)
    +                        .h(1)
    +                        .build()).build())).build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adfBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().imp(singletonList(Imp.builder().id("123")
    +                        .xNative(new Native())
    +                        .build())).build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adfBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldThrowErrorNoImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().imp(singletonList(Imp.builder().id("123")
    +                        .build())).build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adfBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer) {
    +        return givenBidRequest(impCustomizer, identity());
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .video(Video.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdf.of("12345"))))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +
    +    private static BidResponse givenBidResponse(
    +            Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/adform/AdformAdapterTest.java b/src/test/java/org/prebid/server/bidder/adform/AdformAdapterTest.java
    deleted file mode 100644
    index 58abc2ee353..00000000000
    --- a/src/test/java/org/prebid/server/bidder/adform/AdformAdapterTest.java
    +++ /dev/null
    @@ -1,260 +0,0 @@
    -package org.prebid.server.bidder.adform;
    -
    -import com.iab.openrtb.request.Device;
    -import com.iab.openrtb.request.Regs;
    -import com.iab.openrtb.request.User;
    -import io.netty.handler.codec.http.HttpHeaderValues;
    -import io.vertx.core.http.HttpMethod;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.auction.model.AdapterRequest;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    -import org.prebid.server.bidder.adform.model.AdformBid;
    -import org.prebid.server.bidder.adform.model.AdformParams;
    -import org.prebid.server.bidder.model.AdapterHttpRequest;
    -import org.prebid.server.bidder.model.ExchangeCall;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust;
    -import org.prebid.server.proto.request.PreBidRequest;
    -import org.prebid.server.proto.response.Bid;
    -import org.prebid.server.proto.response.MediaType;
    -import org.prebid.server.util.HttpUtil;
    -
    -import java.util.Base64;
    -import java.math.BigDecimal;
    -import java.util.List;
    -import java.util.Map;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.singletonList;
    -import static java.util.stream.Collectors.toList;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatThrownBy;
    -import static org.assertj.core.api.Assertions.tuple;
    -import static org.mockito.BDDMockito.given;
    -
    -public class AdformAdapterTest extends VertxTest {
    -
    -    private static final String BIDDER = "adform";
    -    private static final String COOKIE_FAMILY = BIDDER;
    -    private static final String ENDPOINT_URL = "http://adform.com/openrtb2d";
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private UidsCookie uidsCookie;
    -
    -    private AdformAdapter adformAdapter;
    -
    -    @Before
    -    public void setUp() {
    -        adformAdapter = new AdformAdapter(COOKIE_FAMILY, ENDPOINT_URL, jacksonMapper);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnAdapterHttpRequestWithCorrectUrlAndHeaders() {
    -        // given
    -        final AdapterRequest adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                AdUnitBid.builder().bidId("bidId").adUnitCode("AdUnitCode")
    -                        .params(mapper.valueToTree(
    -                                AdformParams.of(15L, "gross", "color:red", "red", "300X60", 2.5,
    -                                        "https://adform.com?a=b")))
    -                        .build()));
    -        final PreBidRequestContext preBidRequestContext = PreBidRequestContext.builder().preBidRequest(
    -                PreBidRequest.builder().tid("tid").device(Device.builder().ifa("ifaId").build())
    -                        .regs(Regs.of(null, ExtRegs.of(1, null)))
    -                        .user(User.builder()
    -                                .ext(ExtUser.builder()
    -                                        .consent("consent")
    -                                        .digitrust(ExtUserDigiTrust.of("id", 123, 1))
    -                                        .build())
    -                                .build())
    -                        .build())
    -                .secure(0).ua("userAgent").ip("192.168.0.1").referer("www.example.com").uidsCookie(uidsCookie).build();
    -
    -        given(uidsCookie.uidFrom(BIDDER)).willReturn("buyeruid");
    -
    -        // when
    -        final List> adapterHttpRequests = adformAdapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        final String expectedEncodedPart = Base64.getUrlEncoder().withoutPadding()
    -                .encodeToString("mid=15&rcur=USD&mkv=color:red&mkw=red&cdims=300X60&minp=2.50".getBytes());
    -        assertThat(adapterHttpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getUri)
    -                .containsExactly(
    -                        "http://adform.com/openrtb2d?CC=1&adid=ifaId&fd=1&gdpr=1&gdpr_consent=consent&ip=192.168.0.1"
    -                                + "&pt=gross&rp=4&stid=tid&url=https%3A%2F%2Fadform.com%3Fa%3Db"
    -                                + "&" + expectedEncodedPart);
    -        assertThat(adapterHttpRequests)
    -                .extracting(AdapterHttpRequest::getMethod)
    -                .containsExactly(HttpMethod.GET);
    -
    -        assertThat(adapterHttpRequests)
    -                .extracting(AdapterHttpRequest::getPayload).containsNull();
    -
    -        assertThat(adapterHttpRequests)
    -                .flatExtracting(adapterHttpRequest -> adapterHttpRequest.getHeaders().entries())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    -                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()),
    -                        tuple(HttpUtil.USER_AGENT_HEADER.toString(), "userAgent"),
    -                        tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "192.168.0.1"),
    -                        tuple(HttpUtil.X_REQUEST_AGENT_HEADER.toString(), "PrebidAdapter 0.1.3"),
    -                        tuple(HttpUtil.REFERER_HEADER.toString(), "www.example.com"),
    -                        // Base64 encoded {"id":"id","version":1,"keyv":123,"privacy":{"optout":true}}
    -                        tuple(HttpUtil.COOKIE_HEADER.toString(),
    -                                "uid=buyeruid;DigiTrust.v1.identity=eyJpZCI6ImlkIiwidmVyc2lvbiI6MSwia2V5diI6MTIzLCJw"
    -                                        + "cml2YWN5Ijp7Im9wdG91dCI6dHJ1ZX19"));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldThrowPrebidExceptionIfMasterTagIdIsEqualsToZero() {
    -        // given
    -        final AdapterRequest adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                AdUnitBid.builder()
    -                        .params(mapper.valueToTree(AdformParams.of(0L, null, null, null, null, null, null))).build()));
    -        final PreBidRequestContext preBidRequestContext = PreBidRequestContext.builder().build();
    -
    -        // when and then
    -        assertThatThrownBy(() -> adformAdapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("master tag(placement) id is invalid=0");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldThrowPrebidExceptionIfAdUnitParamsIsNull() {
    -        // given
    -        final AdapterRequest adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                AdUnitBid.builder().build()));
    -        final PreBidRequestContext preBidRequestContext = PreBidRequestContext.builder().build();
    -
    -        // when and then
    -        assertThatThrownBy(() -> adformAdapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Adform params section is missing");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldThrowPrebidExceptionIfAdUnitParamsCannotBeParsed() {
    -        // given
    -        final AdapterRequest adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                AdUnitBid.builder()
    -                        .params(mapper.createObjectNode().put("mid", "string")).build()));
    -        final PreBidRequestContext preBidRequestContext = PreBidRequestContext.builder().build();
    -
    -        // when and then
    -        assertThatThrownBy(() -> adformAdapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Cannot deserialize value of type `java.lang.Long` from String \"string\": not a valid Long"
    -                        + " value\n at [Source: UNKNOWN; line: -1, column: -1] (through reference chain: "
    -                        + "org.prebid.server.bidder.adform.model.AdformParams[\"mid\"])");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestShouldReturnAdapterHttpRequestWithHttpsProtocolIfSecured() {
    -        // given
    -        final AdapterRequest adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                AdUnitBid.builder()
    -                        .params(mapper.valueToTree(AdformParams.of(15L, null, null, null, null, null, null))).build()));
    -        final PreBidRequestContext preBidRequestContext = PreBidRequestContext.builder().secure(1)
    -                .uidsCookie(uidsCookie)
    -                .preBidRequest(PreBidRequest.builder().build()).build();
    -
    -        // when
    -        final List> adapterHttpRequests = adformAdapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -
    -        // bWlkPTE1 is Base64 encoded "mid=15" value
    -        assertThat(adapterHttpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getUri)
    -                .containsExactly("https://adform.com/openrtb2d?CC=1&fd=1&gdpr=&gdpr_consent=&ip=&rp=4&"
    -                        + "stid=&bWlkPTE1JnJjdXI9VVNE");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuilder() {
    -        // given
    -        final AdapterRequest adapterRequest = AdapterRequest.of(BIDDER, singletonList(AdUnitBid.builder()
    -                .bidId("bidId").adUnitCode("adUnitCode").build()));
    -        final ExchangeCall> exchangeCall = ExchangeCall.success(null,
    -                singletonList(AdformBid.builder().winBid(BigDecimal.ONE).banner("banner").response("banner")
    -                        .width(300).height(250).dealId("dealId").winCrid("gross").build()), null);
    -
    -        // when
    -        final List result = adformAdapter.extractBids(adapterRequest, exchangeCall).stream()
    -                .map(Bid.BidBuilder::build).collect(toList());
    -
    -        // then
    -        assertThat(result).hasSize(1).containsExactly(Bid.builder().bidId("bidId").code("adUnitCode").bidder(BIDDER)
    -                .price(BigDecimal.ONE).adm("banner").width(300).height(250).dealId("dealId")
    -                .creativeId("gross").mediaType(MediaType.banner).build());
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnEmptyListIfResponseTypeIsNotBanner() {
    -        // given
    -        final AdapterRequest adapterRequest = AdapterRequest.of(BIDDER, null);
    -        final ExchangeCall> exchangeCall = ExchangeCall.success(null,
    -                singletonList(AdformBid.builder().banner("banner").response("notBanner").build()), null);
    -
    -        // when
    -        final List result = adformAdapter.extractBids(adapterRequest, exchangeCall);
    -
    -        // then
    -        assertThat(result).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnEmptyListIfAdFormBidBannerIsNull() {
    -        // given
    -        final AdapterRequest adapterRequest = AdapterRequest.of(BIDDER, null);
    -        final ExchangeCall> exchangeCall = ExchangeCall.success(null,
    -                singletonList(AdformBid.builder().banner(null).response("banner").build()), null);
    -
    -        // when
    -        final List result = adformAdapter.extractBids(adapterRequest, exchangeCall);
    -
    -        // then
    -        assertThat(result).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnListWithOneBidBuilderWhenTwoBidsInResponseAndOneIsInvalid() {
    -        // given
    -        final AdapterRequest adapterRequest = AdapterRequest.of(BIDDER, singletonList(AdUnitBid.builder()
    -                .bidId("bidId").adUnitCode("adUnitCode").build()));
    -        final ExchangeCall> exchangeCall = ExchangeCall.success(null,
    -                asList(AdformBid.builder().winBid(BigDecimal.ONE).banner("banner").response("banner").build(),
    -                        AdformBid.builder().build()), null);
    -
    -        // when
    -        final List result = adformAdapter.extractBids(adapterRequest, exchangeCall).stream()
    -                .map(Bid.BidBuilder::build).collect(toList());
    -
    -        // then
    -        assertThat(result).hasSize(1);
    -    }
    -
    -    @Test
    -    public void tolerateErrorsShouldReturnFalse() {
    -        // when
    -        final boolean isTolerate = adformAdapter.tolerateErrors();
    -
    -        // then
    -        assertThat(isTolerate).isFalse();
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java b/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java
    index a64941f993d..cf86cb8aa5d 100644
    --- a/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adform/AdformBidderTest.java
    @@ -25,15 +25,14 @@
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
     import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEid;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid;
     import org.prebid.server.proto.openrtb.ext.request.adform.ExtImpAdform;
     import org.prebid.server.proto.openrtb.ext.response.BidType;
     import org.prebid.server.util.HttpUtil;
     
    +import java.util.Arrays;
     import java.util.Base64;
    -import java.math.BigDecimal;
     import java.util.List;
     import java.util.Map;
     
    @@ -69,7 +68,6 @@ public void makeHttpRequestsShouldReturnHttpRequestWithoutErrors() {
                             .buyeruid("buyeruid")
                             .ext(ExtUser.builder()
                                     .consent("consent")
    -                                .digitrust(ExtUserDigiTrust.of("id", 123, 1))
                                     .eids(singletonList(ExtUserEid.of("test.com", "some_user_id",
                                             singletonList(ExtUserEidUid.of("uId", 1, null)), null)))
                                     .build())
    @@ -104,9 +102,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithoutErrors() {
                             tuple(HttpUtil.X_REQUEST_AGENT_HEADER.toString(), "PrebidAdapter 0.1.3"),
                             tuple(HttpUtil.REFERER_HEADER.toString(), "www.example.com"),
                             // Base64 encoded {"id":"id","version":1,"keyv":123,"privacy":{"optout":true}}
    -                        tuple(HttpUtil.COOKIE_HEADER.toString(),
    -                                "uid=buyeruid;DigiTrust.v1.identity=eyJpZCI6ImlkIiwidmVyc2lvbiI6MSwia2V5diI6MTIzLCJwcml"
    -                                        + "2YWN5Ijp7Im9wdG91dCI6dHJ1ZX19"));
    +                        tuple(HttpUtil.COOKIE_HEADER.toString(), "uid=buyeruid"));
         }
     
         @Test
    @@ -185,7 +181,6 @@ public void makeHttpRequestsShouldReturnHttpsUrlIfAtLeastOneImpIsSecured() {
                             .buyeruid("buyeruid")
                             .ext(ExtUser.builder()
                                     .consent("consent")
    -                                .digitrust(ExtUserDigiTrust.of("id", 123, 1))
                                     .eids(singletonList(ExtUserEid.of("test.com", "some_user_id",
                                             singletonList(ExtUserEidUid.of("uId", 1, null)), null)))
                                     .build())
    @@ -248,11 +243,10 @@ public void makeBidsShouldReturnEmptyListIfResponseTypeIsNotBanner() throws Json
         }
     
         @Test
    -    public void makeBidsShouldReturnBidderBid() throws JsonProcessingException {
    +    public void makeBidsShouldReturnBidderBidIfResponseIsBannerAndBannerIsPresent() throws JsonProcessingException {
             // given
             final String adformResponse = mapper.writeValueAsString(AdformBid.builder().banner("admBanner")
    -                .response("banner").winCur("currency").dealId("dealId").height(300).width(400).winCrid("gross")
    -                .winBid(BigDecimal.ONE).build());
    +                .response("banner").winCur("currency").build());
     
             final HttpCall httpCall = givenHttpCall(adformResponse);
             final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("id").build())).build();
    @@ -263,8 +257,70 @@ public void makeBidsShouldReturnBidderBid() throws JsonProcessingException {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1).containsOnly(BidderBid.of(
    -                Bid.builder().id("id").impid("id").price(BigDecimal.ONE).adm("admBanner").w(400).h(300).dealid("dealId")
    -                        .crid("gross").build(), BidType.banner, "currency"));
    +                Bid.builder().id("id").impid("id").adm("admBanner").build(), BidType.banner, "currency"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBidderBidsWithCurrencyFromEveryAdformResponse() throws JsonProcessingException {
    +        // given
    +        final AdformBid firstAdformResponse = AdformBid.builder().banner("admBanner")
    +                .response("banner").winCur("EUR").build();
    +        final AdformBid secondAdformResponse = AdformBid.builder().banner("admBanner")
    +                .response("banner").winCur("USD").build();
    +        final String adformBidResponse =
    +                mapper.writeValueAsString(Arrays.asList(firstAdformResponse, secondAdformResponse));
    +
    +        final HttpCall httpCall = givenHttpCall(adformBidResponse);
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(Imp.builder().id("firstId").build(), Imp.builder().id("secondId").build()))
    +                .build();
    +
    +        // when
    +        final Result> result = adformBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(2).containsExactly(
    +                BidderBid.of(Bid.builder().id("firstId").impid("firstId").adm("admBanner").build(),
    +                        BidType.banner,
    +                        "EUR"),
    +                BidderBid.of(Bid.builder().id("secondId").impid("secondId").adm("admBanner").build(),
    +                        BidType.banner,
    +                        "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfResponseEqualsVastContent() throws JsonProcessingException {
    +        // given
    +        final String adformResponse = mapper.writeValueAsString(
    +                AdformBid.builder().winCur("currency").vastContent("admVastContent").response("vast_content").build());
    +
    +        final HttpCall httpCall = givenHttpCall(adformResponse);
    +        final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("id").build())).build();
    +
    +        // when
    +        final Result> result = adformBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).containsOnly(BidderBid.of(Bid.builder().id("id").impid("id")
    +                .adm("admVastContent").build(), BidType.video, "currency"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyResultIfVastContentOrBannerIsNotPresent() throws JsonProcessingException {
    +        // given
    +        final String adformResponse = mapper.writeValueAsString(AdformBid.builder().build());
    +
    +        final HttpCall httpCall = givenHttpCall(adformResponse);
    +        final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("id").build())).build();
    +
    +        // when
    +        final Result> result = adformBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    @@ -302,9 +358,8 @@ public void makeHttpRequestsShouldPassMultipleUserIds() {
                             .buyeruid("buyeruid")
                             .ext(ExtUser.builder()
                                     .consent("consent")
    -                                .digitrust(ExtUserDigiTrust.of("id", 123, 1))
                                     .eids(asList(ExtUserEid.of("test.com", "some_user_id",
    -                                                singletonList(ExtUserEidUid.of("uId", 1, null)), null),
    +                                        singletonList(ExtUserEidUid.of("uId", 1, null)), null),
                                             ExtUserEid.of("test.com", "some_user_id",
                                                     singletonList(ExtUserEidUid.of("uId", 2, null)), null),
                                             ExtUserEid.of("test.net", "some_user_id",
    @@ -319,8 +374,9 @@ public void makeHttpRequestsShouldPassMultipleUserIds() {
             // then
             assertThat(result.getValue()).hasSize(1)
                     .extracting(HttpRequest::getUri)
    -                .containsExactly("https://adform.com/openrtb2d?CC=1&eids=eyJ0ZXN0LmNvbSI6eyJ1SWQiOlsxLDJdfSwidGVzdC5uZXQiOnsiaWRfc29tZV91c2VyIjpbM119fQ&fd=1&gdpr="
    -                        + "&gdpr_consent=consent&ip=&rp=4&stid=tid&bWlkPTE1JnJjdXI9VVNE");
    +                .containsExactly(
    +                        "https://adform.com/openrtb2d?CC=1&eids=eyJ0ZXN0LmNvbSI6eyJ1SWQiOlsxLDJdfSwidGVzdC5uZXQiOnsiaWRfc29tZV91c2VyIjpbM119fQ&fd=1&gdpr="
    +                                + "&gdpr_consent=consent&ip=&rp=4&stid=tid&bWlkPTE1JnJjdXI9VVNE");
         }
     
         private static HttpCall givenHttpCall(String body) {
    diff --git a/src/test/java/org/prebid/server/bidder/adform/AdformHttpUtilTest.java b/src/test/java/org/prebid/server/bidder/adform/AdformHttpUtilTest.java
    index bda3754eec2..3b5c320b764 100644
    --- a/src/test/java/org/prebid/server/bidder/adform/AdformHttpUtilTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adform/AdformHttpUtilTest.java
    @@ -5,8 +5,6 @@
     import org.junit.Before;
     import org.junit.Test;
     import org.prebid.server.VertxTest;
    -import org.prebid.server.bidder.adform.model.AdformDigitrust;
    -import org.prebid.server.bidder.adform.model.AdformDigitrustPrivacy;
     import org.prebid.server.bidder.adform.model.UrlParameters;
     import org.prebid.server.util.HttpUtil;
     
    @@ -27,7 +25,7 @@ public class AdformHttpUtilTest extends VertxTest {
     
         @Before
         public void setUp() {
    -        httpUtil = new AdformHttpUtil(jacksonMapper);
    +        httpUtil = new AdformHttpUtil();
         }
     
         @Test
    @@ -38,8 +36,7 @@ public void buildAdformHeadersShouldReturnAllHeaders() {
                     "userAgent",
                     "ip",
                     "www.example.com",
    -                "buyeruid",
    -                AdformDigitrust.of("id", 1, 123, AdformDigitrustPrivacy.of(true)));
    +                "buyeruid");
     
             // then
             assertThat(headers).hasSize(7)
    @@ -50,10 +47,7 @@ public void buildAdformHeadersShouldReturnAllHeaders() {
                             tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "ip"),
                             tuple(HttpUtil.X_REQUEST_AGENT_HEADER.toString(), "PrebidAdapter 0.1.0"),
                             tuple(HttpUtil.REFERER_HEADER.toString(), "www.example.com"),
    -                        tuple(HttpUtil.COOKIE_HEADER.toString(),
    -                                // Base64 encoded {"id":"id","version":1,"keyv":123,"privacy":{"optout":true}}
    -                                "uid=buyeruid;DigiTrust.v1.identity=eyJpZCI6ImlkIiwidmVyc2lvbiI6MSwia2V5diI6MTIzLC"
    -                                        + "Jwcml2YWN5Ijp7Im9wdG91dCI6dHJ1ZX19"));
    +                        tuple(HttpUtil.COOKIE_HEADER.toString(), "uid=buyeruid"));
         }
     
         @Test
    @@ -64,63 +58,41 @@ public void buildAdformHeadersShouldNotContainRefererHeaderIfRefererIsEmpty() {
                     "userAgent",
                     "ip",
                     "",
    -                "buyeruid",
    -                AdformDigitrust.of("id", 1, 123, AdformDigitrustPrivacy.of(true)));
    +                "buyeruid");
     
             // then
             assertThat(headers).extracting(Map.Entry::getKey).doesNotContain(HttpUtil.REFERER_HEADER.toString());
         }
     
         @Test
    -    public void buildAdformHeadersShouldNotContainCookieHeaderIfUserIdAndDigiTrustAreEmpty() {
    +    public void buildAdformHeadersShouldNotContainCookieHeaderIfUserIdIsEmpty() {
             // when
             final MultiMap headers = httpUtil.buildAdformHeaders(
                     "0.1.0",
                     "userAgent",
                     "ip",
                     "referer",
    -                "",
    -                null);
    +                "");
     
             // then
             assertThat(headers).extracting(Map.Entry::getKey).doesNotContain(HttpUtil.COOKIE_HEADER.toString());
         }
     
         @Test
    -    public void buildAdformHeaderShouldContainCookieHeaderOnlyWithUserIdIfUserIdPresentAndDigitrustAbsent() {
    +    public void buildAdformHeaderShouldContainCookieHeaderOnlyWithUserIdIfUserIdPresent() {
             // when
             final MultiMap headers = httpUtil.buildAdformHeaders(
                     "0.1.0",
                     "userAgent",
                     "ip",
                     "referer",
    -                "buyeruid",
    -                null);
    +                "buyeruid");
     
             // then
             assertThat(headers).extracting(Map.Entry::getKey, Map.Entry::getValue)
                     .contains(tuple(HttpUtil.COOKIE_HEADER.toString(), "uid=buyeruid"));
         }
     
    -    @Test
    -    public void buildAdformHeaderShouldContainCookieHeaderOnlyWithDigitrustIfUserIsAbsentAndDigitrustPresent() {
    -        // when
    -        final MultiMap headers = httpUtil.buildAdformHeaders(
    -                "0.1.0",
    -                "userAgent",
    -                "ip",
    -                "referer",
    -                "",
    -                AdformDigitrust.of("id", 1, 123, AdformDigitrustPrivacy.of(true)));
    -
    -        // then
    -        assertThat(headers).extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .contains(tuple(HttpUtil.COOKIE_HEADER.toString(),
    -                        // Base64 encoded {"id":"id","version":1,"keyv":123,"privacy":{"optout":true}}
    -                        "DigiTrust.v1.identity=eyJpZCI6ImlkIiwidmVyc2lvbiI6MSwia2V5diI6MTIzLCJwcml2YWN5Ijp7Im9wdG91dC"
    -                                + "I6dHJ1ZX19"));
    -    }
    -
         @Test
         public void buildAdformUrlShouldReturnCorrectUrl() {
             // when
    diff --git a/src/test/java/org/prebid/server/bidder/adform/AdformRequestUtilTest.java b/src/test/java/org/prebid/server/bidder/adform/AdformRequestUtilTest.java
    index 4cf50a36201..46afb44585d 100644
    --- a/src/test/java/org/prebid/server/bidder/adform/AdformRequestUtilTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adform/AdformRequestUtilTest.java
    @@ -4,11 +4,8 @@
     import org.junit.Before;
     import org.junit.Test;
     import org.prebid.server.VertxTest;
    -import org.prebid.server.bidder.adform.model.AdformDigitrust;
    -import org.prebid.server.bidder.adform.model.AdformDigitrustPrivacy;
     import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
     import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust;
     
     import static org.assertj.core.api.Assertions.assertThat;
     
    @@ -92,44 +89,4 @@ public void getConsentShouldReturnConsent() {
             // then
             assertThat(consent).isEqualTo("consent");
         }
    -
    -    @Test
    -    public void getAdformDigiTrustShouldReturnNullIfUserExtIsNull() {
    -        // given and when
    -        final AdformDigitrust adformDigitrust = requestUtil.getAdformDigitrust(null);
    -
    -        // then
    -        assertThat(adformDigitrust).isNull();
    -    }
    -
    -    @Test
    -    public void getAdformDigiTrustShouldReturnNullIfUserExtDigitrustIsNull() {
    -        // given and when
    -        final AdformDigitrust adformDigitrust = requestUtil.getAdformDigitrust(ExtUser.builder().build());
    -
    -        // then
    -        assertThat(adformDigitrust).isNull();
    -    }
    -
    -    @Test
    -    public void getAdformDigiTrustShouldReturnAdformDigitrustWithOptOutFalseIfPrefIsZero() {
    -        // given and when
    -        final AdformDigitrust adformDigitrust = requestUtil.getAdformDigitrust(ExtUser.builder()
    -                .digitrust(ExtUserDigiTrust.of("id", 123, 0))
    -                .build());
    -
    -        // then
    -        assertThat(adformDigitrust).isEqualTo(AdformDigitrust.of("id", 1, 123, AdformDigitrustPrivacy.of(false)));
    -    }
    -
    -    @Test
    -    public void getAdformDigiTrustShouldReturnAdformDigitrustWithOptOutTrueIfPrefIsNotZero() {
    -        // given and when
    -        final AdformDigitrust adformDigitrust = requestUtil.getAdformDigitrust(ExtUser.builder()
    -                .digitrust(ExtUserDigiTrust.of("id", 123, 1))
    -                .build());
    -
    -        // then
    -        assertThat(adformDigitrust).isEqualTo(AdformDigitrust.of("id", 1, 123, AdformDigitrustPrivacy.of(true)));
    -    }
     }
    diff --git a/src/test/java/org/prebid/server/bidder/adgeneration/AdgenerationBidderTest.java b/src/test/java/org/prebid/server/bidder/adgeneration/AdgenerationBidderTest.java
    index 2585a55d6dd..f32f3e63156 100644
    --- a/src/test/java/org/prebid/server/bidder/adgeneration/AdgenerationBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adgeneration/AdgenerationBidderTest.java
    @@ -27,8 +27,6 @@
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -52,21 +50,6 @@ public void creationShouldFailOnInvalidEndpointUrl() {
                     .withMessage("URL supplied is not valid: invalid_url");
         }
     
    -    @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpressionListSizeIsZero() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(emptyList())
    -                .build();
    -
    -        // when
    -        final Result>> result = adgenerationBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("No impression in the bid request"));
    -    }
    -
         @Test
         public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             // given
    @@ -187,20 +170,6 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             assertThat(result.getValue()).isEmpty();
         }
     
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = adgenerationBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
         @Test
         public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingException {
             // given
    @@ -328,11 +297,6 @@ public void makeBidsShouldReturnIfImpExtCouldNotBeParsed() throws JsonProcessing
             assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(adgenerationBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
    diff --git a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java
    index 7971e268918..7cb67c1739b 100644
    --- a/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adhese/AdheseBidderTest.java
    @@ -1,11 +1,14 @@
     package org.prebid.server.bidder.adhese;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.node.ArrayNode;
     import com.github.fge.jsonpatch.JsonPatchException;
     import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
     import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
     import com.iab.openrtb.request.Imp;
    -import com.fasterxml.jackson.databind.JsonNode;
    +import com.iab.openrtb.request.Site;
     import com.iab.openrtb.request.User;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
    @@ -14,6 +17,7 @@
     import org.prebid.server.VertxTest;
     import org.prebid.server.bidder.adhese.model.AdheseBid;
     import org.prebid.server.bidder.adhese.model.AdheseOriginData;
    +import org.prebid.server.bidder.adhese.model.AdheseRequestBody;
     import org.prebid.server.bidder.adhese.model.AdheseResponseExt;
     import org.prebid.server.bidder.adhese.model.Cpm;
     import org.prebid.server.bidder.adhese.model.CpmValues;
    @@ -30,9 +34,12 @@
     import org.prebid.server.proto.openrtb.ext.response.BidType;
     
     import java.math.BigDecimal;
    +import java.util.Arrays;
    +import java.util.Collections;
     import java.util.HashMap;
     import java.util.List;
     import java.util.Map;
    +import java.util.TreeMap;
     
     import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
    @@ -91,7 +98,8 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequestUri() {
    +    public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequestUri()
    +            throws JsonProcessingException {
             // given
             Map> targets = new HashMap<>();
             targets.put("ci", asList("gent", "brussels"));
    @@ -102,32 +110,119 @@ public void makeHttpRequestsShouldModifyIncomingRequestAndSetExpectedHttpRequest
                             .ext(ExtUser.builder().consent("dummy").build())
                             .build())
                     .imp(singletonList(Imp.builder()
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdhese.of("demo",
    -                                "_adhese_prebid_demo_", "leaderboard", mapper.convertValue(targets, JsonNode.class)))))
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdhese.of(
    +                                "demo",
    +                                "_adhese_prebid_demo_",
    +                                "leaderboard",
    +                                mapper.convertValue(targets, JsonNode.class)))))
                             .build()))
    +                .device(Device.builder().ifa("dum-my").build())
                     .build();
     
             // when
             final Result>> result = adheseBidder.makeHttpRequests(bidRequest);
     
             // then
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsOnly("https://ads-demo.adhese.com/json");
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getBody)
    +                .containsOnly(jacksonMapper.mapper().writeValueAsString(
    +                        AdheseRequestBody
    +                        .builder()
    +                        .slots(Collections.singletonList(
    +                                AdheseRequestBody.Slot.builder()
    +                                        .slotname("_adhese_prebid_demo_-leaderboard")
    +                                        .build()))
    +                        .parameters(new TreeMap>() {{
    +                                put("ag", Arrays.asList("55"));
    +                                put("ci", Arrays.asList("gent", "brussels"));
    +                                put("tl", Arrays.asList("all"));
    +                                put("xt", Arrays.asList("dummy"));
    +                                put("xz", Arrays.asList("dum-my")); }})
    +                        .build()));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldModifyIncomingRequestWithIfaParameter() throws JsonProcessingException {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ifa("ifaValue").build())
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpAdhese.of("demo", "_adhese_prebid_demo_", "leaderboard",
    +                                        mapper.convertValue(emptyMap(), JsonNode.class)))))
    +                        .build()))
    +                .build();
     
    +        // when
    +        final Result>> result = adheseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
             assertThat(result.getValue())
                     .extracting(HttpRequest::getUri)
    -                .containsOnly("https://ads-demo.adhese.com/json/sl_adhese_prebid_demo_-leaderboard/ag55/cigent;brussels"
    -                        + "/tlall/xtdummy");
    +                .containsOnly("https://ads-demo.adhese.com/json");
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getBody)
    +                .containsOnly(jacksonMapper.mapper().writeValueAsString(
    +                        AdheseRequestBody
    +                                .builder()
    +                                .slots(Collections.singletonList(
    +                                        AdheseRequestBody.Slot.builder()
    +                                                .slotname("_adhese_prebid_demo_-leaderboard")
    +                                                .build()))
    +                                .parameters(new TreeMap<>(Collections.singletonMap(
    +                                        "xz", Arrays.asList("ifaValue"))))
    +                                .build()));
         }
     
         @Test
    -    public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent() {
    +    public void makeHttpRequestsShouldModifyIncomingRequestWithRefererParameter() throws JsonProcessingException {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .site(Site.builder().page("pageValue").build())
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpAdhese.of("demo", "_adhese_prebid_demo_", "leaderboard",
    +                                        mapper.convertValue(emptyMap(), JsonNode.class)))))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = adheseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsOnly("https://ads-demo.adhese.com/json");
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getBody)
    +                .containsOnly(jacksonMapper.mapper().writeValueAsString(
    +                        AdheseRequestBody
    +                                .builder()
    +                                .slots(Collections.singletonList(
    +                                        AdheseRequestBody.Slot.builder()
    +                                                .slotname("_adhese_prebid_demo_-leaderboard")
    +                                                .build()))
    +                                .parameters(new TreeMap<>(Collections.singletonMap(
    +                                        "xf", Arrays.asList("pageValue"))))
    +                                .build()));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent() throws JsonProcessingException {
             // given
             final BidRequest bidRequest = BidRequest.builder()
                     .user(User.builder()
                             .ext(ExtUser.builder().consent("dummy").build())
                             .build())
                     .imp(singletonList(Imp.builder()
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdhese.of("demo",
    -                                "_adhese_prebid_demo_", "leaderboard", mapper.convertValue(null, JsonNode.class)))))
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdhese.of(
    +                                "demo",
    +                                "_adhese_prebid_demo_",
    +                                "leaderboard",
    +                                mapper.convertValue(null, JsonNode.class)))))
                             .build()))
                     .build();
     
    @@ -137,29 +232,55 @@ public void makeHttpRequestsShouldNotModifyIncomingRequestIfTargetsNotPresent()
             // then
             assertThat(result.getValue())
                     .extracting(HttpRequest::getUri)
    -                .containsOnly("https://ads-demo.adhese.com/json/sl_adhese_prebid_demo_-leaderboard/xtdummy");
    +                .containsOnly("https://ads-demo.adhese.com/json");
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getBody)
    +                .containsOnly(jacksonMapper.mapper().writeValueAsString(
    +                        AdheseRequestBody
    +                                .builder()
    +                                .slots(Collections.singletonList(
    +                                        AdheseRequestBody.Slot.builder()
    +                                                .slotname("_adhese_prebid_demo_-leaderboard")
    +                                                .build()))
    +                                .parameters(new TreeMap<>(Collections.singletonMap(
    +                                        "xt", Arrays.asList("dummy"))))
    +                                .build()));
         }
     
         @Test
         public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             // given
    -        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +        final HttpCall httpCall = givenHttpCall("invalid");
     
             // when
             final Result> result = adheseBidder.makeBids(httpCall, null);
     
             // then
             assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Unrecognized token 'invalid'");
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
             assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
             assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    +    public void makeBidsShouldReturnErrorIfResponseBodyIsUnexpected() {
             // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    +        final HttpCall httpCall = givenHttpCall("{}");
    +
    +        // when
    +        final Result> result = adheseBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Unexpected response body");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyResultWhenResponseIsEmptyArray() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall("[]");
     
             // when
             final Result> result = adheseBidder.makeBids(httpCall, null);
    @@ -169,6 +290,21 @@ public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
             assertThat(result.getValue()).isEmpty();
         }
     
    +    @Test
    +    public void makeBidsShouldReturnEmptyResultWhenResponseIsArrayWithEmptyObject() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall("[{}]");
    +
    +        // when
    +        final Result> result = adheseBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Response resulted in an empty seatBid array");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
         @Test
         public void makeBidsShouldReturnErrorIfSeatbidIsEmpty() throws JsonProcessingException, JsonPatchException {
             // given
    @@ -193,14 +329,18 @@ public void makeBidsShouldReturnErrorIfSeatbidIsEmpty() throws JsonProcessingExc
     
             final JsonNode mergedResponseBid = JsonMergePatch.fromJson(adheseBidNode).apply(adheseResponseExtNode);
             final JsonNode mergedResponse = JsonMergePatch.fromJson(adheseOriginDataNode).apply(mergedResponseBid);
    -        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(mergedResponse));
    +
    +        final ArrayNode body = mapper.createArrayNode().add(mergedResponse);
    +        final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(body));
     
             // when
             final Result> result = adheseBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Response resulted in an empty seatBid array.");
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Response resulted in an empty seatBid array");
             assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    @@ -234,7 +374,8 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio
             final JsonNode mergedResponseBid = JsonMergePatch.fromJson(adheseBidNode).apply(adheseResponseExtNode);
             final JsonNode mergedResponse = JsonMergePatch.fromJson(adheseOriginDataNode).apply(mergedResponseBid);
     
    -        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(mergedResponse));
    +        final ArrayNode body = mapper.createArrayNode().add(mergedResponse);
    +        final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(body));
     
             // when
             final Result> result = adheseBidder.makeBids(httpCall, bidRequest);
    @@ -255,9 +396,9 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio
                                     "adspaceId", "libId", "slotID", "viewableImpressionCounter")))
                             .build(),
                     BidType.banner, "USD");
    -        assertThat(result.getValue().get(0).getBid().getAdm()).isEqualTo(adm);
    +
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).doesNotContainNull().hasSize(1).element(0).isEqualTo(expected);
    +        assertThat(result.getValue()).doesNotContainNull().hasSize(1).first().isEqualTo(expected);
         }
     
         @Test
    @@ -280,8 +421,8 @@ public void makeBidsShouldReturnCorrectBidIfAdheseBidContainsVastTag() throws Js
                     .build();
     
             final AdheseResponseExt adheseResponseExt = AdheseResponseExt.of("60613369", "888", "https://hosts-demo."
    -                + "adhese.com/rtb_gateway/handlers/client/track/?id=a2f39296-6dd0-4b3c-be85-7baa22e7ff4a", "tag",
    -                "js");
    +                        + "adhese.com/rtb_gateway/handlers/client/track/?id=a2f39296-6dd0-4b3c-be85-7baa22e7ff4a",
    +                "tag", "js");
             final AdheseOriginData adheseOriginData = AdheseOriginData.of("priority", "orderProperty", "adFormat",
                     "adType", "adspaceId", "libId", "slotID", "viewableImpressionCounter");
     
    @@ -292,7 +433,8 @@ public void makeBidsShouldReturnCorrectBidIfAdheseBidContainsVastTag() throws Js
             final JsonNode mergedResponseBid = JsonMergePatch.fromJson(adheseBidNode).apply(adheseResponseExtNode);
             final JsonNode mergedResponse = JsonMergePatch.fromJson(adheseOriginDataNode).apply(mergedResponseBid);
     
    -        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(mergedResponse));
    +        final ArrayNode body = mapper.createArrayNode().add(mergedResponse);
    +        final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(body));
     
             // when
             final Result> result = adheseBidder.makeBids(httpCall, bidRequest);
    @@ -313,9 +455,9 @@ public void makeBidsShouldReturnCorrectBidIfAdheseBidContainsVastTag() throws Js
                                     "adspaceId", "libId", "slotID", "viewableImpressionCounter")))
                             .build(),
                     BidType.video, "USD");
    -        assertThat(result.getValue().get(0).getBid().getAdm()).isEqualTo(adm);
    +
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).doesNotContainNull().hasSize(1).element(0).isEqualTo(expected);
    +        assertThat(result.getValue()).doesNotContainNull().hasSize(1).first().isEqualTo(expected);
         }
     
         @Test
    @@ -349,7 +491,8 @@ public void makeBidsShouldReturnCorrectBidIfAdheseResponseExtIsNotEqualJs() thro
             final JsonNode mergedResponseBid = JsonMergePatch.fromJson(adheseBidNode).apply(adheseResponseExtNode);
             final JsonNode mergedResponse = JsonMergePatch.fromJson(adheseOriginDataNode).apply(mergedResponseBid);
     
    -        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(mergedResponse));
    +        final ArrayNode body = mapper.createArrayNode().add(mergedResponse);
    +        final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(body));
     
             // when
             final Result> result = adheseBidder.makeBids(httpCall, bidRequest);
    @@ -370,19 +513,14 @@ public void makeBidsShouldReturnCorrectBidIfAdheseResponseExtIsNotEqualJs() thro
                                     "adspaceId", "libId", "slotID", "viewableImpressionCounter")))
                             .build(),
                     BidType.banner, "USD");
    -        assertThat(result.getValue().get(0).getBid().getAdm()).isEqualTo(adm);
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).doesNotContainNull().hasSize(1).element(0).isEqualTo(expected);
    -    }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(adheseBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).doesNotContainNull().hasSize(1).first().isEqualTo(expected);
         }
     
    -    private static HttpCall givenHttpCall(String requestBody, String responseBody) {
    +    private static HttpCall givenHttpCall(String responseBody) {
             return HttpCall.success(
    -                HttpRequest.builder().body(requestBody).build(),
    +                HttpRequest.builder().build(),
                     HttpResponse.of(200, null, responseBody), null);
         }
     }
    diff --git a/src/test/java/org/prebid/server/bidder/adkernel/AdkernelBidderTest.java b/src/test/java/org/prebid/server/bidder/adkernel/AdkernelBidderTest.java
    index 282cf261205..2f4ea13a6bc 100644
    --- a/src/test/java/org/prebid/server/bidder/adkernel/AdkernelBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adkernel/AdkernelBidderTest.java
    @@ -32,7 +32,6 @@
     import java.util.function.Function;
     
     import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -69,9 +68,9 @@ public void makeHttpRequestsShouldReturnErrorWhenImpHasNoBannerOrVideo() {
             final Result>> result = adkernelBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getValue()).isNull();
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Invalid imp id=123. Expected imp.banner or imp.video"));
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Invalid imp id=123. Expected imp.banner or imp.video"));
         }
     
         @Test
    @@ -90,12 +89,13 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
     
             // then
             assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    -        assertThat(result.getValue()).isNull();
    +        assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_input
    +                && error.getMessage().startsWith("Cannot deserialize instance"));
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfExtZoneIdIsNull() {
    +    public void makeHttpRequestsShouldReturnErrorIfExtZoneIdisEmpty() {
             // given
             final BidRequest bidRequest = givenBidRequest(identity(), extBuilder -> extBuilder.zoneId(null));
     
    @@ -103,9 +103,9 @@ public void makeHttpRequestsShouldReturnErrorIfExtZoneIdIsNull() {
             final Result>> result = adkernelBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Invalid zoneId value: null. Ignoring imp id=123"));
    -        assertThat(result.getValue()).isNull();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Invalid zoneId value: null. Ignoring imp id=123"));
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    @@ -117,13 +117,13 @@ public void makeHttpRequestsShouldReturnErrorIfExtZoneIdIsInvalid() {
             final Result>> result = adkernelBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Invalid zoneId value: 0. Ignoring imp id=123"));
    -        assertThat(result.getValue()).isNull();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Invalid zoneId value: 0. Ignoring imp id=123"));
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfExtHostIsNull() {
    +    public void makeHttpRequestsShouldReturnErrorIfExtHostisEmpty() {
             // given
             final BidRequest bidRequest = givenBidRequest(identity(), extBuilder -> extBuilder.host(null));
     
    @@ -131,9 +131,9 @@ public void makeHttpRequestsShouldReturnErrorIfExtHostIsNull() {
             final Result>> result = adkernelBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Host is empty. Ignoring imp id=123"));
    -        assertThat(result.getValue()).isNull();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Host is empty. Ignoring imp id=123"));
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    @@ -145,9 +145,9 @@ public void makeHttpRequestsShouldReturnErrorIfExtHostIsInvalid() {
             final Result>> result = adkernelBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Host is empty. Ignoring imp id=123"));
    -        assertThat(result.getValue()).isNull();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Host is empty. Ignoring imp id=123"));
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    @@ -165,10 +165,10 @@ public void makeHttpRequestsShouldSetExpectedMethodUrlAndHeaders() {
                     .returns("http://test_host/hb?zone=3426", HttpRequest::getUri);
             assertThat(result.getValue().get(0).getHeaders()).isNotNull()
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(
    +                .containsExactly(
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), "application/json;charset=utf-8"),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), "application/json"),
    -                        tuple("x-openrtb-version", "2.5"));
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
         }
     
         @Test
    @@ -208,7 +208,7 @@ public void makeHttpRequestShouldSetAudioVideoAndNativeNullAndKeepBannerWhenBann
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getBanner, Imp::getVideo, Imp::getAudio, Imp::getXNative)
    -                .containsOnly(tuple(Banner.builder().build(), null, null, null));
    +                .containsExactly(tuple(Banner.builder().build(), null, null, null));
         }
     
         @Test
    @@ -228,7 +228,7 @@ public void makeHttpRequestShouldSetAudioAndNativeNullAndKeepBannerNullWhenVideo
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getBanner, Imp::getVideo, Imp::getAudio, Imp::getXNative)
    -                .containsOnly(tuple(null, Video.builder().build(), null, null));
    +                .containsExactly(tuple(null, Video.builder().build(), null, null));
         }
     
         @Test
    @@ -247,7 +247,7 @@ public void makeHttpRequestShouldModifySite() {
             assertThat(result.getValue()).hasSize(1)
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .extracting(BidRequest::getSite)
    -                .containsOnly(Site.builder().publisher(null).build());
    +                .containsExactly(Site.builder().publisher(null).build());
         }
     
         @Test
    @@ -267,7 +267,7 @@ public void makeHttpRequestShouldModifyApp() {
             assertThat(result.getValue()).hasSize(1)
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .extracting(BidRequest::getApp)
    -                .containsOnly(App.builder().publisher(null).build());
    +                .containsExactly(App.builder().publisher(null).build());
         }
     
         @Test
    @@ -280,13 +280,14 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
     
             // then
             assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_server_response
    +                && error.getMessage().startsWith("Failed to decode: Unrecognized token"));
             assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
             assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    -    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +    public void makeBidsShouldReturnEmptyListIfBidResponseisEmpty() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(null,
                     mapper.writeValueAsString(null));
    @@ -300,7 +301,7 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces
         }
     
         @Test
    -    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidisEmpty() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(null,
                     mapper.writeValueAsString(BidResponse.builder().build()));
    @@ -323,8 +324,8 @@ public void makeBidsShouldReturnErrorWhenSeatBidsCountIsNotOne() throws JsonProc
             final Result> result = adkernelBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badServerResponse("Invalid SeatBids count: 0"));
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badServerResponse("Invalid SeatBids count: 0"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -343,7 +344,7 @@ public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
         @Test
    @@ -362,12 +363,7 @@ public void makeBidsShouldReturnBannerBidIfRequestImpHasBanner() throws JsonProc
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(adkernelBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         private static BidRequest givenBidRequest(
    @@ -404,6 +400,7 @@ private static Imp givenImp(
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidderTest.java b/src/test/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidderTest.java
    index a71ae2901d0..c3591e0a899 100644
    --- a/src/test/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adkerneladn/AdkernelAdnBidderTest.java
    @@ -34,7 +34,6 @@
     
     import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -45,7 +44,7 @@
     
     public class AdkernelAdnBidderTest extends VertxTest {
     
    -    private static final String ENDPOINT_URL = "http://test.domain.com/rtbpub?account=";
    +    private static final String ENDPOINT_URL = "http://{{Host}}/test?account={{PublisherID}}";
     
         private AdkernelAdnBidder adkernelAdnBidder;
     
    @@ -150,13 +149,13 @@ public void makeHttpRequestsShouldFillMethodAndUrlAndExpectedHeaders() {
             // then
             assertThat(result.getValue()).hasSize(1).element(0).isNotNull()
                     .returns(HttpMethod.POST, HttpRequest::getMethod)
    -                .returns("http://test.domain.com/rtbpub?account=50357", HttpRequest::getUri);
    +                .returns("http://tag.adkernel.com/test?account=50357", HttpRequest::getUri);
             assertThat(result.getValue().get(0).getHeaders()).isNotNull()
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
                     .containsOnly(
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), "application/json;charset=utf-8"),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), "application/json"),
    -                        tuple("x-openrtb-version", "2.5"));
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
         }
     
         @Test
    @@ -173,27 +172,7 @@ public void makeHttpRequestShouldChangeDomainIfHostIsSpecified() {
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
                     .extracting(HttpRequest::getUri)
    -                .containsOnly("http://different.domanin.com/rtbpub?account=50357");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestShouldRemovePortIfHostIsSpecified() {
    -        // given
    -        final String urlWithPort = "http://test:8080/rtbpub?account=";
    -        adkernelAdnBidder = new AdkernelAdnBidder(urlWithPort, jacksonMapper);
    -
    -        final BidRequest bidRequest = givenBidRequest(
    -                identity(),
    -                extImpAdkernelAdnBuilder -> extImpAdkernelAdnBuilder.host("different.domanin.com"));
    -
    -        // when
    -        final Result>> result = adkernelAdnBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(HttpRequest::getUri)
    -                .containsOnly("http://different.domanin.com/rtbpub?account=50357");
    +                .containsOnly("http://different.domanin.com/test?account=50357");
         }
     
         @Test
    @@ -440,11 +419,6 @@ public void makeBidsShouldReturnBannerBidIfRequestImpHasBanner() throws JsonProc
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(adkernelAdnBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer,
    @@ -483,6 +457,7 @@ private static Imp givenImp(
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/adman/AdmanBidderTest.java b/src/test/java/org/prebid/server/bidder/adman/AdmanBidderTest.java
    index c25cb15aa33..d32963a99d9 100644
    --- a/src/test/java/org/prebid/server/bidder/adman/AdmanBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adman/AdmanBidderTest.java
    @@ -1,9 +1,11 @@
     package org.prebid.server.bidder.adman;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Audio;
     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.Video;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
    @@ -24,11 +26,9 @@
     import java.util.List;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.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.banner;
     import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
     
    @@ -43,29 +43,6 @@ public void setUp() {
             admanBidder = new AdmanBidder(ENDPOINT_URL, jacksonMapper);
         }
     
    -    @Test
    -    public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException().isThrownBy(() -> new AdmanBidder("invalid_url", jacksonMapper));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder()
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    -                        .build()))
    -                .build();
    -
    -        // when
    -        final Result>> result = admanBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
         @Test
         public void makeHttpRequestsShouldReturnExpectedBidRequest() {
             // given
    @@ -81,7 +58,7 @@ public void makeHttpRequestsShouldReturnExpectedBidRequest() {
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .containsOnly(expectedRequest);
    +                .containsExactly(expectedRequest);
         }
     
         @Test
    @@ -104,103 +81,115 @@ public void makeHttpRequestsShouldMakeOneRequestPerImp() {
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp).hasSize(2)
                     .extracting(Imp::getTagid)
    -                .containsOnly("tagidString", "otherTagId");
    +                .containsExactly("tagidString", "otherTagId");
         }
     
         @Test
    -    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
             // given
    -        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
     
             // when
             final Result> result = admanBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    -    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +    public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException {
             // given
    -        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
     
             // when
             final Result> result = admanBidder.makeBids(httpCall, null);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
         @Test
    -    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +    public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException {
             // given
    -        final HttpCall httpCall = givenHttpCall(null,
    -                mapper.writeValueAsString(BidResponse.builder().build()));
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(givenImp(identity())))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
     
             // when
             final Result> result = admanBidder.makeBids(httpCall, null);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    -    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
    +    public void makeBidsShouldReturnBannerBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(
    -                BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(),
    -                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
     
             // when
             final Result> result = admanBidder.makeBids(httpCall, null);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    -    public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException {
    +    public void makeBidsShouldReturnBannerBidIfAudioIsPresentInRequestImp() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(
                     BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .imp(singletonList(Imp.builder().id("123").audio(Audio.builder().build()).build()))
                             .build(),
    -                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
     
             // when
             final Result> result = admanBidder.makeBids(httpCall, null);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    -    public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException {
    +    public void makeBidsShouldReturnErrorWithUnknownBidTypeIfNotSupportedBidType() throws JsonProcessingException {
             // given
    -        final HttpCall httpCall = givenHttpCall(
    -                BidRequest.builder()
    -                        .imp(singletonList(givenImp(identity())))
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
                             .build(),
    -                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("125"))));
     
             // when
             final Result> result = admanBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(admanBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsExactly(BidderError.badServerResponse("Failed to find impression 125"));
    +        assertThat(result.getValue()).isEmpty();
         }
     
         private static BidRequest givenBidRequest(
    @@ -228,6 +217,7 @@ private static Imp givenImp(Function impCustomiz
         private static BidResponse givenBidResponse(
                 Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/admixer/AdmixerBidderTest.java b/src/test/java/org/prebid/server/bidder/admixer/AdmixerBidderTest.java
    index e82672f549b..462a7d78710 100644
    --- a/src/test/java/org/prebid/server/bidder/admixer/AdmixerBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/admixer/AdmixerBidderTest.java
    @@ -23,13 +23,12 @@
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.request.admixer.ExtImpAdmixer;
     
    -import java.util.Collections;
    +import java.math.BigDecimal;
     import java.util.List;
     import java.util.Map;
     import java.util.function.Function;
     
     import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.Collections.singletonMap;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -56,18 +55,79 @@ public void creationShouldFailOnInvalidEndpointUrl() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpressionListSizeIsZero() {
    +    public void shouldSetBidfloorToZeroIfExtImpFloorValuIsNull() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .imp(emptyList())
    -                        .build();
    +                .imp(singletonList(Imp.builder()
    +                        .id("123")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpAdmixer.of("tentententtentententtentententetetet", null,
    +                                        givenCustomParams("foo1", singletonList("bar1"))))))
    +                        .build()))
    +                .build();
     
             // when
             final Result>> result = admixerBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("No valid impressions in the bid request"));
    +        final Imp expectedImp = Imp.builder()
    +                .id("123")
    +                .tagid("tentententtentententtentententetetet")
    +                .bidfloor(BigDecimal.ZERO)
    +                .ext(mapper.valueToTree(ExtImpAdmixer.of(null, null,
    +                                givenCustomParams("foo1", singletonList("bar1")))))
    +                .build();
    +        assertThat(result.getErrors()).hasSize(0);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .containsExactly(expectedImp);
    +    }
    +
    +    @Test
    +    public void shouldSetExtToNullIfCustomParamsAreNotPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .id("123")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpAdmixer.of("tentententtentententtentententetetet", null, null))))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = admixerBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final Imp expectedImp = Imp.builder()
    +                .id("123")
    +                .tagid("tentententtentententtentententetetet")
    +                .bidfloor(BigDecimal.ZERO)
    +                .ext(null)
    +                .build();
    +        assertThat(result.getErrors()).hasSize(0);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .containsExactly(expectedImp);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .id("123")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))).build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = admixerBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_input
    +                && error.getMessage().startsWith("Wrong Admixer bidder ext in imp with id : 123"));
         }
     
         @Test
    @@ -85,7 +145,7 @@ public void makeHttpRequestsShouldReturnErrorIfZoneIdNotHaveLength() {
     
             // then
             assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("ZoneId must be UUID/GUID"));
    +                .containsExactly(BidderError.badInput("ZoneId must be UUID/GUID"));
         }
     
         @Test
    @@ -97,7 +157,22 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             final Result> result = admixerBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_server_response
    +                && error.getMessage().startsWith("Failed to decode:"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyResponseWhenResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall =
    +                givenHttpCall(mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = admixerBidder.makeBids(httpCall, BidRequest.builder().build());
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -112,9 +187,7 @@ public void makeBidsShouldReturnErrorsWhenSeatBidIsEmptyList() throws JsonProces
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result).isNotNull()
    -                .extracting(Result::getValue, Result::getErrors)
    -                .containsOnly(Collections.emptyList(), Collections.emptyList());
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    @@ -124,17 +197,34 @@ public void makeBidsShouldReturnErrorsWhenBidsEmptyList()
             final HttpCall httpCall =
                     givenHttpCall(mapper.writeValueAsString(
                             BidResponse.builder()
    -                        .seatbid(singletonList(SeatBid.builder().bid(emptyList()).build()))
    -                        .build()));
    +                                .seatbid(singletonList(SeatBid.builder().bid(emptyList()).build()))
    +                                .build()));
     
             // when
             final Result> result = admixerBidder.makeBids(httpCall, BidRequest.builder().build());
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result).isNotNull()
    -                .extracting(Result::getValue, Result::getErrors)
    -                .containsOnly(Collections.emptyList(), Collections.emptyList());
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = admixerBidder.makeBids(httpCall,
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build());
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    @@ -148,12 +238,12 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws
             final Result> result = admixerBidder.makeBids(httpCall,
                     BidRequest.builder()
                             .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    -                .build());
    +                        .build());
     
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    @@ -172,7 +262,7 @@ public void makeBidsShouldReturnBannerBidIfVideoIsPresentInRequestImp() throws J
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
         @Test
    @@ -191,7 +281,7 @@ public void makeBidsShouldReturnBannerBidIfNativeIsPresentInRequestImp() throws
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
         }
     
         @Test
    @@ -210,16 +300,12 @@ public void makeBidsShouldReturnBannerBidIfAudioIsPresentInRequestImp() throws J
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD"));
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(admixerBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD"));
         }
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
                     .build();
    diff --git a/src/test/java/org/prebid/server/bidder/adocean/AdoceanBidderTest.java b/src/test/java/org/prebid/server/bidder/adocean/AdoceanBidderTest.java
    index f20fff04dd6..9704b6f8bb8 100644
    --- a/src/test/java/org/prebid/server/bidder/adocean/AdoceanBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adocean/AdoceanBidderTest.java
    @@ -1,6 +1,7 @@
     package org.prebid.server.bidder.adocean;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.App;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.Device;
    @@ -33,13 +34,13 @@
     import java.util.Map;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyList;
    +import static java.util.Arrays.asList;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
     import static org.assertj.core.api.Assertions.tuple;
    -import static java.util.Arrays.asList;
    +import static org.prebid.server.bidder.model.BidderError.Type.bad_server_response;
     
     public class AdoceanBidderTest extends VertxTest {
     
    @@ -60,10 +61,35 @@ public void creationShouldFailOnInvalidEndpointUrl() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpressionListSizeIsZero() {
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .id("123")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = adoceanBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Error parsing adOceanExt parameters, in imp with id : 123"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfEndpointUrlComposingFails() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .imp(emptyList())
    +                .user(User.builder()
    +                        .ext(ExtUser.builder()
    +                                .consent("consent").build())
    +                        .build())
    +                .imp(singletonList(Imp.builder()
    +                        .id("ao-test")
    +                        .banner(Banner.builder().build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpAdocean.of("invalid domain", "masterId",
    +                                        "adoceanmyaozpniqismex")))).build()))
    +                .test(1)
                     .build();
     
             // when
    @@ -71,26 +97,91 @@ public void makeHttpRequestsShouldReturnErrorIfImpressionListSizeIsZero() {
     
             // then
             assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("No impression in the bid request"));
    +                .allSatisfy(error -> {
    +                    assertThat(error.getMessage()).startsWith("Invalid url: https://invalid domain/");
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +                });
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +    public void makeHttpRequestsShouldCreateRequestForEveryValidImp() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder
    -                        .id("123")
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .user(User.builder()
    +                        .buyeruid("testBuyerUid")
    +                        .ext(ExtUser.builder()
    +                                .consent("consent").build())
    +                        .build())
    +                .imp(asList(Imp.builder()
    +                                .id("ao-test")
    +                                .banner(Banner.builder().format(asList(Format.builder().h(250).w(300).build(),
    +                                        Format.builder().h(320).w(600).build())).build())
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                        ExtImpAdocean.of("myao.adocean.pl", "masterId",
    +                                                "adoceanmyaozpniqismex")))).build(),
    +                        Imp.builder()
    +                                .id("notValidImp")
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))).build(),
    +                        Imp.builder()
    +                                .id("i2-test")
    +                                .banner(Banner.builder().w(577).h(333).build())
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                        ExtImpAdocean.of("em.dom", "masterId2",
    +                                                "slaveId")))).build()))
    +                .test(1)
    +                .build();
    +
    +        // when
    +        final Result>> result = adoceanBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Error parsing adOceanExt parameters, "
    +                        + "in imp with id : notValidImp"));
    +        assertThat(result.getValue()).hasSize(2)
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://myao.adocean.pl/_10000000/ad.json?pbsrv_v=1.2.0&id=masterId&nc=1"
    +                        + "&nosecure=1&aid=adoceanmyaozpniqismex%3Aao-test&gdpr_consent=consent&gdpr=1"
    +                        + "&hcuserid=testBuyerUid&aosspsizes=myaozpniqismex"
    +                        + "%7E300x250_600x320", "https://em.dom/_10000000/ad.json?pbsrv_v=1.2.0&id="
    +                        + "masterId2&nc=1&nosecure=1&aid=slaveId%3Ai2-test&gdpr_consent=consent&gdpr=1"
    +                        + "&hcuserid=testBuyerUid&aosspsizes=slaveId%7E577x333");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateUniqueRequestIfMasterIdEqualsAndSlaveIdExists() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .user(User.builder()
    +                        .buyeruid("testBuyerUid")
    +                        .ext(ExtUser.builder()
    +                                .consent("consent").build())
    +                        .build())
    +                .imp(asList(Imp.builder()
    +                                .id("ao-test")
    +                                .banner(Banner.builder().format(asList(Format.builder().h(250).w(300).build(),
    +                                        Format.builder().h(320).w(600).build())).build())
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                        ExtImpAdocean.of("myao.adocean.pl", "masterId",
    +                                                "slaveId")))).build(),
    +                        Imp.builder()
    +                                .id("i2-test")
    +                                .banner(Banner.builder().w(577).h(333).build())
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                        ExtImpAdocean.of("em.dom", "masterId",
    +                                                "slaveId")))).build()))
    +                .test(1)
    +                .build();
    +
             // when
             final Result>> result = adoceanBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getValue()).hasSize(2);
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetExpectedRequestUrl() {
    +    public void makeHttpRequestsShouldCreateRequestWithoutSizeIfBannerSizesNotPresent() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
                     .user(User.builder()
    @@ -99,9 +190,10 @@ public void makeHttpRequestsShouldSetExpectedRequestUrl() {
                             .build())
                     .imp(singletonList(Imp.builder()
                             .id("ao-test")
    +                        .banner(Banner.builder().build())
                             .ext(mapper.valueToTree(ExtPrebid.of(null,
                                     ExtImpAdocean.of("myao.adocean.pl", "masterId",
    -                                        "slaveId")))).build()))
    +                                        "adoceanmyaozpniqismex")))).build()))
                     .test(1)
                     .build();
     
    @@ -110,10 +202,46 @@ public void makeHttpRequestsShouldSetExpectedRequestUrl() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://myao.adocean.pl/_10000000/ad.json?pbsrv_v=1.2.0&id=masterId&nc=1&nosecure=1"
    +                        + "&aid=adoceanmyaozpniqismex%3Aao-test&gdpr_consent=consent&gdpr=1");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateRequestsForSimilarSlaveIds() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .user(User.builder()
    +                        .buyeruid("testBuyerUid")
    +                        .ext(ExtUser.builder()
    +                                .consent("consent").build())
    +                        .build())
    +                .imp(asList(Imp.builder()
    +                                .id("ao-test")
    +                                .banner(Banner.builder().format(asList(Format.builder().h(250).w(300).build(),
    +                                        Format.builder().h(320).w(600).build())).build())
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                        ExtImpAdocean.of("myao.adocean.pl", "masterId",
    +                                                "slaveId")))).build(),
    +                        Imp.builder()
    +                                .id("i2-test")
    +                                .banner(Banner.builder().w(577).h(333).build())
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                        ExtImpAdocean.of("em.dom", "masterId",
    +                                                "slaveId2")))).build()))
    +                .test(1)
    +                .build();
    +
    +        // when
    +        final Result>> result = adoceanBidder.makeHttpRequests(bidRequest);
    +
    +        // then
             assertThat(result.getValue()).hasSize(1)
                     .extracting(HttpRequest::getUri)
    -                .containsOnly("https://myao.adocean.pl/_10000000/ad.json?pbsrv_v=1.0.0&id=masterId&nc=1&nosecure=1"
    -                        + "&aid=slaveId%3Aao-test&gdpr_consent=consent&gdpr=1");
    +                .containsExactlyInAnyOrder("https://myao.adocean.pl/_10000000/ad.json?pbsrv_v=1.2.0&id=masterId&nc=1"
    +                        + "&nosecure=1&aid=slaveId%3Aao-test&gdpr_consent=consent&gdpr=1&hcuserid=testBuyerUid"
    +                        + "&aosspsizes=slaveId%7E300x250_600x320&aid=slaveId2%3Ai2-test&aosspsizes=slaveId2%7E577x333");
         }
     
         @Test
    @@ -128,7 +256,7 @@ public void makeHttpRequestsShouldSetExpectedHeadersIfDeviceIpIsPresent() {
                             .id("ao-test")
                             .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(250).build()))
                                     .id("banner_id").build())
    -                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
                                     ExtImpAdocean.of("myao.adocean.pl",
                                             "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7",
                                             "adoceanmyaozpniqismex"))))
    @@ -144,7 +272,7 @@ public void makeHttpRequestsShouldSetExpectedHeadersIfDeviceIpIsPresent() {
             // then
             assertThat(result.getValue().get(0).getHeaders()).isNotNull()
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    +                .containsExactly(tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()),
                             tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "192.168.1.1"),
                             tuple(HttpUtil.REFERER_HEADER.toString(), "http://www.example.com"));
    @@ -175,26 +303,12 @@ public void makeHttpRequestsShouldSetExpectedHeadersIfDeviceIpv6IsPresent() {
             // then
             assertThat(result.getValue().get(0).getHeaders()).isNotNull()
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    +                .containsExactly(tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()),
                             tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
                             tuple(HttpUtil.REFERER_HEADER.toString(), "http://www.example.com"));
         }
     
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = adoceanBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
         @Test
         public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             // given
    @@ -204,9 +318,12 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             final Result> result = adoceanBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    -        assertThat(result.getErrors().get(0).getMessage())
    -                .startsWith("Failed to decode: No content to map due to end-of-input");
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(bad_server_response);
    +                    assertThat(error.getMessage())
    +                            .startsWith("Failed to decode: No content to map due to end-of-input");
    +                });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -218,30 +335,8 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio
                             .id("impId")
                             .build()))
                     .build();
    -        final List adoceanResponseAdUnit = asList(AdoceanResponseAdUnit.builder()
    -                .id("ad")
    -                .price("1")
    -                .winUrl("https://win-url.com")
    -                .statsUrl("https://stats-url.com")
    -                .code("  ")
    -                .currency("EUR")
    -                .width("300")
    -                .height("250")
    -                .crid("0af345b42983cc4bc0")
    -                .error("false")
    -                .build(),
    -                AdoceanResponseAdUnit.builder()
    -                        .id("adoceanmyaozpniqis")
    -                        .price("1")
    -                        .winUrl("https://win-url.com")
    -                        .statsUrl("https://stats-url.com")
    -                        .code("  ")
    -                        .currency("EUR")
    -                        .width("300")
    -                        .height("250")
    -                        .crid("0af345b42983cc4bc0")
    -                        .error("false")
    -                        .build());
    +        final List adoceanResponseAdUnit = asList(adoceanResponseCreator(identity()),
    +                adoceanResponseCreator(response -> response.id("adoceanmyaozpniqis")));
     
             final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(adoceanResponseAdUnit));
     
    @@ -280,30 +375,9 @@ public void makeBidsShouldReturnEmptyListOfBids() throws JsonProcessingException
                             .id("impId")
                             .build()))
                     .build();
    -        final List adoceanResponseAdUnit = asList(AdoceanResponseAdUnit.builder()
    -                        .id("ad")
    -                        .price("1")
    -                        .winUrl("https://win-url.com")
    -                        .statsUrl("https://stats-url.com")
    -                        .code("  ")
    -                        .currency("EUR")
    -                        .width("300")
    -                        .height("250")
    -                        .crid("0af345b42983cc4bc0")
    -                        .error("true")
    -                        .build(),
    -                AdoceanResponseAdUnit.builder()
    -                        .id("adoceanmyaozpniqis")
    -                        .price("1")
    -                        .winUrl("https://win-url.com")
    -                        .statsUrl("https://stats-url.com")
    -                        .code("  ")
    -                        .currency("EUR")
    -                        .width("300")
    -                        .height("250")
    -                        .crid("0af345b42983cc4bc0")
    -                        .error("false")
    -                        .build());
    +        final List adoceanResponseAdUnit = asList(
    +                adoceanResponseCreator(response -> response.error("true")),
    +                adoceanResponseCreator(response -> response.id("adoceanmyaozpniqis")));
     
             final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(adoceanResponseAdUnit));
     
    @@ -315,6 +389,114 @@ public void makeBidsShouldReturnEmptyListOfBids() throws JsonProcessingException
             assertThat(result.getValue()).isEqualTo(Collections.emptyList());
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldBuildUrlIfAppIsPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .user(User.builder()
    +                        .ext(ExtUser.builder()
    +                                .consent("consent").build())
    +                        .build())
    +                .imp(singletonList(Imp.builder()
    +                        .id("ao-test")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpAdocean.of("myao.adocean.pl", "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7",
    +                                        "adoceanmyaozpniqismex"))))
    +                        .build()))
    +                .test(1)
    +                .app(App.builder().name("name").bundle("bundle").domain("domain").build())
    +                .build();
    +
    +        // when
    +        final Result>> result = adoceanBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getUri)
    +                .containsExactlyInAnyOrder("https://myao.adocean.pl/_10000000/ad.json?pbsrv_v=1.2.0"
    +                        + "&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1"
    +                        + "&aid=adoceanmyaozpniqismex%3Aao-test&gdpr_consent=consent"
    +                        + "&gdpr=1&app=1&appname=name&appbundle=bundle&appdomain=domain");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldBuildUrlIfDeviceWithIfaIsPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .user(User.builder()
    +                        .ext(ExtUser.builder()
    +                                .consent("consent").build())
    +                        .build())
    +                .imp(singletonList(Imp.builder()
    +                        .id("ao-test")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpAdocean.of("myao.adocean.pl", "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7",
    +                                        "adoceanmyaozpniqismex"))))
    +                        .build()))
    +                .test(1)
    +                .device(Device.builder().ifa("ifa").os("os").osv("osv").model("model").make("make").build())
    +                .build();
    +
    +        // when
    +        final Result>> result = adoceanBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getUri)
    +                .containsExactlyInAnyOrder("https://myao.adocean.pl/_10000000/ad.json?pbsrv_v=1.2.0"
    +                        + "&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7"
    +                        + "&nc=1&nosecure=1&aid=adoceanmyaozpniqismex%3Aao-test"
    +                        + "&gdpr_consent=consent&gdpr=1&ifa=ifa&devos=os&devosv=osv&devmodel=model&devmake=make");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldBuildUrlIfDeviceWithIfaIsNotPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .user(User.builder()
    +                        .ext(ExtUser.builder()
    +                                .consent("consent").build())
    +                        .build())
    +                .imp(singletonList(Imp.builder()
    +                        .id("ao-test")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpAdocean.of("myao.adocean.pl", "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7",
    +                                        "adoceanmyaozpniqismex"))))
    +                        .build()))
    +                .test(1)
    +                .device(Device.builder().dpidmd5("dpidmd5").os("os").osv("osv").model("model").make("make").build())
    +                .build();
    +
    +        // when
    +        final Result>> result = adoceanBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getUri)
    +                .containsExactlyInAnyOrder("https://myao.adocean.pl/_10000000/ad.json?pbsrv_v=1.2.0"
    +                        + "&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7"
    +                        + "&nc=1&nosecure=1&aid=adoceanmyaozpniqismex%3Aao-test"
    +                        + "&gdpr_consent=consent&gdpr=1&dpidmd5=dpidmd5&devos=os&devosv=osv"
    +                        + "&devmodel=model&devmake=make");
    +    }
    +
    +    private static AdoceanResponseAdUnit adoceanResponseCreator(
    +            Function adoceanCustomizer) {
    +        return adoceanCustomizer.apply(AdoceanResponseAdUnit.builder()
    +                .id("ad")
    +                .price("1")
    +                .winUrl("https://win-url.com")
    +                .statsUrl("https://stats-url.com")
    +                .code("  ")
    +                .currency("EUR")
    +                .width("300")
    +                .height("250")
    +                .crid("0af345b42983cc4bc0")
    +                .error("false"))
    +                .build();
    +    }
    +
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
    diff --git a/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java b/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java
    index 23cc5d050ca..607d308944a 100644
    --- a/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adoppler/AdopplerBidderTest.java
    @@ -4,14 +4,16 @@
     import com.fasterxml.jackson.databind.node.ObjectNode;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Imp;
     import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
     import com.iab.openrtb.response.SeatBid;
     import io.netty.handler.codec.http.HttpHeaderValues;
    +import org.junit.Before;
    +import org.junit.Test;
     import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.adoppler.model.AdopplerResponseAdsExt;
     import org.prebid.server.bidder.adoppler.model.AdopplerResponseExt;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderError;
    @@ -22,16 +24,12 @@
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.request.adoppler.ExtImpAdoppler;
     import org.prebid.server.util.HttpUtil;
    -import org.junit.Before;
    -import org.junit.Test;
     
    -import java.util.ArrayList;
     import java.util.Collections;
     import java.util.List;
     import java.util.Map;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -40,7 +38,7 @@
     
     public class AdopplerBidderTest extends VertxTest {
     
    -    private static final String ENDPOINT_URL = "https://test.endpoint.com";
    +    private static final String ENDPOINT_URL = "http://{{AccountID}}.test.com/some/path/{{AdUnit}}";
     
         private AdopplerBidder adopplerBidder;
     
    @@ -59,26 +57,19 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     impBuilder -> impBuilder
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder().w(300).h(500).build()))
    -                                .build())
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdoppler.of(null)))));
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdoppler.of(null, null)))));
             // when
             final Result>> result = adopplerBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("$.imp.ext.adoppler.adunit required");
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("adunit parameter is required for adoppler bidder"));
         }
     
         @Test
         public void makeHttpRequestsShouldCreateCorrectURL() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder().w(300).h(500).build()))
    -                                .build()));
    +        final BidRequest bidRequest = givenBidRequest(identity());
     
             // when
             final Result>> result = adopplerBidder.makeHttpRequests(bidRequest);
    @@ -86,76 +77,40 @@ public void makeHttpRequestsShouldCreateCorrectURL() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1);
    -        assertThat(result.getValue().get(0).getUri()).isEqualTo("https://test.endpoint.com/processHeaderBid/adUnit");
    +        assertThat(result.getValue().get(0).getUri()).isEqualTo("http://clientId.test.com/some/path/adUnit");
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetExpectedRequestUrlAndDefaultHeaders() {
    +    public void makeHttpRequestsShouldCreateUrlWithDefaultAppParamIfClientIsMissing() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(identity());
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdoppler.of("adUnit", "")))));
     
             // when
             final Result>> result = adopplerBidder.makeHttpRequests(bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue().get(0).getHeaders()).isNotNull()
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple("x-openrtb-version", "2.5"),
    -                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    -                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnErrorIfDuplicateId() throws JsonProcessingException {
    -        // given
    -        final Imp imp1 = Imp.builder().id("impId").banner(Banner.builder().build()).build();
    -        final Imp imp2 = Imp.builder().id("impId").video(Video.builder().build()).build();
    -        final List imps = new ArrayList();
    -        imps.add(imp1);
    -        imps.add(imp2);
    -        BidRequest bidRequest = BidRequest.builder()
    -                .imp(imps)
    -                .build();
    -        final HttpCall httpCall = givenHttpCall(
    -                bidRequest, mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    -
    -        // when
    -        final Result> result = adopplerBidder.makeBids(httpCall, bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage())
    -                .startsWith("duplicate $.imp.id impId");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input);
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getValue().get(0).getUri()).isEqualTo("http://app.test.com/some/path/adUnit");
         }
     
         @Test
    -    public void makeBidsShouldReturnErrorIfEmptyImp() throws JsonProcessingException {
    +    public void makeHttpRequestsShouldSetExpectedHeaders() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder().id("123")
    -                        .banner(null)
    -                        .video(null)
    -                        .audio(null)
    -                        .xNative(null)
    -                        .build()))
    -                .build();
    -        final HttpCall httpCall = givenHttpCall(
    -                bidRequest, mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +        final BidRequest bidRequest = givenBidRequest(identity());
     
             // when
    -        final Result> result = adopplerBidder.makeBids(httpCall, bidRequest);
    +        final Result>> result = adopplerBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage())
    -                .startsWith("one of $.imp.banner, $.imp.video, $.imp.audio and $.imp.native field required");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input);
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue().get(0).getHeaders()).isNotNull()
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .containsExactlyInAnyOrder(tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"),
    +                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    +                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
         }
     
         @Test
    @@ -174,10 +129,8 @@ public void makeBidsShouldReturnErrorIfBidIdEmpty() throws JsonProcessingExcepti
             final Result> result = adopplerBidder.makeBids(httpCall, bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage())
    -                .startsWith("unknown impid: null");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input);
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("unknown impId: null"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -185,9 +138,8 @@ public void makeBidsShouldReturnErrorIfBidIdEmpty() throws JsonProcessingExcepti
         public void makeBidsShouldReturnErrorIfExtEmpty() throws JsonProcessingException {
             // given
             final Imp imp = Imp.builder().id("impId").video(Video.builder().build()).build();
    -        final List imps = Collections.singletonList(imp);
    -        final BidRequest bidRequest = BidRequest.builder().imp(imps).build();
    -        final ObjectNode ext = mapper.valueToTree(AdopplerResponseExt.of(null));
    +        final BidRequest bidRequest = BidRequest.builder().imp(Collections.singletonList(imp)).build();
    +        final ObjectNode ext = mapper.valueToTree(AdopplerResponseExt.of(AdopplerResponseAdsExt.of(null)));
             final HttpCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(
                     givenBidResponse(bidBuilder -> bidBuilder
                             .id("321")
    @@ -198,32 +150,11 @@ public void makeBidsShouldReturnErrorIfExtEmpty() throws JsonProcessingException
             final Result> result = adopplerBidder.makeBids(httpCall, bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage())
    -                .startsWith("$.seatbid.bid.ext.ads.video required");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input);
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = adopplerBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("$.seatbid.bid.ext.ads.video required"));
             assertThat(result.getValue()).isEmpty();
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(adopplerBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
    @@ -241,13 +172,15 @@ private static Imp givenImp(Function impCustomiz
             return impCustomizer.apply(Imp.builder()
                     .id("123")
                     .banner(Banner.builder().id("banner_id").build())
    -                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdoppler.of("adUnit")))))
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdoppler.of("adUnit", "clientId")))))
                     .build();
         }
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    -                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
                     .build();
         }
    diff --git a/src/test/java/org/prebid/server/bidder/adot/AdotBidderTest.java b/src/test/java/org/prebid/server/bidder/adot/AdotBidderTest.java
    new file mode 100644
    index 00000000000..b06ad1b8ddd
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/adot/AdotBidderTest.java
    @@ -0,0 +1,214 @@
    +package org.prebid.server.bidder.adot;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.adot.model.AdotBidExt;
    +import org.prebid.server.bidder.adot.model.AdotExtAdot;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.adot.ExtImpAdot;
    +
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class AdotBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private AdotBidder adotBidder;
    +
    +    @Before
    +    public void setUp() {
    +        adotBidder = new AdotBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new AdotBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotModifyIncomingBidRequestAndPassItOn() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = adotBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +
    +        final List> httpRequests = result.getValue();
    +        assertThat(httpRequests)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .containsExactly(bidRequest);
    +        assertThat(httpRequests)
    +                .extracting(HttpRequest::getPayload)
    +                .containsExactly(bidRequest);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = adotBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsEmpty() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = adotBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsEmpty() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = adotBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidExtAdotMediaTypeIsInvalid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(givenBidResponse("invalid")));
    +
    +        // when
    +        final Result> result = adotBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidWhenBidExtAdotMediaTypeIsVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(givenBidResponse("video")));
    +
    +        // when
    +        final Result> result = adotBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(givenBid("video"), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidWhenBidExtAdotMediaTypeIsBanner() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(givenBidResponse("banner")));
    +
    +        // when
    +        final Result> result = adotBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(givenBid("banner"), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidWhenBidExtAdotMediaTypeIsNative() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(givenBidResponse("native")));
    +
    +        // when
    +        final Result> result = adotBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(givenBid("native"), xNative, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("firstImp")
    +                .banner(Banner.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdot.of(true, "placementId")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(String bidExtMediaType) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(givenBid(bidExtMediaType)))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static Bid givenBid(String bidExtMediaType) {
    +        return Bid.builder()
    +                .ext(mapper.valueToTree(AdotBidExt.of(AdotExtAdot.of(bidExtMediaType))))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/adpone/AdponeBidderTest.java b/src/test/java/org/prebid/server/bidder/adpone/AdponeBidderTest.java
    index 5bfeb2438af..be50215ffcf 100644
    --- a/src/test/java/org/prebid/server/bidder/adpone/AdponeBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adpone/AdponeBidderTest.java
    @@ -147,6 +147,7 @@ public void makeBidsShouldReturnReturnBannerBid() throws JsonProcessingException
             final Bid bid = Bid.builder().id("bidId").build();
             final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(
                     BidResponse.builder()
    +                        .cur("USD")
                             .seatbid(singletonList(SeatBid.builder()
                                     .bid(singletonList(bid))
                                     .build()))
    @@ -161,11 +162,6 @@ public void makeBidsShouldReturnReturnBannerBid() throws JsonProcessingException
                     .containsOnly(BidderBid.of(bid, BidType.banner, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(adponeBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(Collections.emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(JsonNode bidderNode) {
             return BidRequest.builder()
                     .imp(Collections.singletonList(Imp.builder()
    diff --git a/src/test/java/org/prebid/server/bidder/adprime/AdprimeBidderTest.java b/src/test/java/org/prebid/server/bidder/adprime/AdprimeBidderTest.java
    new file mode 100644
    index 00000000000..8a7bc1bc7d2
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/adprime/AdprimeBidderTest.java
    @@ -0,0 +1,237 @@
    +package org.prebid.server.bidder.adprime;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.adprime.ExtImpAdprime;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +
    +public class AdprimeBidderTest extends VertxTest {
    +
    +    public static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private AdprimeBidder adprimeBidder;
    +
    +    @Before
    +    public void setUp() {
    +        adprimeBidder = new AdprimeBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new AdprimeBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = adprimeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnExpectedBidRequest() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = adprimeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final BidRequest expectedRequest = bidRequest.toBuilder()
    +                .imp(singletonList(bidRequest.getImp().get(0).toBuilder()
    +                        .tagid("tagidString").build()))
    +                .build();
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .containsOnly(expectedRequest);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestPerImp() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        Imp.builder()
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdprime.of("otherTagId"))))
    +                                .build())));
    +
    +        // when
    +        final Result>> result = adprimeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(2)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).hasSize(2)
    +                .extracting(Imp::getTagid)
    +                .containsOnly("tagidString", "otherTagId");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = adprimeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = adprimeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = adprimeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adprimeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adprimeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(givenImp(identity())))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adprimeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer) {
    +        return givenBidRequest(impCustomizer, identity());
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .video(Video.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdprime.of("tagidString"))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(
    +            Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java b/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java
    index e32fa29657b..7c3e00ed205 100644
    --- a/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java
    @@ -32,9 +32,11 @@
     import java.math.BigDecimal;
     import java.util.List;
     import java.util.Map;
    +import java.util.function.Function;
     
     import static java.util.Arrays.asList;
     import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.tuple;
     
    @@ -50,82 +52,77 @@ public void setUp() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMethod()
    -            throws JsonProcessingException {
    +    public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMethod() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder()
    -                        .banner(Banner.builder().build())
    -                        .ext(mapper.valueToTree(
    -                                ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))).build()))
    -                .user(User.builder()
    -                        .ext(ExtUser.builder().consent("consent").build())
    -                        .build())
    -                .regs(Regs.of(0, ExtRegs.of(1, null)))
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(identity());
     
             // when
             final Result>> result = adtargetBidder.makeHttpRequests(bidRequest);
     
             // then
    +        final BidRequest expectedBidRequest = bidRequest
    +                .toBuilder()
    +                .imp(singletonList(bidRequest.getImp().get(0).toBuilder()
    +                        .bidfloor(BigDecimal.valueOf(3))
    +                        .ext(mapper.valueToTree(AdtargetImpExt.of(
    +                                ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3)))))
    +                        .build()))
    +                .build();
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1).extracting(HttpRequest::getMethod).containsExactly(HttpMethod.POST);
             assertThat(result.getValue()).extracting(HttpRequest::getUri).containsExactly("http://adtelligent.com?aid=15");
             assertThat(result.getValue()).flatExtracting(httpRequest -> httpRequest.getHeaders().entries())
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(
    +                .containsExactlyInAnyOrder(
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
    -        assertThat(result.getValue()).extracting(HttpRequest::getBody).containsExactly(mapper.writeValueAsString(
    -                BidRequest.builder()
    -                        .imp(singletonList(
    -                                Imp.builder()
    -                                        .banner(Banner.builder().build())
    -                                        .bidfloor(BigDecimal.valueOf(3))
    -                                        .ext(mapper.valueToTree(AdtargetImpExt.of(
    -                                                ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3)))))
    -                                        .build()))
    -                        .user(User.builder()
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null)))
    -                        .build()));
    +        assertThat(result.getValue()).extracting(HttpRequest::getPayload).containsExactly(expectedBidRequest);
         }
     
         @Test
         public void makeHttpRequestShouldReturnErrorMessageWhenMediaTypeWasNotDefined() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder()
    -                        .id("impId")
    -                        .ext(mapper.valueToTree(
    -                                ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))).build()))
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(null));
     
             // when
             final Result>> result = adtargetBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    +        assertThat(result.getErrors())
                     .containsExactly(BidderError.badInput(
                             "ignoring imp id=impId, Adtarget supports only Video and Banner"));
             assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    -    public void makeHttpRequestShouldReturnErrorMessageWhenImpExtIsEmpty() {
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder()
    -                        .id("impId")
    -                        .banner(Banner.builder().build())
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, null))).build()))
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +
             // when
             final Result>> result = adtargetBidder.makeHttpRequests(bidRequest);
     
             // then
             assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(error.getMessage()).startsWith("ignoring imp id=impId, error while "
    +                            + "decoding impExt, err: Cannot deserialize instance");
    +                });
    +    }
    +
    +    @Test
    +    public void makeHttpRequestShouldReturnErrorMessageWhenImpExtIsEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, null))));
    +
    +        // when
    +        final Result>> result = adtargetBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
                     .containsExactly(BidderError.badInput("ignoring imp id=impId, extImpBidder is empty"));
             assertThat(result.getValue()).isEmpty();
         }
    @@ -150,7 +147,7 @@ public void makeHttpRequestShouldReturnHttpRequestWithErrorMessage() {
             final Result>> result = adtargetBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    +        assertThat(result.getErrors())
                     .containsExactly(BidderError.badInput("ignoring imp id=impId, extImpBidder is empty"));
             assertThat(result.getValue()).extracting(HttpRequest::getUri).containsExactly("http://adtelligent.com?aid=15");
             assertThat(result.getValue()).hasSize(1)
    @@ -176,7 +173,7 @@ public void makeHttpRequestShouldReturnWithBidFloorPopulatedFromImpWhenIsMissedI
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp).hasSize(1)
                     .extracting(Imp::getBidfloor).containsExactly(BigDecimal.valueOf(16));
         }
    @@ -205,6 +202,27 @@ public void makeHttpRequestShouldReturnTwoHttpRequestsWhenTwoImpsHasDifferentSou
             assertThat(result.getValue()).hasSize(2);
         }
     
    +    @Test
    +    public void makeHttpRequestShouldSetZeroToAidParamIfSourceIdIsNull() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().build())
    +                        .ext(mapper.valueToTree(
    +                                ExtPrebid.of(null, ExtImpAdtarget.of(null, 1, 2, BigDecimal.valueOf(3)))))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = adtargetBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("http://adtelligent.com?aid=0");
    +    }
    +
         @Test
         public void makeHttpRequestShouldReturnOneHttpRequestForTowImpsWhenImpsHasSameSourceId() {
             // given
    @@ -232,20 +250,11 @@ public void makeHttpRequestShouldReturnOneHttpRequestForTowImpsWhenImpsHasSameSo
         @Test
         public void makeBidsShouldReturnBidWithoutErrors() throws JsonProcessingException {
             // given
    -        final String response = mapper.writeValueAsString(BidResponse.builder()
    -                .cur("EUR")
    -                .seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("impId").build()))
    -                        .build()))
    -                .build());
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder().id("impId").build()))
    -                .build();
    -
    -        final HttpCall httpCall = givenHttpCall(response);
    +        final String response = mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.id(null)));
    +        final BidRequest bidRequest = givenBidRequest(identity());
     
             // when
    -        final Result> result = adtargetBidder.makeBids(httpCall, bidRequest);
    +        final Result> result = adtargetBidder.makeBids(givenHttpCall(response), bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    @@ -256,22 +265,15 @@ public void makeBidsShouldReturnBidWithoutErrors() throws JsonProcessingExceptio
         @Test
         public void makeBidsShouldReturnErrorMessageWhenMatchingToBidImpWasNotFound() throws JsonProcessingException {
             // given
    -        final String response = mapper.writeValueAsString(BidResponse.builder()
    -                .seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().id("bidId").impid("invalidId").build()))
    -                        .build()))
    -                .build());
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder().id("impId").build()))
    -                .build();
    -
    -        final HttpCall httpCall = givenHttpCall(response);
    +        final String response = mapper.writeValueAsString(
    +                givenBidResponse(bidBuilder -> bidBuilder.impid("invalidId")));
    +        final BidRequest bidRequest = givenBidRequest(identity());
     
             // when
    -        final Result> result = adtargetBidder.makeBids(httpCall, bidRequest);
    +        final Result> result = adtargetBidder.makeBids(givenHttpCall(response), bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    +        assertThat(result.getErrors())
                     .containsExactly(BidderError.badServerResponse(
                             "ignoring bid id=bidId, request doesn't contain any impression with id=invalidId"));
             assertThat(result.getValue()).isEmpty();
    @@ -286,17 +288,13 @@ public void makeBidsShouldReturnBidWithErrorMessage() throws JsonProcessingExcep
                                     Bid.builder().id("bidId2").impid("impId").build()))
                             .build()))
                     .build());
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder().id("impId").build()))
    -                .build();
    -
    -        final HttpCall httpCall = givenHttpCall(response);
    +        final BidRequest bidRequest = givenBidRequest(identity());
     
             // when
    -        final Result> result = adtargetBidder.makeBids(httpCall, bidRequest);
    +        final Result> result = adtargetBidder.makeBids(givenHttpCall(response), bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    +        assertThat(result.getErrors())
                     .containsExactly(BidderError.badServerResponse(
                             "ignoring bid id=bidId1, request doesn't contain any impression with id=invalidId"));
             assertThat(result.getValue()).hasSize(1)
    @@ -320,10 +318,8 @@ public void makeBidsShouldReturnBidsFromDifferentSeatBidsInResponse() throws Jso
                     .imp(asList(Imp.builder().id("impId1").build(), Imp.builder().id("impId2").build()))
                     .build();
     
    -        final HttpCall httpCall = givenHttpCall(response);
    -
             // when
    -        final Result> result = adtargetBidder.makeBids(httpCall, bidRequest);
    +        final Result> result = adtargetBidder.makeBids(givenHttpCall(response), bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    @@ -336,49 +332,34 @@ public void makeBidsShouldReturnBidsFromDifferentSeatBidsInResponse() throws Jso
         public void makeBidsShouldReturnBidderBidWithBannerBidTypeWhenMediaTypeInMatchedImpIsNotVideo()
                 throws JsonProcessingException {
             // given
    -        final String response = mapper.writeValueAsString(BidResponse.builder()
    -                .seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("impId").build()))
    -                        .build()))
    -                .build());
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder().id("impId").build()))
    -                .build();
    -
    -        final HttpCall httpCall = givenHttpCall(response);
    +        final String response = mapper.writeValueAsString(givenBidResponse(identity()));
    +        final BidRequest bidRequest = givenBidRequest(identity());
     
             // when
    -        final Result> result = adtargetBidder.makeBids(httpCall, bidRequest);
    +        final Result> result = adtargetBidder.makeBids(givenHttpCall(response), bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(BidderBid::getType).containsExactly(BidType.banner);
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getType)
    +                .containsExactly(BidType.banner);
         }
     
         @Test
         public void makeBidsShouldReturnBidderBidWithVideoBidTypeIfBannerAndVideoMediaTypesAreInMatchedImp()
                 throws JsonProcessingException {
             // given
    -        final String response = mapper.writeValueAsString(BidResponse.builder()
    -                .seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("impId").build()))
    -                        .build()))
    -                .build());
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder().video(Video.builder().build())
    -                        .banner(Banner.builder().build()).id("impId").build()))
    -                .build();
    -
    -        final HttpCall httpCall = givenHttpCall(response);
    +        final String response = mapper.writeValueAsString(givenBidResponse(identity()));
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.video(Video.builder().build()));
     
             // when
    -        final Result> result = adtargetBidder.makeBids(httpCall, bidRequest);
    +        final Result> result = adtargetBidder.makeBids(givenHttpCall(response), bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(BidderBid::getType).containsExactly(BidType.video);
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getType)
    +                .containsExactly(BidType.video);
         }
     
         @Test
    @@ -386,10 +367,10 @@ public void makeBidsShouldReturnEmptyBidderBidAndErrorListsIfSeatBidIsNotPresent
                 throws JsonProcessingException {
             // given
             final String response = mapper.writeValueAsString(BidResponse.builder().build());
    -        final HttpCall httpCall = givenHttpCall(response);
     
             // when
    -        final Result> result = adtargetBidder.makeBids(httpCall, BidRequest.builder().build());
    +        final Result> result = adtargetBidder
    +                .makeBids(givenHttpCall(response), BidRequest.builder().build());
     
             // then
             assertThat(result.getErrors()).isEmpty();
    @@ -406,10 +387,45 @@ public void makeBidsShouldReturnEmptyBidderWithErrorWhenResponseCantBeParsed() {
     
             // then
             assertThat(result.getErrors()).hasSize(1)
    -                .containsExactly(BidderError.badServerResponse(
    -                        "Failed to decode: Unexpected end-of-input: expected close marker for Object (start marker at"
    -                                + " [Source: (String)\"{\"; line: 1, column: 1])\n at [Source: (String)\"{\"; line: 1, "
    -                                + "column: 3]"));
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unexpected end-of-input:");
    +                });
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .user(User.builder()
    +                        .ext(ExtUser.builder().consent("consent").build())
    +                        .build())
    +                .regs(Regs.of(0, ExtRegs.of(1, null)))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("impId")
    +                .banner(Banner.builder().build())
    +                .ext(mapper.valueToTree(
    +                        ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("EUR")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder().id("bidId").impid("impId")).build()))
    +                        .build()))
    +                .build();
         }
     
         private static HttpCall givenHttpCall(String body) {
    diff --git a/src/test/java/org/prebid/server/bidder/advangelists/AdvangelistsBidderTest.java b/src/test/java/org/prebid/server/bidder/advangelists/AdvangelistsBidderTest.java
    index cdf492dea2f..c106b3cdfbe 100644
    --- a/src/test/java/org/prebid/server/bidder/advangelists/AdvangelistsBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/advangelists/AdvangelistsBidderTest.java
    @@ -33,7 +33,6 @@
     
     import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -44,7 +43,7 @@
     
     public class AdvangelistsBidderTest extends VertxTest {
     
    -    private static final String ENDPOINT_URL = "http://test/get?pubid=";
    +    private static final String ENDPOINT_URL = "http://test/get?pubid={{PublisherID}}";
     
         private AdvangelistsBidder advangelistsBidder;
     
    @@ -63,6 +62,7 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
                     .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().build())
                             .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
                             .build()))
                     .build();
    @@ -72,14 +72,18 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() {
     
             // then
             assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getErrors()).allSatisfy(error -> {
    +            assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +            assertThat(error.getMessage()).startsWith("Cannot deserialize instance");
    +        });
             assertThat(result.getValue()).isEmpty();
         }
     
         @Test
         public void makeHttpRequestsShouldReturnErrorWhenExtPubIdIsNull() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(identity(), ExtImpAdvangelists.of(null, null));
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build()),
    +                ExtImpAdvangelists.of(null, null));
     
             // when
             final Result>> result = advangelistsBidder.makeHttpRequests(bidRequest);
    @@ -93,7 +97,8 @@ public void makeHttpRequestsShouldReturnErrorWhenExtPubIdIsNull() {
         @Test
         public void makeHttpRequestsShouldReturnErrorWhenExtPubIdIsBlank() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(identity(), ExtImpAdvangelists.of(" ", null));
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build()),
    +                ExtImpAdvangelists.of("", null));
     
             // when
             final Result>> result = advangelistsBidder.makeHttpRequests(bidRequest);
    @@ -124,26 +129,21 @@ public void makeHttpRequestShouldReturnAllErrorsWithRequest() {
             // given
             final Imp impWithoutFormatFirst =
                     givenImp(impBuilder -> impBuilder.banner(Banner.builder().format(null).build()));
    -        final Imp impWithoutFormatSecond =
    -                givenImp(impBuilder -> impBuilder.banner(Banner.builder().format(null).build()));
             final Imp impWithoutType = givenImp(identity());
    -        final Imp impWithoutPubIdFirst = givenImp(identity(), ExtImpAdvangelists.of(" ", null));
    -        final Imp impWithoutPubIdSecond = givenImp(identity(), ExtImpAdvangelists.of(" ", null));
    +        final Imp impWithoutPubIdFirst = givenImp(impBuilder -> impBuilder.banner(Banner.builder().build()),
    +                ExtImpAdvangelists.of("", null));
             final Imp normalImp = givenImp(impBuilder -> impBuilder.video(Video.builder().build()));
     
             final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(impWithoutFormatFirst, impWithoutPubIdFirst, impWithoutFormatSecond, impWithoutPubIdSecond,
    -                        normalImp, impWithoutType))
    +                .imp(asList(impWithoutFormatFirst, impWithoutPubIdFirst, normalImp, impWithoutType))
                     .build();
     
             // when
             final Result>> result = advangelistsBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(5)
    -                .containsOnly(BidderError.badInput("No pubid value provided"),
    -                        BidderError.badInput("No pubid value provided"),
    -                        BidderError.badInput("Expected at least one banner.format entry or explicit w/h"),
    +        assertThat(result.getErrors()).hasSize(3)
    +                .containsExactlyInAnyOrder(BidderError.badInput("No pubid value provided"),
                             BidderError.badInput("Expected at least one banner.format entry or explicit w/h"),
                             BidderError.badInput("Unsupported impression has been received"));
             assertThat(result.getValue()).hasSize(1);
    @@ -181,7 +181,7 @@ public void makeHttpRequestsShouldFillMethodAndUrlAndExpectedHeaders() {
                     .containsOnly(
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), "application/json;charset=utf-8"),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), "application/json"),
    -                        tuple("x-openrtb-version", "2.5"));
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
         }
     
         @Test
    @@ -334,8 +334,10 @@ public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() {
     
             // then
             assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getErrors()).allSatisfy(error -> {
    +            assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +            assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -404,11 +406,6 @@ public void makeBidsShouldReturnVideoBidWhenVideoPresent() throws JsonProcessing
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(advangelistsBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer,
    @@ -445,6 +442,7 @@ private static Imp givenImp(Function impCustomiz
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/adxcg/AdxcgBidderTest.java b/src/test/java/org/prebid/server/bidder/adxcg/AdxcgBidderTest.java
    new file mode 100644
    index 00000000000..2e869622ec4
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/adxcg/AdxcgBidderTest.java
    @@ -0,0 +1,252 @@
    +package org.prebid.server.bidder.adxcg;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Native;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.adxcg.ExtImpAdxcg;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class AdxcgBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "http://test.com/";
    +
    +    private AdxcgBidder adxcgBidder;
    +
    +    @Before
    +    public void setUp() {
    +        adxcgBidder = new AdxcgBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new AdxcgBidder("invalid", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotModifyIncomingRequest() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdxcg.of("adZoneId"))))
    +                        .build()))
    +                .id("request_id")
    +                .build();
    +
    +        // when
    +        final Result>> result = adxcgBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .containsOnly(bidRequest);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = adxcgBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = adxcgBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result> result = adxcgBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.video(Video.builder().build())
    +                .banner(null));
    +
    +        // when
    +        final Result> result = adxcgBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.xNative(Native.builder().build()));
    +
    +        // when
    +        final Result> result = adxcgBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidAndErrorIfNativeAndEmptyImpsArePresent() throws JsonProcessingException {
    +        // given
    +        BidResponse bidResponse = BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(Arrays.asList(Bid.builder().impid("123").build(),
    +                                Bid.builder().impid("12").build()))
    +                        .build()))
    +                .build();
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(Arrays.asList(Imp.builder().id("123").xNative(Native.builder().build()).build(),
    +                                Imp.builder().id("12").build())).build(),
    +                mapper.writeValueAsString(bidResponse));
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.xNative(Native.builder().build()));
    +
    +        // when
    +        final Result> result = adxcgBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .extracting(BidderError::getMessage)
    +                .containsExactly("Failed to find native/banner/video impression 12");
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateOneRequestForAllImps() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(
    +                        givenImp(impBuilder -> impBuilder
    +                                .id("123")
    +                                .banner(Banner.builder()
    +                                        .format(singletonList(Format.builder().w(300).h(200).build())).build())),
    +                        givenImp(impBuilder -> impBuilder
    +                                .id("321")
    +                                .banner(Banner.builder()
    +                                        .format(singletonList(Format.builder().w(600).h(400).build())).build()))))
    +                .build();
    +
    +        // when
    +        final Result>> result = adxcgBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getId)
    +                .containsOnly("123", "321");
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().id("banner_id").build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpAdxcg.of("adzoneid")))))
    +                .build();
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/adyoulike/AdyoulikeBidderTest.java b/src/test/java/org/prebid/server/bidder/adyoulike/AdyoulikeBidderTest.java
    new file mode 100644
    index 00000000000..914df222948
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/adyoulike/AdyoulikeBidderTest.java
    @@ -0,0 +1,303 @@
    +package org.prebid.server.bidder.adyoulike;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.adyoulike.ExtImpAdyoulike;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class AdyoulikeBidderTest extends VertxTest {
    +
    +    public static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private AdyoulikeBidder adyoulikeBidder;
    +
    +    @Before
    +    public void setUp() {
    +        adyoulikeBidder = new AdyoulikeBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new AdyoulikeBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestWithAllImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        givenImp(identity()))));
    +
    +        // when
    +        final Result>> result = adyoulikeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .hasSize(2);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCanNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(Imp.builder()
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    +                                .build(),
    +                        givenImp(identity())))
    +                .build();
    +
    +        // when
    +        final Result>> result = adyoulikeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(error.getMessage()).startsWith("Cannot deserialize instance");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnEveryOccurredErrorWithNoValue() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(Imp.builder()
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    +                                .build(),
    +                        Imp.builder()
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    +                                .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = adyoulikeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(2);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpTagId() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = adyoulikeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid)
    +                .containsExactly("placementId");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = adyoulikeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = adyoulikeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = adyoulikeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adyoulikeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adyoulikeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adyoulikeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "EUR"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNoBannerAndHasNative() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().xNative(Native.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adyoulikeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "EUR"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = adyoulikeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "EUR"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer) {
    +        return givenBidRequest(impCustomizer, identity());
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpAdyoulike.of("placementId", "campaign", "track",
    +                                "creative", "source", "debug"))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("EUR")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/aja/AjaBidderTest.java b/src/test/java/org/prebid/server/bidder/aja/AjaBidderTest.java
    index 6f4e5ea4bf6..5ee510448b5 100644
    --- a/src/test/java/org/prebid/server/bidder/aja/AjaBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/aja/AjaBidderTest.java
    @@ -24,7 +24,6 @@
     import java.util.List;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -161,25 +160,6 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = ajaBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(ajaBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
    diff --git a/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java b/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java
    new file mode 100644
    index 00000000000..59043d30a7d
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java
    @@ -0,0 +1,282 @@
    +package org.prebid.server.bidder.algorix;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.request.Native;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.algorix.ExtImpAlgorix;
    +
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.groups.Tuple.tuple;
    +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;
    +
    +/**
    + * AlgoriX Bidder Test
    + */
    +public class AlgorixBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://xyz.svr-algorix.com/rtb/sa?sid={SID}&token={TOKEN}";
    +
    +    private AlgorixBidder algorixBidder;
    +
    +    @Before
    +    public void setUp() {
    +        algorixBidder = new AlgorixBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndPointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new AlgorixBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .id("123")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +
    +        // when
    +        final Result>> result = algorixBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Invalid ExtImpAlgoriX value"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorOfEveryNotValidImp() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(givenImp(impBuilder -> impBuilder
    +                                .id("123")
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))),
    +                        givenImp(identity())))
    +                .build();
    +
    +        // when
    +        final Result>> result = algorixBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Impression Id=123, has invalid Ext"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = algorixBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .hasSize(1)
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://xyz.svr-algorix.com/rtb/sa?sid=testSid&token=testToken");
    +    }
    +
    +    @Test
    +    public void shouldSetBannerFormatWAndHValuesToBannerIfTheyAreNotPresentInBanner() {
    +        // given
    +        final Format bannerFormat = Format.builder().w(320).h(50).build();
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(Banner.builder()
    +                        .format(singletonList(bannerFormat)).build()));
    +
    +        // when
    +        final Result>> result = algorixBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(0);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .extracting(Banner::getW, Banner::getH)
    +                .containsOnly(tuple(320, 50));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = algorixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = algorixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = algorixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = algorixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = algorixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = algorixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = algorixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().id("banner_id").build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpAlgorix.of("testSid", "testToken")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java b/src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java
    new file mode 100644
    index 00000000000..eb5deed54fc
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/amx/AmxBidderTest.java
    @@ -0,0 +1,298 @@
    +package org.prebid.server.bidder.amx;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.iab.openrtb.request.App;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Publisher;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.amx.ExtImpAmx;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +
    +public class AmxBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.com/prebid/bid";
    +
    +    private AmxBidder amxBidder;
    +
    +    @Before
    +    public void setUp() {
    +        amxBidder = new AmxBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new AmxBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = amxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_input
    +                && error.getMessage().startsWith("Cannot deserialize instance"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build()));
    +
    +        // when
    +        final Result>> result = amxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://test.com/prebid/bid?v=pbs1.1");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateRequestAndImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.app(App.builder().build()).site(Site.builder().build()),
    +                impBuilder -> impBuilder.banner(Banner.builder().build()));
    +
    +        // when
    +        final Result>> result = amxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final BidRequest expectedBidRequest = BidRequest.builder()
    +                .app(App.builder().publisher(Publisher.builder().id("testTagId").build()).build())
    +                .site(Site.builder().publisher(Publisher.builder().id("testTagId").build()).build())
    +                .imp(singletonList(Imp.builder()
    +                        .id("123")
    +                        .banner(Banner.builder().build())
    +                        .tagid("testAdUnitId")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpAmx.of("testTagId", "testAdUnitId"))))
    +                        .build())).build();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .containsExactly(expectedBidRequest);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = amxBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_server_response
    +                && error.getMessage().startsWith("Failed to decode: Unrecognized token"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = amxBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = amxBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsBidExtNotPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().build(), mapper.writeValueAsString(givenBidResponse(identity())));
    +
    +        // when
    +        final Result> result = amxBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfStartDelayIsPresentInBidExt() throws JsonProcessingException {
    +        // given
    +        final ObjectNode bidExt = mapper.createObjectNode();
    +        bidExt.put("himp", mapper.convertValue(Arrays.asList("someHintVAlue1", "someHintValue2"), JsonNode.class));
    +        bidExt.put("startdelay", "2");
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder().build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder
    +                                .adm("ExistingAdm")
    +                                .nurl("nurlValue")
    +                                .ext(bidExt))));
    +
    +        // when
    +        final Result> result = amxBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        final String expectedAdm = "ExistingAdm"
    +                + ""
    +                + ""
    +                + "";
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder()
    +                        .nurl("")
    +                        .ext(bidExt)
    +                        .adm(expectedAdm)
    +                        .build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfAdmNotContainSearchPoint() throws JsonProcessingException {
    +        // given
    +        final ObjectNode bidExt = mapper.createObjectNode();
    +        bidExt.put("startdelay", "2");
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder().build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder
    +                                .id("bidId")
    +                                .adm("no_point")
    +                                .ext(bidExt))));
    +
    +        // when
    +        final Result> result = amxBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(
    +                        BidderError.badServerResponse("Adm should contain vast search point in bidder: bidId"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldSkipBidAndAddErrorIfFailedToParseBidExt() throws JsonProcessingException {
    +        // given
    +        final ObjectNode bidExt = mapper.createObjectNode();
    +        bidExt.put("startdelay", "2");
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder().build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder
    +                                .id("bidId")
    +                                .adm("")
    +                                .ext(mapper.createObjectNode().set("startdelay", mapper.createObjectNode())))));
    +
    +        // when
    +        final Result> result = amxBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .satisfies(error -> {
    +                    assertThat(error).extracting(BidderError::getType).containsExactly(BidderError.Type.bad_input);
    +                    assertThat(error).extracting(BidderError::getMessage)
    +                            .element(0).asString().startsWith("Cannot deserialize instance");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfAdmIsNotPresent() throws JsonProcessingException {
    +        // given
    +        final ObjectNode bidExt = mapper.createObjectNode();
    +        bidExt.put("startdelay", "2");
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder().build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder
    +                                .id("bidId")
    +                                .ext(bidExt))));
    +
    +        // when
    +        final Result> result = amxBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badServerResponse("Adm should not be blank in bidder: bidId"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().id("banner_id").build()).ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpAmx.of("testTagId", "testAdUnitId")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    +
    diff --git a/src/test/java/org/prebid/server/bidder/applogy/ApplogyBidderTest.java b/src/test/java/org/prebid/server/bidder/applogy/ApplogyBidderTest.java
    index 7c78749f48e..19bebdc2361 100644
    --- a/src/test/java/org/prebid/server/bidder/applogy/ApplogyBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/applogy/ApplogyBidderTest.java
    @@ -29,7 +29,6 @@
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -358,25 +357,6 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
         }
     
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = applogyBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(applogyBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
    @@ -400,6 +380,7 @@ private static Imp givenImp(Function impCustomiz
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
                     .build();
    diff --git a/src/test/java/org/prebid/server/bidder/appnexus/AppnexusAdapterTest.java b/src/test/java/org/prebid/server/bidder/appnexus/AppnexusAdapterTest.java
    deleted file mode 100644
    index e56e9028a95..00000000000
    --- a/src/test/java/org/prebid/server/bidder/appnexus/AppnexusAdapterTest.java
    +++ /dev/null
    @@ -1,731 +0,0 @@
    -package org.prebid.server.bidder.appnexus;
    -
    -import com.fasterxml.jackson.databind.node.ObjectNode;
    -import com.fasterxml.jackson.databind.node.TextNode;
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.Banner;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.Device;
    -import com.iab.openrtb.request.Format;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Regs;
    -import com.iab.openrtb.request.Site;
    -import com.iab.openrtb.request.Source;
    -import com.iab.openrtb.request.User;
    -import com.iab.openrtb.response.Bid;
    -import com.iab.openrtb.response.BidResponse;
    -import com.iab.openrtb.response.SeatBid;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.auction.model.AdapterRequest;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    -import org.prebid.server.bidder.appnexus.proto.AppnexusBidExt;
    -import org.prebid.server.bidder.appnexus.proto.AppnexusBidExtAppnexus;
    -import org.prebid.server.bidder.appnexus.proto.AppnexusImpExt;
    -import org.prebid.server.bidder.appnexus.proto.AppnexusImpExtAppnexus;
    -import org.prebid.server.bidder.appnexus.proto.AppnexusKeyVal;
    -import org.prebid.server.bidder.appnexus.proto.AppnexusParams;
    -import org.prebid.server.bidder.model.AdapterHttpRequest;
    -import org.prebid.server.bidder.model.ExchangeCall;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.request.PreBidRequest;
    -import org.prebid.server.proto.request.Video;
    -import org.prebid.server.proto.response.BidderDebug;
    -import org.prebid.server.proto.response.MediaType;
    -
    -import java.math.BigDecimal;
    -import java.util.EnumSet;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.function.Function;
    -import java.util.stream.Collectors;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptySet;
    -import static java.util.Collections.singleton;
    -import static java.util.Collections.singletonList;
    -import static java.util.function.Function.identity;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
    -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    -import static org.assertj.core.api.Assertions.assertThatNullPointerException;
    -import static org.assertj.core.api.Assertions.assertThatThrownBy;
    -import static org.assertj.core.api.Assertions.from;
    -import static org.assertj.core.api.Assertions.tuple;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -
    -public class AppnexusAdapterTest extends VertxTest {
    -
    -    private static final String BIDDER = "appnexus";
    -    private static final String COOKIE_FAMILY = "adnxs";
    -    private static final String ENDPOINT_URL = "http://endpoint.org/";
    -    private static final Integer BANNER_TYPE = 0;
    -    private static final Integer VIDEO_TYPE = 1;
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private UidsCookie uidsCookie;
    -
    -    private AdapterRequest adapterRequest;
    -    private PreBidRequestContext preBidRequestContext;
    -    private ExchangeCall exchangeCall;
    -    private AppnexusAdapter adapter;
    -
    -    @Before
    -    public void setUp() {
    -        adapterRequest = givenBidder(identity(), identity());
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -        adapter = new AppnexusAdapter(COOKIE_FAMILY, ENDPOINT_URL, jacksonMapper);
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnNullArguments() {
    -        assertThatNullPointerException().isThrownBy(() -> new AppnexusAdapter(null, null, jacksonMapper));
    -        assertThatNullPointerException().isThrownBy(() -> new AppnexusAdapter(COOKIE_FAMILY, null, jacksonMapper));
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException()
    -                .isThrownBy(() -> new AppnexusAdapter(COOKIE_FAMILY, "invalid_url", jacksonMapper))
    -                .withMessage("URL supplied is not valid: invalid_url");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedHeaders() {
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).flatExtracting(r -> r.getHeaders().entries())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple("Content-Type", "application/json;charset=utf-8"),
    -                        tuple("Accept", "application/json"));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedEndpointUrl() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(identity(), params -> params.invCode("invCode1").member("member1"))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getUri).containsOnly("http://endpoint.org/?member_id=member1");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfParamsMissingInAtLeastOneAdUnitBid() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(identity(), identity()),
    -                givenAdUnitBid(builder -> builder.params(null), identity())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Appnexus params section is missing");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamsCouldNotBeParsed() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("placement_id", new TextNode("non-integer"));
    -        adapterRequest = givenBidder(builder -> builder.params(params), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessageStartingWith("Cannot deserialize value of type");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfPlacementOrMemberWithInvcodeMissingInAdUnitBidParams() {
    -        // given
    -        adapterRequest = givenBidder(identity(), builder -> builder.placementId(null));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("No placement or member+invcode provided");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                                .adUnitCode("adUnitCode1")
    -                                .mediaTypes(emptySet()),
    -                        identity())));
    -
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("openRTB bids need at least one Imp");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsVideoAndMimesListIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                                .adUnitCode("adUnitCode1")
    -                                .mediaTypes(singleton(MediaType.video))
    -                                .video(Video.builder()
    -                                        .mimes(emptyList())
    -                                        .build()),
    -                        identity())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid AdUnit: VIDEO media type with no video data");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder
    -                        .bidderCode(BIDDER)
    -                        .adUnitCode("adUnitCode1")
    -                        .instl(1)
    -                        .topframe(1)
    -                        .sizes(singletonList(Format.builder().w(300).h(250).build())),
    -                appnexusParamsBuilder -> appnexusParamsBuilder
    -                        .keywords(singletonList(AppnexusKeyVal.of("k1", singletonList("v1"))))
    -                        .trafficSourceCode(""));
    -
    -        preBidRequestContext = givenPreBidRequestContext(
    -                builder -> builder
    -                        .referer("http://www.example.com")
    -                        .domain("example.com")
    -                        .ip("192.168.144.1")
    -                        .ua("userAgent"),
    -                builder -> builder
    -                        .tid("tid1")
    -                        .user(User.builder()
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null)))
    -                        .timeoutMillis(1500L)
    -                        .device(Device.builder()
    -                                .pxratio(new BigDecimal("4.2"))
    -                                .build()));
    -
    -        given(uidsCookie.uidFrom(eq(COOKIE_FAMILY))).willReturn("buyerUid");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .containsOnly(BidRequest.builder()
    -                        .id("tid1")
    -                        .at(1)
    -                        .tmax(1500L)
    -                        .imp(singletonList(Imp.builder()
    -                                .id("adUnitCode1")
    -                                .instl(1)
    -                                .tagid("30011")
    -                                .banner(Banner.builder()
    -                                        .w(300)
    -                                        .h(250)
    -                                        .topframe(1)
    -                                        .format(singletonList(Format.builder()
    -                                                .w(300)
    -                                                .h(250)
    -                                                .build()))
    -                                        .build())
    -                                .ext(mapper.valueToTree(AppnexusImpExt.of(
    -                                        AppnexusImpExtAppnexus.of(9848285, "k1=v1", "", null, null))))
    -                                .build()))
    -                        .site(Site.builder()
    -                                .domain("example.com")
    -                                .page("http://www.example.com")
    -                                .build())
    -                        .device(Device.builder()
    -                                .ua("userAgent")
    -                                .ip("192.168.144.1")
    -                                .pxratio(new BigDecimal("4.2"))
    -                                .build())
    -                        .user(User.builder()
    -                                .buyeruid("buyerUid")
    -                                .id("buyerUid")
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null)))
    -                        .source(Source.builder()
    -                                .fd(1)
    -                                .tid("tid1")
    -                                .build())
    -                        .build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithAppFromPreBidRequest() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder
    -                .app(App.builder().id("appId").build()).user(User.builder().build()));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(r -> r.getPayload().getApp().getId())
    -                .containsOnly("appId");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithUserFromPreBidRequestIfAppPresent() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder
    -                .app(App.builder().build())
    -                .user(User.builder().buyeruid("buyerUid").build()));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUidFromCookie");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(r -> r.getPayload().getUser())
    -                .containsOnly(User.builder().buyeruid("buyerUid").build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithOneRequestWithOneImpIfAdUnitContainsBannerAndVideoMediaTypes() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                                .mediaTypes(EnumSet.of(MediaType.video, MediaType.banner))
    -                                .video(Video.builder()
    -                                        .mimes(singletonList("Mime"))
    -                                        .playbackMethod(1)
    -                                        .build()),
    -                        identity())));
    -
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp())
    -                .containsOnly(
    -                        Imp.builder()
    -                                .banner(Banner.builder().w(300).h(250).format(
    -                                        singletonList(Format.builder().w(300).h(250).build())).build())
    -                                .video(com.iab.openrtb.request.Video.builder().w(300).h(250).mimes(
    -                                        singletonList("Mime")).playbackmethod(singletonList(1)).build())
    -                                .tagid("30011")
    -                                .ext(mapper.valueToTree(AppnexusImpExt.of(
    -                                        AppnexusImpExtAppnexus.of(9848285, null, null, null, null))))
    -                                .build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithOneRequestIfMultipleAdUnitsInPreBidRequest() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1"), identity()),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"), identity())));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(2)
    -                .extracting(Imp::getId).containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldHonorLegacyParams() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(identity(), params -> params
    -                        .placementId(null)
    -                        .legacyPlacementId(101)
    -                        .invCode(null)
    -                        .legacyInvCode("legacyInvCode1")
    -                        .trafficSourceCode(null)
    -                        .legacyTrafficSourceCode("legacyTrafficSourceCode1"))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(1)
    -                .extracting(Imp::getTagid, Imp::getExt).containsOnly(
    -                tuple(
    -                        "legacyInvCode1",
    -                        mapper.valueToTree(AppnexusImpExt.of(
    -                                AppnexusImpExtAppnexus.of(101, null, "legacyTrafficSourceCode1", null, null)))));
    -    }
    -
    -    @Test
    -    public void extractBidsShouldFailIfBidImpIdDoesNotMatchAdUnitCode() {
    -        // given
    -        adapterRequest = givenBidder(builder -> builder.adUnitCode("adUnitCode"), identity());
    -
    -        exchangeCall = givenExchangeCall(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("anotherAdUnitCode").build()))
    -                        .build())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Unknown ad unit code 'anotherAdUnitCode'");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .price(new BigDecimal("8.43"))
    -                                        .adm("adm")
    -                                        .crid("crid")
    -                                        .w(300)
    -                                        .h(250)
    -                                        .dealid("dealId")
    -                                        .ext(mapper.valueToTree(AppnexusBidExt.of(
    -                                                AppnexusBidExtAppnexus.builder().bidAdType(BANNER_TYPE).build())))
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids)
    -                .containsExactly(org.prebid.server.proto.response.Bid.builder()
    -                        .code("adUnitCode")
    -                        .price(new BigDecimal("8.43"))
    -                        .adm("adm")
    -                        .creativeId("crid")
    -                        .width(300)
    -                        .height(250)
    -                        .dealId("dealId")
    -                        .bidder(BIDDER)
    -                        .bidId("bidId")
    -                        .mediaType(MediaType.banner)
    -                        .build());
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithBidMediaTypeAsVideo() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .ext(mapper.valueToTree(AppnexusBidExt.of(
    -                                                AppnexusBidExtAppnexus.builder().bidAdType(VIDEO_TYPE).build())))
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).element(0)
    -                .returns(MediaType.video, from(org.prebid.server.proto.response.Bid::getMediaType));
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithBidMediaTypeAsBanner() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .ext(mapper.valueToTree(AppnexusBidExt.of(
    -                                                AppnexusBidExtAppnexus.builder().bidAdType(BANNER_TYPE).build())))
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).element(0)
    -                .returns(MediaType.banner, from(org.prebid.server.proto.response.Bid::getMediaType));
    -    }
    -
    -    @Test
    -    public void extractBidsShouldThrowExceptionWhenBidExtNotDefined() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .ext(null)
    -                                        .build()))
    -                                .build())));
    -
    -        // when and then
    -        assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall))
    -                .withMessage("bidResponse.bid.ext should be defined for appnexus");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldThrowExceptionWhenBidExtAppnexusNotDefined() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .ext(mapper.createObjectNode())
    -                                        .build()))
    -                                .build())));
    -
    -        // when and then
    -        assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall))
    -                .withMessage("bidResponse.bid.ext.appnexus should be defined");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldThrowExceptionWhenBidExtAppnexusBidTypeNotDefined() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .ext(mapper.valueToTree(AppnexusBidExt.of(
    -                                                AppnexusBidExtAppnexus.builder().build())))
    -                                        .build()))
    -                                .build())));
    -
    -        // when and then
    -        assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall))
    -                .withMessage("bidResponse.bid.ext.appnexus.bid_ad_type should be defined");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldThrowExceptionWhenBidExtAppnexusBidTypeUnrecognized() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .ext(mapper.valueToTree(AppnexusBidExt.of(
    -                                                AppnexusBidExtAppnexus.builder().bidAdType(42).build())))
    -                                        .build()))
    -                                .build())));
    -
    -        // when and then
    -        assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall))
    -                .withMessage("Unrecognized bid_ad_type in response from appnexus: 42");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnEmptyBidsIfEmptyOrNullBidResponse() {
    -        // given
    -        adapterRequest = givenBidder(identity(), identity());
    -
    -        exchangeCall = givenExchangeCall(identity(), br -> br.seatbid(null));
    -
    -        // when and then
    -        assertThat(adapter.extractBids(adapterRequest, exchangeCall)).isEmpty();
    -        assertThat(adapter.extractBids(adapterRequest, ExchangeCall.empty(null))).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnMultipleBidBuildersIfMultipleAdUnitsInPreBidRequestAndBidsInResponse() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1"), identity()),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"), identity())));
    -
    -        exchangeCall = givenExchangeCall(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(asList(
    -                                        Bid.builder()
    -                                                .impid("adUnitCode1")
    -                                                .ext(mapper.valueToTree(AppnexusBidExt.of(
    -                                                        AppnexusBidExtAppnexus.builder()
    -                                                                .bidAdType(BANNER_TYPE)
    -                                                                .build())))
    -                                                .build(),
    -                                        Bid.builder()
    -                                                .impid("adUnitCode2")
    -                                                .ext(mapper.valueToTree(AppnexusBidExt.of(
    -                                                        AppnexusBidExtAppnexus.builder()
    -                                                                .bidAdType(BANNER_TYPE)
    -                                                                .build())))
    -                                                .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(2)
    -                .extracting(org.prebid.server.proto.response.Bid::getCode)
    -                .containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    private static AdapterRequest givenBidder(
    -            Function adUnitBidBuilderCustomizer,
    -            Function
    -                    paramsBuilderCustomizer) {
    -
    -        return AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(adUnitBidBuilderCustomizer, paramsBuilderCustomizer)));
    -    }
    -
    -    private static AdUnitBid givenAdUnitBid(
    -            Function adUnitBidBuilderCustomizer,
    -            Function
    -                    paramsBuilderCustomizer) {
    -
    -        // params
    -        final AppnexusParams.AppnexusParamsBuilder paramsBuilder = AppnexusParams.builder()
    -                .placementId(9848285)
    -                .invCode("30011");
    -        final AppnexusParams.AppnexusParamsBuilder paramsBuilderCustomized = paramsBuilderCustomizer
    -                .apply(paramsBuilder);
    -        final AppnexusParams params = paramsBuilderCustomized.build();
    -
    -        // ad unit bid
    -        final AdUnitBid.AdUnitBidBuilder adUnitBidBuilderMinimal = AdUnitBid.builder()
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .params(mapper.valueToTree(params))
    -                .mediaTypes(singleton(MediaType.banner));
    -        final AdUnitBid.AdUnitBidBuilder adUnitBidBuilderCustomized = adUnitBidBuilderCustomizer.apply(
    -                adUnitBidBuilderMinimal);
    -
    -        return adUnitBidBuilderCustomized.build();
    -    }
    -
    -    private PreBidRequestContext givenPreBidRequestContext(
    -            Function preBidRequestContextBuilderCustomizer,
    -            Function
    -                    preBidRequestBuilderCustomizer) {
    -
    -        final PreBidRequest.PreBidRequestBuilder preBidRequestBuilderMinimal = PreBidRequest.builder()
    -                .accountId("accountId");
    -        final PreBidRequest preBidRequest = preBidRequestBuilderCustomizer.apply(preBidRequestBuilderMinimal).build();
    -
    -        final PreBidRequestContext.PreBidRequestContextBuilder preBidRequestContextBuilderMinimal =
    -                PreBidRequestContext.builder()
    -                        .preBidRequest(preBidRequest)
    -                        .uidsCookie(uidsCookie);
    -        return preBidRequestContextBuilderCustomizer.apply(preBidRequestContextBuilderMinimal).build();
    -    }
    -
    -    private static ExchangeCall givenExchangeCall(
    -            Function bidRequestBuilderCustomizer,
    -            Function bidResponseBuilderCustomizer) {
    -
    -        final BidRequest.BidRequestBuilder bidRequestBuilderMinimal = BidRequest.builder();
    -        final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(bidRequestBuilderMinimal).build();
    -
    -        final BidResponse.BidResponseBuilder bidResponseBuilderMinimal = BidResponse.builder();
    -        final BidResponse bidResponse = bidResponseBuilderCustomizer.apply(bidResponseBuilderMinimal).build();
    -
    -        return ExchangeCall.success(bidRequest, bidResponse, BidderDebug.builder().build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/appnexus/AppnexusBidderTest.java b/src/test/java/org/prebid/server/bidder/appnexus/AppnexusBidderTest.java
    index cca21834b53..53217e24c59 100644
    --- a/src/test/java/org/prebid/server/bidder/appnexus/AppnexusBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/appnexus/AppnexusBidderTest.java
    @@ -21,6 +21,7 @@
     import org.junit.Before;
     import org.junit.Test;
     import org.prebid.server.VertxTest;
    +import org.prebid.server.auction.model.Endpoint;
     import org.prebid.server.bidder.appnexus.proto.AppnexusBidExt;
     import org.prebid.server.bidder.appnexus.proto.AppnexusBidExtAppnexus;
     import org.prebid.server.bidder.appnexus.proto.AppnexusImpExt;
    @@ -41,6 +42,7 @@
     import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidPbs;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
     import org.prebid.server.proto.openrtb.ext.request.ExtUser;
     import org.prebid.server.proto.openrtb.ext.request.appnexus.ExtImpAppnexus;
    @@ -51,8 +53,11 @@
     import java.math.BigDecimal;
     import java.util.Collection;
     import java.util.Collections;
    +import java.util.HashSet;
     import java.util.List;
    +import java.util.Objects;
     import java.util.function.UnaryOperator;
    +import java.util.regex.Pattern;
     import java.util.stream.Collectors;
     import java.util.stream.IntStream;
     
    @@ -295,15 +300,15 @@ public void makeHttpRequestsShouldSetRequestExtAppnexusTrueWhenPrimaryAdserverIs
                     .extracting(BidRequest::getExt).isNotNull()
                     .extracting(ext -> mapper.convertValue(ext.getProperties(), AppnexusReqExt.class))
                     .extracting(AppnexusReqExt::getAppnexus)
    -                .containsOnly(AppnexusReqExtAppnexus.of(true, true));
    +                .containsOnly(AppnexusReqExtAppnexus.of(true, true, null));
         }
     
         @Test
    -    public void makeHttpRequestsShouldUpdateRequestExtAppnexusTrueWhenPrimaryAdserverIsNotZero() {
    +    public void makeHttpRequestsShouldUpdateRequestExtAppnexusTrueWhenPrimaryAdserverIsNotNull() {
             // given
             final ExtRequestPrebid requestPrebid = ExtRequestPrebid.builder()
                     .targeting(ExtRequestTargeting.builder()
    -                        .includebrandcategory(ExtIncludeBrandCategory.of(-120, null, null))
    +                        .includebrandcategory(ExtIncludeBrandCategory.of(null, null, null))
                             .build())
                     .build();
     
    @@ -323,15 +328,15 @@ public void makeHttpRequestsShouldUpdateRequestExtAppnexusTrueWhenPrimaryAdserve
                     .extracting(BidRequest::getExt).isNotNull()
                     .extracting(ext -> mapper.convertValue(ext.getProperties(), AppnexusReqExt.class))
                     .extracting(AppnexusReqExt::getAppnexus)
    -                .containsOnly(AppnexusReqExtAppnexus.of(true, true));
    +                .containsOnly(AppnexusReqExtAppnexus.of(true, true, null));
         }
     
         @Test
    -    public void makeHttpRequestsShouldNotUpdateRequestExtAppnexusWhenPrimaryAdserverIsZero() {
    +    public void makeHttpRequestsShouldNotUpdateRequestExtAppnexusWhenIncludeBrandCategoryIsNull() {
             // given
             final ExtRequestPrebid requestPrebid = ExtRequestPrebid.builder()
                     .targeting(ExtRequestTargeting.builder()
    -                        .includebrandcategory(ExtIncludeBrandCategory.of(0, null, null))
    +                        .includebrandcategory(null)
                             .build())
                     .build();
     
    @@ -339,7 +344,7 @@ public void makeHttpRequestsShouldNotUpdateRequestExtAppnexusWhenPrimaryAdserver
                     bidRequestBuilder -> bidRequestBuilder
                             .ext(jacksonMapper.fillExtension(
                                     ExtRequest.of(requestPrebid),
    -                                AppnexusReqExt.of(AppnexusReqExtAppnexus.of(false, true)))),
    +                                AppnexusReqExt.of(AppnexusReqExtAppnexus.of(false, true, null)))),
                     impBuilder -> impBuilder.banner(Banner.builder().build()),
                     extImpAppnexusBuilder -> extImpAppnexusBuilder.placementId(20));
     
    @@ -353,7 +358,7 @@ public void makeHttpRequestsShouldNotUpdateRequestExtAppnexusWhenPrimaryAdserver
                     .extracting(BidRequest::getExt).isNotNull()
                     .extracting(ext -> mapper.convertValue(ext.getProperties(), AppnexusReqExt.class))
                     .extracting(AppnexusReqExt::getAppnexus)
    -                .containsOnly(AppnexusReqExtAppnexus.of(false, true));
    +                .containsOnly(AppnexusReqExtAppnexus.of(false, true, null));
         }
     
         @Test
    @@ -367,7 +372,7 @@ public void makeHttpRequestsShouldNotUpdateRequestExtAppnexusWhenIncludeBrandCat
                     bidRequestBuilder -> bidRequestBuilder
                             .ext(jacksonMapper.fillExtension(
                                     ExtRequest.of(requestPrebid),
    -                                AppnexusReqExt.of(AppnexusReqExtAppnexus.of(false, true)))),
    +                                AppnexusReqExt.of(AppnexusReqExtAppnexus.of(false, true, null)))),
                     impBuilder -> impBuilder.banner(Banner.builder().build()),
                     extImpAppnexusBuilder -> extImpAppnexusBuilder.placementId(20));
     
    @@ -381,7 +386,7 @@ public void makeHttpRequestsShouldNotUpdateRequestExtAppnexusWhenIncludeBrandCat
                     .extracting(BidRequest::getExt).isNotNull()
                     .extracting(ext -> mapper.convertValue(ext.getProperties(), AppnexusReqExt.class))
                     .extracting(AppnexusReqExt::getAppnexus)
    -                .containsOnly(AppnexusReqExtAppnexus.of(false, true));
    +                .containsOnly(AppnexusReqExtAppnexus.of(false, true, null));
         }
     
         @Test
    @@ -482,6 +487,69 @@ public void makeHttpRequestsShouldSetImpTagidAndImpBidFloorIfExtImpAppnexusHasIn
                             .build()));
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldSetReserveIfImpBidFloorIsNotSet() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                identity(),
    +                extImpAppnexusBuilder -> extImpAppnexusBuilder
    +                        .placementId(20)
    +                        .reserve(BigDecimal.valueOf(123)));
    +
    +        // when
    +        final Result>> result = appnexusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor)
    +                .containsExactly(BigDecimal.valueOf(123));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetReserveIfImpBidFloorIsZero() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                impBuilder -> impBuilder.bidfloor(BigDecimal.ZERO),
    +                extImpAppnexusBuilder -> extImpAppnexusBuilder
    +                        .placementId(20)
    +                        .reserve(BigDecimal.valueOf(123)));
    +
    +        // when
    +        final Result>> result = appnexusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor)
    +                .containsExactly(BigDecimal.valueOf(123));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetReserveIfImpBidFloorIsNegative() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                impBuilder -> impBuilder.bidfloor(BigDecimal.ZERO.subtract(BigDecimal.ONE)),
    +                extImpAppnexusBuilder -> extImpAppnexusBuilder
    +                        .placementId(20)
    +                        .reserve(BigDecimal.valueOf(123)));
    +
    +        // when
    +        final Result>> result = appnexusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor)
    +                .containsExactly(BigDecimal.valueOf(123));
    +    }
    +
         @Test
         public void makeHttpRequestsShouldSetNativeIfRequestImpIsNative() {
             // given
    @@ -715,6 +783,194 @@ public void makeHttpRequestsShouldHonorLegacyParams() {
                                     AppnexusImpExtAppnexus.of(101, null, "legacyTrafficSourceCode1", null, null)))));
         }
     
    +    @Test
    +    public void makeHttpRequestShouldReturnSingleRequestWhenOnePod() {
    +        // given
    +        final List imps = IntStream.rangeClosed(0, 2)
    +                .mapToObj(impIdSuffix -> givenImp(
    +                        imp -> imp
    +                                .id(String.format("1_%d", impIdSuffix))
    +                                .banner(Banner.builder().build()),
    +                        ext -> ext.placementId(10).generateAdPodId(true)))
    +                .collect(Collectors.toList());
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(imps)
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .pbs(ExtRequestPrebidPbs.of(Endpoint.openrtb2_video.value()))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = appnexusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ext -> mapper.convertValue(ext.getProperties(), AppnexusReqExt.class))
    +                .extracting(AppnexusReqExt::getAppnexus).doesNotContainNull()
    +                .extracting(AppnexusReqExtAppnexus::getAdpodId).doesNotContainNull()
    +                .allMatch(adPodId -> Pattern.matches("\\d+", adPodId));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestShouldReturnMultipleRequestsWhenOnePodAndManyImps() {
    +        // given
    +        final List imps = IntStream.rangeClosed(0, 15)
    +                .mapToObj(impIdSuffix -> givenImp(
    +                        imp -> imp
    +                                .id(String.format("1_%d", impIdSuffix))
    +                                .banner(Banner.builder().build()),
    +                        ext -> ext.placementId(10).generateAdPodId(true)))
    +                .collect(Collectors.toList());
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(imps)
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .pbs(ExtRequestPrebidPbs.of(Endpoint.openrtb2_video.value()))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = appnexusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(2)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ext -> mapper.convertValue(ext.getProperties(), AppnexusReqExt.class))
    +                .extracting(AppnexusReqExt::getAppnexus).doesNotContainNull()
    +                .extracting(AppnexusReqExtAppnexus::getAdpodId).doesNotContainNull()
    +                .matches(adPodIds -> new HashSet<>(adPodIds).size() == 1); // adPodIds should be the same
    +    }
    +
    +    @Test
    +    public void makeHttpRequestShouldReturnMultipleRequestsWhenTwoPods() {
    +        // given
    +        final List imps = IntStream.rangeClosed(1, 2)
    +                .boxed()
    +                .flatMap(impIdPrefix -> IntStream.rangeClosed(0, 2)
    +                        .mapToObj(impIdSuffix -> givenImp(
    +                                imp -> imp
    +                                        .id(String.format("%d_%d", impIdPrefix, impIdSuffix))
    +                                        .banner(Banner.builder().build()),
    +                                ext -> ext.placementId(10).generateAdPodId(true))))
    +                .collect(Collectors.toList());
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(imps)
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .pbs(ExtRequestPrebidPbs.of(Endpoint.openrtb2_video.value()))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = appnexusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(2)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ext -> mapper.convertValue(ext.getProperties(), AppnexusReqExt.class))
    +                .extracting(AppnexusReqExt::getAppnexus).doesNotContainNull()
    +                .extracting(AppnexusReqExtAppnexus::getAdpodId).doesNotContainNull()
    +                .matches(adPodIds -> new HashSet<>(adPodIds).size() == 2); // adPodIds should be different
    +    }
    +
    +    @Test
    +    public void makeHttpRequestShouldReturnMultipleRequestsWhenTwoPodsAndManyImps() {
    +        // given
    +        final List imps = IntStream.rangeClosed(1, 2)
    +                .boxed()
    +                .flatMap(impIdPrefix -> IntStream.rangeClosed(0, 15)
    +                        .mapToObj(impIdSuffix -> givenImp(
    +                                imp -> imp
    +                                        .id(String.format("%d_%d", impIdPrefix, impIdSuffix))
    +                                        .banner(Banner.builder().build()),
    +                                ext -> ext.placementId(10).generateAdPodId(true))))
    +                .collect(Collectors.toList());
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(imps)
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .pbs(ExtRequestPrebidPbs.of(Endpoint.openrtb2_video.value()))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = appnexusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(4)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getExt).isNotNull()
    +                .extracting(ext -> mapper.convertValue(ext.getProperties(), AppnexusReqExt.class))
    +                .extracting(AppnexusReqExt::getAppnexus).doesNotContainNull()
    +                .extracting(AppnexusReqExtAppnexus::getAdpodId).doesNotContainNull()
    +                .matches(adPodIds -> new HashSet<>(adPodIds).size() == 2); // adPodIds should be different
    +    }
    +
    +    @Test
    +    public void makeHttpRequestShouldReturnErrorIfRequestContainsMultipleGenerateAdPodIdsValues() {
    +        // given
    +        final List imps = IntStream.rangeClosed(1, 2)
    +                .boxed()
    +                .flatMap(impIdPrefix -> IntStream.rangeClosed(0, 15)
    +                        .mapToObj(impIdSuffix -> givenImp(
    +                                imp -> imp
    +                                        .id(String.format("%d_%d", impIdPrefix, impIdSuffix))
    +                                        .banner(Banner.builder().build()),
    +                                ext -> ext.placementId(10).generateAdPodId(impIdSuffix % 2 == 0))))
    +                .collect(Collectors.toList());
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(imps)
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .pbs(ExtRequestPrebidPbs.of(Endpoint.openrtb2_video.value()))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = appnexusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Generate ad pod option should be same for all pods in request"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestShouldNotGenerateAdPodIdWhenFlagIsNotSetInRequestImpExt() {
    +        // given
    +        final List imps = IntStream.rangeClosed(1, 2)
    +                .boxed()
    +                .flatMap(impIdPrefix -> IntStream.rangeClosed(0, 15)
    +                        .mapToObj(impIdSuffix -> givenImp(
    +                                imp -> imp
    +                                        .id(String.format("%d_%d", impIdPrefix, impIdSuffix))
    +                                        .banner(Banner.builder().build()),
    +                                ext -> ext.placementId(10).generateAdPodId(false))))
    +                .collect(Collectors.toList());
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(imps)
    +                .ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .targeting(ExtRequestTargeting.builder()
    +                                .includebrandcategory(ExtIncludeBrandCategory.of(null, null, null))
    +                                .build())
    +                        .pbs(ExtRequestPrebidPbs.of(Endpoint.openrtb2_video.value()))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = appnexusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getExt)
    +                .extracting(ext -> mapper.convertValue(ext.getProperties(), AppnexusReqExt.class))
    +                .extracting(AppnexusReqExt::getAppnexus).doesNotContainNull()
    +                .extracting(AppnexusReqExtAppnexus::getAdpodId)
    +                .matches(adPodIds -> adPodIds.stream().allMatch(Objects::isNull));
    +    }
    +
         @Test
         public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             // given
    @@ -948,7 +1204,7 @@ private static BidRequest givenBidRequest(UnaryOperator bidRe
                                                   UnaryOperator impCustomizer,
                                                   UnaryOperator extCustomizer) {
             return bidRequestCustomizer.apply(BidRequest.builder()
    -                .imp(singletonList(givenImp(impCustomizer, extCustomizer))))
    +                        .imp(singletonList(givenImp(impCustomizer, extCustomizer))))
                     .build();
         }
     
    @@ -959,7 +1215,7 @@ private static BidRequest givenBidRequest(UnaryOperator impCustomize
         private static Imp givenImp(UnaryOperator impCustomizer,
                                     UnaryOperator extCustomizer) {
             return impCustomizer.apply(Imp.builder()
    -                .ext(givenExt(extCustomizer)))
    +                        .ext(givenExt(extCustomizer)))
                     .build();
         }
     
    @@ -993,15 +1249,16 @@ private static String givenBidResponse(
                 UnaryOperator bidExtCustomizer)
                 throws JsonProcessingException {
     
    -        return mapper.writeValueAsString(bidResponseCustomizer.apply(BidResponse.builder()
    -                .seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(bidCustomizer.apply(Bid.builder()
    -                                .impid("impId")
    -                                .ext(mapper.valueToTree(AppnexusBidExt.of(
    -                                        bidExtCustomizer.apply(AppnexusBidExtAppnexus.builder().bidAdType(BANNER_TYPE))
    -                                                .build()))))
    -                                .build()))
    -                        .build())))
    +        return mapper.writeValueAsString(bidResponseCustomizer.apply(
    +                        BidResponse.builder()
    +                                .seatbid(singletonList(SeatBid.builder()
    +                                        .bid(singletonList(bidCustomizer.apply(Bid.builder()
    +                                                .impid("impId")
    +                                                .ext(mapper.valueToTree(AppnexusBidExt.of(bidExtCustomizer.apply(
    +                                                                AppnexusBidExtAppnexus.builder()
    +                                                                        .bidAdType(BANNER_TYPE))
    +                                                        .build())))).build()))
    +                                        .build())))
                     .build());
         }
     
    diff --git a/src/test/java/org/prebid/server/bidder/avocet/AvocetBidderTest.java b/src/test/java/org/prebid/server/bidder/avocet/AvocetBidderTest.java
    index 39a339844c7..3de121a0b11 100644
    --- a/src/test/java/org/prebid/server/bidder/avocet/AvocetBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/avocet/AvocetBidderTest.java
    @@ -24,7 +24,6 @@
     import java.util.List;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    @@ -201,11 +200,6 @@ public void makeBidsShouldReturnVideoBidIfDurationIsNotZero() throws JsonProcess
                             null));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(avocetBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
                     .seatbid(singletonList(SeatBid.builder()
    diff --git a/src/test/java/org/prebid/server/bidder/axonix/AxonixBidderTest.java b/src/test/java/org/prebid/server/bidder/axonix/AxonixBidderTest.java
    new file mode 100644
    index 00000000000..6ec80b06c91
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/axonix/AxonixBidderTest.java
    @@ -0,0 +1,241 @@
    +package org.prebid.server.bidder.axonix;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Native;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.between.BetweenBidder;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.axonix.ExtImpAxonix;
    +import org.prebid.server.proto.openrtb.ext.request.between.ExtImpBetween;
    +
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class AxonixBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://randomurl.com/{{SupplyId}}";
    +
    +    private AxonixBidder axonixBidder;
    +
    +    @Before
    +    public void setUp() {
    +        axonixBidder = new AxonixBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new BetweenBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(500).build())).build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAxonix.of("someSupplyId")))));
    +
    +        // when
    +        final Result>> result = axonixBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://randomurl.com/someSupplyId");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = axonixBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Imp.ext could not be parsed"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = axonixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = axonixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = axonixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                .build();
    +
    +        final HttpCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(
    +                givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = axonixBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                .build();
    +
    +        final HttpCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(
    +                givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = axonixBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBidHasNoCorrespondingImp() throws JsonProcessingException {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().id("123").build()))
    +                .build();
    +
    +        final HttpCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(
    +                givenBidResponse(bidBuilder -> bidBuilder.impid("33"))));
    +
    +        // when
    +        final Result> result = axonixBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("33").build(), banner, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerAndVideoAbsentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().id("123").build()))
    +                .build();
    +
    +        final HttpCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(
    +                givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = axonixBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of("127.0.0.1", "pubId")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/beachfront/BeachfrontBidderTest.java b/src/test/java/org/prebid/server/bidder/beachfront/BeachfrontBidderTest.java
    index 6bc0a5f33c4..80412cdb315 100644
    --- a/src/test/java/org/prebid/server/bidder/beachfront/BeachfrontBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/beachfront/BeachfrontBidderTest.java
    @@ -7,6 +7,7 @@
     import com.iab.openrtb.request.Device;
     import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Source;
     import com.iab.openrtb.request.User;
     import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
    @@ -27,6 +28,9 @@
     import org.prebid.server.bidder.model.HttpResponse;
     import org.prebid.server.bidder.model.Result;
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchainNode;
    +import org.prebid.server.proto.openrtb.ext.request.ExtSource;
     import org.prebid.server.proto.openrtb.ext.request.beachfront.ExtImpBeachfront;
     import org.prebid.server.proto.openrtb.ext.request.beachfront.ExtImpBeachfrontAppIds;
     import org.prebid.server.proto.openrtb.ext.response.BidType;
    @@ -38,7 +42,6 @@
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -125,7 +128,9 @@ public void makeHttpRequestsShouldReturnOneBannerOneAdmAndOneNurlRequestsAndSkip
                             givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
                                     mapper.valueToTree(ExtImpBeachfront.of("appId", null, BigDecimal.ONE, "nurl")))))),
                             givenImp(impBuilder -> impBuilder
    -                                .banner(Banner.builder().w(100).h(200).build())
    +                                .banner(Banner.builder()
    +                                        .format(singletonList(Format.builder().w(100).h(200).build()))
    +                                        .build())
                                     .video(null)),
                             givenImp(impBuilder -> impBuilder
                                     .banner(Banner.builder()
    @@ -155,6 +160,47 @@ public void makeHttpRequestsShouldReturnOneBannerOneAdmAndOneNurlRequestsAndSkip
                             VIDEO_ENDPOINT + "appId&prebidserver");
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldUseAdmAsDefaultResponseTypeForVideo() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .imp(singletonList(
    +                        givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                mapper.valueToTree(ExtImpBeachfront.of("appId", null, BigDecimal.ONE, null))))))))
    +                .build();
    +
    +        // when
    +        final Result>> result = beachfrontBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +
    +        final List> httpRequests = result.getValue();
    +        assertThat(httpRequests).hasSize(1)
    +                .extracting(HttpRequest::getUri)
    +                .containsOnly(VIDEO_ENDPOINT + "appId");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldFilterBannerImpWithoutFormat() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .imp(singletonList(
    +                        givenImp(impBuilder -> impBuilder
    +                                .banner(Banner.builder().w(200).h(400).build())
    +                                .video(null))))
    +                .build();
    +
    +        // when
    +        final Result>> result = beachfrontBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsOnly(BidderError.badInput("no valid impressions were found in the request"));
    +    }
    +
         @Test
         public void makeHttpRequestsShouldReturnRequestWithExpectedBasicHeadersAndAdditionalDeviceHeaders() {
             // given
    @@ -197,6 +243,35 @@ public void makeHttpRequestsShouldAdditionalCookieHeaderForVideoRequestWhenBuyer
                             tuple(HttpUtil.COOKIE_HEADER.toString(), "__io_cid=4125"));
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldReturnExpectedBannerRequestWithSchain() {
    +        // given
    +        final ExtRequestPrebidSchainSchainNode globalNode = ExtRequestPrebidSchainSchainNode.of(
    +                "pbshostcompany.com", "00001", null, null, null, null, null);
    +        final ExtRequestPrebidSchainSchain expectedSchain = ExtRequestPrebidSchainSchain.of(
    +                null, null, singletonList(globalNode), null);
    +
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .video(null)
    +                        .banner(Banner.builder()
    +                                .format(singletonList(Format.builder().w(100).h(300).build()))
    +                                .build()),
    +                requestBuilder -> requestBuilder
    +                        .source(Source.builder().ext(ExtSource.of(expectedSchain)).build())
    +        );
    +
    +        // when
    +        final Result>> result = beachfrontBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BeachfrontBannerRequest.class))
    +                .extracting(BeachfrontBannerRequest::getSchain)
    +                .containsExactly(expectedSchain);
    +    }
    +
         @Test
         public void makeHttpRequestsShouldReturnExpectedBannerRequest() {
             // given
    @@ -226,10 +301,10 @@ public void makeHttpRequestsShouldReturnExpectedBannerRequest() {
                             .deviceOs("nokia")
                             .isMobile(1)
                             .user(User.builder().id("userId").buyeruid("buid").build())
    -                        .adapterVersion("0.9.0")
    +                        .adapterVersion("0.9.2")
                             .adapterName("BF_PREBID_S2S")
    -                        .ip("192.168.255.255")
                             .requestId("153")
    +                        .real204(true)
                             .build());
         }
     
    @@ -237,7 +312,8 @@ public void makeHttpRequestsShouldReturnExpectedBannerRequest() {
         public void makeHttpRequestsShouldReturnExpectedAdmAndNurlVideoRequests() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .device(Device.builder().ip("127.0.0.1").build())
    +                .app(App.builder().bundle("prefix_test1.test2.test3_suffix").build())
    +                .device(Device.builder().build())
                     .imp(asList(
                             givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
                                     mapper.valueToTree(ExtImpBeachfront.of("appId2", null, BigDecimal.TEN, "nurl")))))),
    @@ -250,10 +326,6 @@ public void makeHttpRequestsShouldReturnExpectedAdmAndNurlVideoRequests() {
             // then
             assertThat(result.getErrors()).isEmpty();
     
    -        final BidRequest.BidRequestBuilder expectedRequestBuilder = BidRequest.builder()
    -                .device(Device.builder().ip("192.168.255.255").devicetype(1).build())
    -                .cur(singletonList("USD"));
    -
             final Imp.ImpBuilder expectedImpBuilder = Imp.builder()
                     .video(Video.builder().w(300).h(250).build())
                     .secure(0);
    @@ -264,33 +336,121 @@ public void makeHttpRequestsShouldReturnExpectedAdmAndNurlVideoRequests() {
                                     .isPrebid(true)
                                     .appId("appId2")
                                     .videoResponseType("nurl")
    -                                .request(expectedRequestBuilder
    +                                .request(BidRequest.builder()
    +                                        .device(Device.builder().devicetype(1).build())
    +                                        .app(App.builder().bundle("prefix_test1.test2.test3_suffix")
    +                                                .domain("test2.prefix_test1").build())
                                             .imp(singletonList(expectedImpBuilder.id("123")
                                                     .bidfloor(BigDecimal.TEN).build()))
    +                                        .cur(singletonList("USD"))
                                             .build())
                                     .build(),
                             BeachfrontVideoRequest.builder()
                                     .appId("appId")
                                     .videoResponseType("adm")
    -                                .request(expectedRequestBuilder
    +                                .request(BidRequest.builder()
    +                                        .device(Device.builder().ip("255.255.255.255").devicetype(1).build())
    +                                        .app(App.builder().bundle("prefix_test1.test2.test3_suffix")
    +                                                .domain("test2.prefix_test1").build())
                                             .imp(singletonList(expectedImpBuilder.id("234")
                                                     .bidfloor(BigDecimal.ONE).build()))
    +                                        .cur(singletonList("USD"))
                                             .build())
                                     .build());
         }
     
         @Test
    -    public void makeBidsShouldReturnErrorWhenResponseBodyIsEmpty() {
    +    public void makeHttpRequestsShouldCreateAdmRequestForEveryUnknownResponseType() {
             // given
    -        final HttpCall httpCall = givenHttpCall(null, null);
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(
    +                        givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                mapper.valueToTree(ExtImpBeachfront.of("appId2", null, null, "unknownType"))))))
    +                ))
    +                .build();
     
             // when
    -        final Result> result = beachfrontBidder.makeBids(httpCall, null);
    +        final Result>> result = beachfrontBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getValue()).isEmpty();
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badServerResponse("Received a null response from beachfront"));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BeachfrontVideoRequest.class))
    +                .extracting(BeachfrontVideoRequest::getVideoResponseType)
    +                .containsExactly("adm");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnExpectedBidFloorFromBidRequest() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .video(Video.builder().w(1).h(1).build())
    +                        .bidfloor(BigDecimal.ONE)
    +                        .secure(1));
    +
    +        // when
    +        final Result>> result = beachfrontBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BeachfrontVideoRequest.class))
    +                .extracting(BeachfrontVideoRequest::getRequest)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor)
    +                .containsExactly(BigDecimal.ONE);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUseDefaultBidFloorIfNoInRequest() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpBeachfront.of("appId",
    +                                        ExtImpBeachfrontAppIds.of("videoIds", "bannerIds"),
    +                                        null, "adm"))))
    +                        .video(Video.builder().w(1).h(1).build())
    +                        .secure(1));
    +
    +        // when
    +        final Result>> result = beachfrontBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BeachfrontVideoRequest.class))
    +                .extracting(BeachfrontVideoRequest::getRequest)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor)
    +                .containsExactly(BigDecimal.ZERO);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUseImpBidFloor() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpBeachfront.of("appId",
    +                                        ExtImpBeachfrontAppIds.of("videoIds", "bannerIds"),
    +                                        BigDecimal.TEN, "adm"))))
    +                        .video(Video.builder().w(1).h(1).build())
    +                        .bidfloor(BigDecimal.ONE)
    +                        .secure(1));
    +
    +        // when
    +        final Result>> result = beachfrontBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BeachfrontVideoRequest.class))
    +                .extracting(BeachfrontVideoRequest::getRequest)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor)
    +                .containsExactly(BigDecimal.ONE);
         }
     
         @Test
    @@ -317,7 +477,9 @@ public void makeBidsShouldReturnErrorWhenResponseBodyIsInvalid() {
             // then
             assertThat(result.getValue()).isEmpty();
             assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Unrecognized token");
    +        assertThat(result.getErrors().get(0).getMessage())
    +                .isEqualTo("server response failed to unmarshal as valid rtb. "
    +                        + "Run with request.debug = 1 for more info");
         }
     
         @Test
    @@ -431,12 +593,6 @@ public void makeBidsShouldReturnExpectedAdmVideoBid() throws JsonProcessingExcep
                                     .build(), BidType.video, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        // given, when and then
    -        assertThat(beachfrontBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function impCustomizer,
                 Function bidRequestCustomizer) {
    @@ -464,6 +620,7 @@ private static Imp givenImp(Function impCustomiz
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/beintoo/BeintooBidderTest.java b/src/test/java/org/prebid/server/bidder/beintoo/BeintooBidderTest.java
    index e75d9d76962..dc97553eff6 100644
    --- a/src/test/java/org/prebid/server/bidder/beintoo/BeintooBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/beintoo/BeintooBidderTest.java
    @@ -29,7 +29,6 @@
     import java.util.Map;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    @@ -364,12 +363,6 @@ public void makeBidsShouldAlwaysReturnBannerBidWithChangedBidImpId() throws Json
                     .containsOnly(BidderBid.of(Bid.builder().id("321").impid("321").build(), banner, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(beintooBidder.extractTargeting(mapper.createObjectNode()))
    -                .isEqualTo(emptyMap());
    -    }
    -
         private static BidResponse givenBidResponse(
                 Function bidCustomizer) {
             return BidResponse.builder()
    diff --git a/src/test/java/org/prebid/server/bidder/between/BetweenBidderTest.java b/src/test/java/org/prebid/server/bidder/between/BetweenBidderTest.java
    new file mode 100644
    index 00000000000..05edb5cff93
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/between/BetweenBidderTest.java
    @@ -0,0 +1,373 @@
    +package org.prebid.server.bidder.between;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Native;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import io.netty.handler.codec.http.HttpHeaderValues;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.between.ExtImpBetween;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
    +
    +public class BetweenBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://{{Host}}/test?param={{PublisherId}}";
    +
    +    private BetweenBidder betweenBidder;
    +
    +    @Before
    +    public void setUp() {
    +        betweenBidder = new BetweenBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new BetweenBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtContainEmptyOrNullHostParam() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of("", "pubId")))));
    +
    +        final Imp secondImp = givenImp(impBuilder -> impBuilder
    +                .id("456")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of(null, "pubId")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(Arrays.asList(firstImp, secondImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = betweenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(2)
    +                .containsExactly(
    +                        BidderError.badInput("required BetweenSSP parameter host is "
    +                                + "missing in impression with id: 123"),
    +                        BidderError.badInput("required BetweenSSP parameter host is "
    +                                + "missing in impression with id: 456"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfBannerNotPresentOrSizesNotExists() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .banner(null)
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of("host", "pubId")))));
    +
    +        final Imp secondImp = givenImp(impBuilder -> impBuilder
    +                .banner(Banner.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of("host", "pubId")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(Arrays.asList(firstImp, secondImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = betweenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(2)
    +                .containsExactlyInAnyOrder(
    +                        BidderError.badInput("Request needs to include a Banner object"),
    +                        BidderError.badInput("Need at least one size to build request"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCorrectlyAddHeaders() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBetween.of("host", "pubId")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ua("someUa").dnt(5).ip("someIp").language("someLanguage").build())
    +                .site(Site.builder().page("somePage").build())
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = betweenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .flatExtracting(res -> res.getHeaders().entries())
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .containsExactlyInAnyOrder(
    +                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    +                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()),
    +                        tuple(HttpUtil.USER_AGENT_HEADER.toString(), "someUa"),
    +                        tuple(HttpUtil.DNT_HEADER.toString(), "5"),
    +                        tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "someIp"),
    +                        tuple(HttpUtil.ACCEPT_LANGUAGE_HEADER.toString(), "someLanguage"),
    +                        tuple(HttpUtil.REFERER_HEADER.toString(), "somePage"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtContainEmptyOrNullPublisherIdParam() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of("host", "")))));
    +
    +        final Imp secondImp = givenImp(impBuilder -> impBuilder
    +                .id("456")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of("host", null)))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(Arrays.asList(firstImp, secondImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = betweenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(2)
    +                .containsExactlyInAnyOrder(
    +                        BidderError.badInput("required BetweenSSP parameter publisher_id "
    +                                + "is missing in impression with id: 123"),
    +                        BidderError.badInput("required BetweenSSP parameter publisher_id "
    +                                + "is missing in impression with id: 456"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = betweenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Missing bidder ext in impression with id: 123"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(Banner.builder()
    +                        .format(singletonList(Format.builder().w(300).h(500).build()))
    +                        .build()));
    +
    +        // when
    +        final Result>> result = betweenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://127.0.0.1/test?param=pubId");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldEnrichEveryImpWithSecureBidFlor() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder
    +                        .site(Site.builder().page("https://page.com").build()),
    +                impBuilder -> impBuilder.banner(Banner.builder()
    +                        .format(singletonList(Format.builder().w(300).h(500).build()))
    +                        .build()));
    +
    +        // when
    +        final Result>> result = betweenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getSecure)
    +                .containsExactly(1);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetDefaultParamsIfAnotherNotPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder
    +                        .site(Site.builder().page("http://page.com").build()),
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of("127.0.0.1", "pubId")))));
    +
    +        // when
    +        final Result>> result = betweenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getSecure)
    +                .containsExactly(0);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = betweenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = betweenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = betweenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = betweenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = betweenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = betweenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of("127.0.0.1", "pubId")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/bidmachine/BidmachineBidderTest.java b/src/test/java/org/prebid/server/bidder/bidmachine/BidmachineBidderTest.java
    new file mode 100644
    index 00000000000..6711f83be51
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/bidmachine/BidmachineBidderTest.java
    @@ -0,0 +1,276 @@
    +package org.prebid.server.bidder.bidmachine;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.bidmachine.ExtImpBidmachine;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
    +
    +public class BidmachineBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://{{HOST}}/{{PATH}}/{{SELLER_ID}}";
    +
    +    private BidmachineBidder bidmachineBidder;
    +
    +    @Before
    +    public void setUp() {
    +        bidmachineBidder = new BidmachineBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new BidmachineBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCorrectlyAddHeaders() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBidmachine.of("host", "pubId", "1")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ua("someUa").dnt(5).ip("someIp").language("someLanguage").build())
    +                .site(Site.builder().page("somePage").build())
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bidmachineBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .flatExtracting(res -> res.getHeaders().entries())
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .contains(tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = bidmachineBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .extracting(BidderError::getMessage)
    +                .containsExactly("Missing bidder ext in impression with id: 123");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(Banner.builder()
    +                        .format(singletonList(Format.builder().w(300).h(500).build()))
    +                        .build()));
    +
    +        // when
    +        final Result>> result = bidmachineBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://127.0.0.1/path/1");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotModifyImplIfNoPrebidIsRequest() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(Banner.builder()
    +                        .format(singletonList(Format.builder().w(300).h(500).build()))
    +                        .build()));
    +
    +        // when
    +        final Result>> result = bidmachineBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .containsExactly(bidRequest);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldModifyImplIfPrebidIsRequestAndBannerBattrDoesNotContain16() {
    +        // given
    +        final Imp imp = givenImp(impBuilder -> impBuilder
    +                .banner(Banner.builder()
    +                        .format(singletonList(Format.builder().w(300).h(500).build()))
    +                        .battr(singletonList(1)).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(
    +                        ExtImpPrebid.builder()
    +                                .isRewardedInventory(1)
    +                                .build(), ExtImpBidmachine.of("host", "pubId", "1")))));
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(imp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bidmachineBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getImp)
    +                .extracting(imps -> imps.get(0))
    +                .flatExtracting(currImp -> currImp.getBanner().getBattr())
    +                .containsExactly(1, 16);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldModifyImplIfPrebidIsRequestAndVideoBattrDoesNotContain16() {
    +        // given
    +        final Imp imp = givenImp(impBuilder -> impBuilder
    +                .video(Video.builder().battr(singletonList(1)).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(
    +                        ExtImpPrebid.builder()
    +                                .isRewardedInventory(1)
    +                                .build(), ExtImpBidmachine.of("host", "pubId", "1")))));
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(imp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bidmachineBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getImp)
    +                .extracting(imps -> imps.get(0))
    +                .flatExtracting(currImp -> currImp.getVideo().getBattr())
    +                .containsExactly(1, 16);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = bidmachineBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = bidmachineBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldAddErrorBidIfBidIdIsNotPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = bidmachineBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).isEqualTo(
    +                            "ignoring bid id=null, request doesn't contain any valid impression with id=123");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBidmachine.of("127.0.0.1", "path", "1")))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/bidmyadz/BidmyadzBidderTest.java b/src/test/java/org/prebid/server/bidder/bidmyadz/BidmyadzBidderTest.java
    new file mode 100644
    index 00000000000..a11252a42ed
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/bidmyadz/BidmyadzBidderTest.java
    @@ -0,0 +1,252 @@
    +package org.prebid.server.bidder.bidmyadz;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import io.vertx.core.MultiMap;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.bidmyadz.ExtImpBidmyadz;
    +import org.prebid.server.proto.openrtb.ext.response.BidType;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.emptyList;
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
    +
    +public class BidmyadzBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.endpoint.com/";
    +
    +    private BidmyadzBidder bidmyadzBidder;
    +
    +    @Before
    +    public void setUp() {
    +        bidmyadzBidder = new BidmyadzBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndPointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new BidmyadzBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsIfBidRequestDoesNotHaveDevice() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.device(null),
    +                identity());
    +
    +        // when
    +        final Result>> result = bidmyadzBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .hasSize(2)
    +                .containsExactly(BidderError.badInput("IP/IPv6 is a required field"),
    +                        BidderError.badInput("User-Agent is a required field"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsIfBidRequestDeviceDoesNotHaveIpOrIpV6() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.device(
    +                Device.builder().ua("ua").build()), identity());
    +
    +        // when
    +        final Result>> result = bidmyadzBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("IP/IPv6 is a required field"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsIfBidRequestDeviceDoesNotHaveUserAgent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.device(
    +                Device.builder().ip("ip").ipv6("ipv6").build()), identity());
    +
    +        // when
    +        final Result>> result = bidmyadzBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("User-Agent is a required field"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsIfBidRequestHasMoreThanOneImp() {
    +        // given
    +        BidRequest bidRequestWithTwoImps = givenBidRequest(identity(), identity()).toBuilder()
    +                .imp(asList(Imp.builder().build(), Imp.builder().build())).build();
    +        // when
    +        final Result>> result = bidmyadzBidder.makeHttpRequests(bidRequestWithTwoImps);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Bidder does not support multi impression"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnBidRequestWithOpenRtbHeader() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity(), identity());
    +
    +        // when
    +        final Result>> result = bidmyadzBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getHeaders)
    +                .flatExtracting(MultiMap::entries)
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .containsExactlyInAnyOrder(
    +                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), "application/json;charset=utf-8"),
    +                        tuple(HttpUtil.ACCEPT_HEADER.toString(), "application/json"),
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = bidmyadzBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfMediaTypeEqualsBanner() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123")
    +                                .ext(mapper.createObjectNode().put("mediaType", "banner")))));
    +
    +        // when
    +        final Result> result = bidmyadzBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).extracting(BidderBid::getType).containsExactly(BidType.banner);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfSeatBidIsEmpty() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(BidResponse.builder().seatbid(emptyList()).build()));
    +
    +        // when
    +        final Result> result = bidmyadzBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty SeatBid"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfSeatBidDoesNotHaveBids() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(BidResponse.builder()
    +                        .seatbid(singletonList(SeatBid.builder().bid(emptyList()).build())).build()));
    +
    +        // when
    +        final Result> result = bidmyadzBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty SeatBid.Bids"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfMediaTypeUnknown() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123")
    +                                .ext(mapper.createObjectNode().put("mediaType", "unknown")))));
    +
    +        // when
    +        final Result> result = bidmyadzBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse(
    +                "No enum constant org.prebid.server.proto.openrtb.ext.response.BidType.unknown"
    +        ));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .device(Device.builder().ip("ip").ipv6("ipv6").ua("ua").build())
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBidmyadz.of("placementId")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/bidscube/BidscubeBidderTest.java b/src/test/java/org/prebid/server/bidder/bidscube/BidscubeBidderTest.java
    new file mode 100644
    index 00000000000..f547e6f457d
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/bidscube/BidscubeBidderTest.java
    @@ -0,0 +1,295 @@
    +package org.prebid.server.bidder.bidscube;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import io.netty.handler.codec.http.HttpHeaderValues;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.bidscube.ExtImpBidscube;
    +import org.prebid.server.proto.openrtb.ext.response.BidType;
    +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
    +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;
    +
    +public class BidscubeBidderTest extends VertxTest {
    +
    +    private BidscubeBidder bidscubeBidder;
    +
    +    @Before
    +    public void setUp() {
    +        bidscubeBidder = new BidscubeBidder(jacksonMapper, "https://randomurl.com");
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() ->
    +                new BidscubeBidder(jacksonMapper, "invalid_url"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtContainNullPlacementIdParam() {
    +        // given
    +        final Imp imp = givenImp(impBuilder -> impBuilder
    +                .id("456")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBidscube.of(null)))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(imp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bidscubeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(
    +                BidderError.badInput("Missing required bidder parameters"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCorrectlyAddHeaders() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBidscube.of("someId")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bidscubeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .flatExtracting(res -> res.getHeaders().entries())
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .containsExactlyInAnyOrder(
    +                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    +                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = bidscubeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Missing required bidder parameters"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = bidscubeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = bidscubeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = bidscubeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnValidBidderBids() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidResponse.builder()
    +                        .seatbid(givenSeatBid(
    +                                givenBid("123", banner),
    +                                givenBid("456", video),
    +                                givenBid("789", audio),
    +                                givenBid("44454", xNative)))
    +                        .build());
    +
    +        // when
    +        final Result> result = bidscubeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactlyInAnyOrder(
    +                        BidderBid.of(givenBid("123", banner), banner, null),
    +                        BidderBid.of(givenBid("456", video), video, null),
    +                        BidderBid.of(givenBid("789", audio), banner, null),
    +                        BidderBid.of(givenBid("44454", xNative), xNative, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidForUnrecognisedType() throws JsonProcessingException {
    +        // given
    +        final ObjectNode givenExtPrebid = mapper.createObjectNode().put("type", "unknownType");
    +        final ObjectNode givenExt = mapper.createObjectNode().set("prebid", givenExtPrebid);
    +
    +        final HttpCall httpCall = givenHttpCall(
    +                BidResponse.builder()
    +                        .seatbid(givenSeatBid(
    +                                givenBid("123", null, bidBuilder -> bidBuilder.ext(givenExt)))
    +                        ).build());
    +
    +        // when
    +        final Result> result = bidscubeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(givenBid("123", video, bidBuilder -> bidBuilder.ext(givenExt)),
    +                        banner, null));
    +        assertThat(result.getErrors()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorOnEmptyBidExt() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidResponse.builder()
    +                        .seatbid(givenSeatBid(
    +                                givenBid("123", banner, bidBuilder -> bidBuilder.ext(null)),
    +                                givenBid("456", banner, bidBuilder -> bidBuilder.ext(mapper.valueToTree(
    +                                        ExtPrebid.of(null, null)))),
    +                                givenBid("213", null)
    +                                )
    +                        ).build());
    +
    +        // when
    +        final Result> result = bidscubeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(
    +                BidderError.badInput("Unable to read bid.ext.prebid.type"),
    +                BidderError.badInput("Unable to read bid.ext.prebid.type"),
    +                BidderError.badInput("Unable to read bid.ext.prebid.type"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorOnInvalidBidExt() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidResponse.builder()
    +                .seatbid(givenSeatBid(givenBid("123", banner, bidBuilder ->
    +                        bidBuilder.ext(mapper.valueToTree(ExtPrebid.of("invalid", null)))))).build());
    +
    +        // when
    +        final Result> result = bidscubeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(
    +                BidderError.badInput("Unable to read bid.ext.prebid.type"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBidscube.of("someId")))))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +
    +    private static HttpCall givenHttpCall(BidResponse bidResponse) throws JsonProcessingException {
    +        return HttpCall.success(
    +                null,
    +                HttpResponse.of(200, null, mapper.writeValueAsString(bidResponse)),
    +                null
    +        );
    +    }
    +
    +    private static Bid givenBid(String impid, BidType bidType, Function bidCustomizer) {
    +        return bidCustomizer.apply(Bid.builder()
    +                .impid(impid)
    +                .ext(mapper.valueToTree(ExtPrebid.of(ExtBidPrebid.builder().type(bidType).build(), null)))
    +        ).build();
    +    }
    +
    +    private static Bid givenBid(String impid, BidType bidType) {
    +        return givenBid(impid, bidType, identity());
    +    }
    +
    +    private static List givenSeatBid(Bid... bids) {
    +        return singletonList(SeatBid.builder().bid(asList(bids)).build());
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/bmtm/BmtmBidderTest.java b/src/test/java/org/prebid/server/bidder/bmtm/BmtmBidderTest.java
    new file mode 100644
    index 00000000000..84cc3043e10
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/bmtm/BmtmBidderTest.java
    @@ -0,0 +1,344 @@
    +package org.prebid.server.bidder.bmtm;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import io.netty.handler.codec.http.HttpHeaderValues;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.bmtm.ExtImpBmtm;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +
    +public class BmtmBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://localhost:8080";
    +
    +    private BmtmBidder bmtmBidder;
    +
    +    @Before
    +    public void setUp() {
    +        bmtmBidder = new BmtmBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new BmtmBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCorrectlyAddHeaders() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBmtm.of("placement_id")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ua("someUa").ip("someIp").build())
    +                .site(Site.builder().page("somePage").build())
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bmtmBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .flatExtracting(res -> res.getHeaders().entries())
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .contains(tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    +                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()),
    +                        tuple(HttpUtil.USER_AGENT_HEADER.toString(), bidRequest.getDevice().getUa()),
    +                        tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), bidRequest.getDevice().getIp()),
    +                        tuple(HttpUtil.REFERER_HEADER.toString(), bidRequest.getSite().getPage()));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotAddIpv6IfIpIsPresent() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBmtm.of("placement_id")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ua("someUa").ip("someIp").ipv6("ipv6").build())
    +                .site(Site.builder().page("somePage").build())
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bmtmBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .flatExtracting(res -> res.getHeaders().entries())
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .contains(tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), bidRequest.getDevice().getIp()));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldAddIpv6IfIpIsNotPresent() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBmtm.of("placement_id")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ua("someUa").ipv6("ipv6").build())
    +                .site(Site.builder().page("somePage").build())
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bmtmBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .flatExtracting(res -> res.getHeaders().entries())
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .contains(tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), bidRequest.getDevice().getIpv6()));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldAddExceptionIfNoBannerAndVideoProvided() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .banner(null)
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBmtm.of("placement_id")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ua("someUa").ipv6("ipv6").build())
    +                .site(Site.builder().page("somePage").build())
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bmtmBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("For Imp ID 123 Banner or Video is undefined"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldModifyTagIdForOutgoingRequest() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBmtm.of("placement_id")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bmtmBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid)
    +                .containsExactly("placement_id");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateRequestForEachImp() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBmtm.of("placement_id")))));
    +        final Imp secondImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBmtm.of("placement_id2")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(firstImp, secondImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bmtmBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(2)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid)
    +                .containsExactly("placement_id", "placement_id2");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldDeleteExtForOutgoingRequestIfRequestIsValid() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpBmtm.of("placement_id")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = bmtmBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final Imp expectedImp = givenImp(impBuilder -> impBuilder.ext(null).tagid("placement_id"));
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .containsOnly(expectedImp);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = bmtmBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .extracting(BidderError::getMessage)
    +                .containsExactly("Missing bidder ext in impression with id: 123");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = bmtmBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = bmtmBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = bmtmBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerIfBidIdIsNotPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = bmtmBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBmtm.of("placement_id")))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/brightroll/BrightrollBidderTest.java b/src/test/java/org/prebid/server/bidder/brightroll/BrightrollBidderTest.java
    index ea86c31d9f4..50d83af29fb 100644
    --- a/src/test/java/org/prebid/server/bidder/brightroll/BrightrollBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/brightroll/BrightrollBidderTest.java
    @@ -39,7 +39,6 @@
     import java.util.Map;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.Collections.singletonMap;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -97,7 +96,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMeth
                             tuple(HttpUtil.ACCEPT_LANGUAGE_HEADER.toString(), "en"),
                             tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "192.168.0.1"),
                             tuple(HttpUtil.DNT_HEADER.toString(), "1"),
    -                        tuple("x-openrtb-version", "2.5"));
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
             assertThat(result.getValue()).extracting(HttpRequest::getBody).containsExactly(mapper.writeValueAsString(
                     BidRequest.builder()
                             .imp(singletonList(Imp.builder()
    @@ -306,7 +305,7 @@ public void makeHttpRequestShouldReturnRequestWithoutDeviceHeadersWhenDeviceIsNu
                     .containsOnly(
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()),
    -                        tuple("x-openrtb-version", "2.5"));
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
         }
     
         @Test
    @@ -587,11 +586,6 @@ public void makeBidsShouldReturnEmptyBidderWithErrorWhenResponseCantBeParsed() {
                                     + "column: 3]"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(brightrollBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static HttpCall givenHttpCall(String body) {
             return HttpCall.success(null, HttpResponse.of(200, null, body), null);
         }
    diff --git a/src/test/java/org/prebid/server/bidder/colossus/ColossusBidderTest.java b/src/test/java/org/prebid/server/bidder/colossus/ColossusBidderTest.java
    new file mode 100644
    index 00000000000..72836735db1
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/colossus/ColossusBidderTest.java
    @@ -0,0 +1,236 @@
    +package org.prebid.server.bidder.colossus;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.colossus.ExtImpColossus;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +
    +public class ColossusBidderTest extends VertxTest {
    +
    +    public static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private ColossusBidder colossusBidder;
    +
    +    @Before
    +    public void setUp() {
    +        colossusBidder = new ColossusBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new ColossusBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = colossusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnExpectedBidRequest() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = colossusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final BidRequest expectedRequest = bidRequest.toBuilder()
    +                .imp(singletonList(bidRequest.getImp().get(0).toBuilder().tagid("tagidString").build()))
    +                .build();
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .containsOnly(expectedRequest);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestPerImp() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        Imp.builder()
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpColossus.of("otherTagId"))))
    +                                .build())));
    +
    +        // when
    +        final Result>> result = colossusBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(2)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).hasSize(2)
    +                .extracting(Imp::getTagid)
    +                .containsOnly("tagidString", "otherTagId");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = colossusBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = colossusBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = colossusBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = colossusBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = colossusBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(givenImp(identity())))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = colossusBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer) {
    +        return givenBidRequest(impCustomizer, identity());
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .video(Video.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpColossus.of("tagidString"))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(
    +            Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/connectad/ConnectadBidderTest.java b/src/test/java/org/prebid/server/bidder/connectad/ConnectadBidderTest.java
    new file mode 100644
    index 00000000000..0fe81516a11
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/connectad/ConnectadBidderTest.java
    @@ -0,0 +1,226 @@
    +package org.prebid.server.bidder.connectad;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.assertj.core.api.Assertions;
    +import org.hamcrest.MatcherAssert;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.connectad.ExtImpConnectAd;
    +
    +import java.math.BigDecimal;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.hamcrest.Matchers.containsInAnyOrder;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
    +
    +public class ConnectadBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.endpoint.com/";
    +
    +    private ConnectadBidder connectadBidder;
    +
    +    @Before
    +    public void setUp() {
    +        connectadBidder = new ConnectadBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        Assertions.assertThatIllegalArgumentException().isThrownBy(() ->
    +                new ConnectadBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .id("123")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = connectadBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(2);
    +        MatcherAssert.assertThat(result.getErrors(),
    +                containsInAnyOrder(BidderError.badInput("Error in preprocess of Imp"),
    +                        BidderError.badInput("Impression id=123, has invalid Ext")));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = connectadBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListWhenBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = connectadBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListWhenBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = connectadBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = connectadBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .id("123")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = connectadBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(2);
    +        MatcherAssert.assertThat(result.getErrors(),
    +                containsInAnyOrder(BidderError.badInput("Impression id=123, has invalid Ext"),
    +                        BidderError.badInput("Error in preprocess of Imp")));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtHasNoSiteId() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .id("123")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpConnectAd.of(12, null, BigDecimal.ONE)))));
    +        // when
    +        final Result>> result = connectadBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(2);
    +        MatcherAssert.assertThat(result.getErrors(),
    +                containsInAnyOrder(BidderError.badInput("Impression id=123, has no siteId present"),
    +                        BidderError.badInput("Error in preprocess of Imp")));
    +    }
    +
    +    @Test
    +    public void impSecureShouldBeOneIfSitePageStartsFromHttps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .id("123")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpConnectAd.of(12, 1, BigDecimal.ONE)))));
    +        // when
    +        final Result>> result = connectadBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(0);
    +        assertThat(result.getValue())
    +                .hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getImp)
    +                .hasSize(1)
    +                .extracting(imp -> imp.get(0).getSecure())
    +                .hasSize(1)
    +                .containsOnly(1);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .site(Site.builder().page("https://test.url.com/").build())
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().id("banner_id").w(14).h(15).build()).ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpConnectAd.of(12, 12, BigDecimal.ONE)))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .cur("EUR")
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/consumable/ConsumableBidderTest.java b/src/test/java/org/prebid/server/bidder/consumable/ConsumableBidderTest.java
    index 490bdfb7b78..8508f57403a 100644
    --- a/src/test/java/org/prebid/server/bidder/consumable/ConsumableBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/consumable/ConsumableBidderTest.java
    @@ -41,7 +41,6 @@
     import java.util.Map;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.Collections.singletonMap;
     import static java.util.function.Function.identity;
    @@ -97,7 +96,6 @@ public void makeHttpRequestsShouldSetDefaultHeaders() {
                     .containsOnly(
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
    -
         }
     
         @Test
    @@ -307,11 +305,6 @@ public void makeBidsShouldReturnBannerBidWithExpectedFields() throws JsonProcess
                             BidType.banner, null));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(consumableBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequestWithTwoImpsAndTwoFormats() {
             return BidRequest.builder()
                     .id("request_id")
    diff --git a/src/test/java/org/prebid/server/bidder/conversant/ConversantAdapterTest.java b/src/test/java/org/prebid/server/bidder/conversant/ConversantAdapterTest.java
    deleted file mode 100644
    index 356a951e84f..00000000000
    --- a/src/test/java/org/prebid/server/bidder/conversant/ConversantAdapterTest.java
    +++ /dev/null
    @@ -1,613 +0,0 @@
    -package org.prebid.server.bidder.conversant;
    -
    -import com.fasterxml.jackson.databind.node.ObjectNode;
    -import com.fasterxml.jackson.databind.node.TextNode;
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.Banner;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.BidRequest.BidRequestBuilder;
    -import com.iab.openrtb.request.Device;
    -import com.iab.openrtb.request.Format;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Regs;
    -import com.iab.openrtb.request.Site;
    -import com.iab.openrtb.request.Source;
    -import com.iab.openrtb.request.User;
    -import com.iab.openrtb.response.Bid;
    -import com.iab.openrtb.response.BidResponse;
    -import com.iab.openrtb.response.BidResponse.BidResponseBuilder;
    -import com.iab.openrtb.response.SeatBid;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.auction.model.AdUnitBid.AdUnitBidBuilder;
    -import org.prebid.server.auction.model.AdapterRequest;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    -import org.prebid.server.auction.model.PreBidRequestContext.PreBidRequestContextBuilder;
    -import org.prebid.server.bidder.conversant.proto.ConversantParams;
    -import org.prebid.server.bidder.conversant.proto.ConversantParams.ConversantParamsBuilder;
    -import org.prebid.server.bidder.model.AdapterHttpRequest;
    -import org.prebid.server.bidder.model.ExchangeCall;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.request.PreBidRequest;
    -import org.prebid.server.proto.request.PreBidRequest.PreBidRequestBuilder;
    -import org.prebid.server.proto.request.Video;
    -import org.prebid.server.proto.response.BidderDebug;
    -import org.prebid.server.proto.response.MediaType;
    -
    -import java.math.BigDecimal;
    -import java.util.EnumSet;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.function.Function;
    -import java.util.stream.Collectors;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptySet;
    -import static java.util.Collections.singleton;
    -import static java.util.Collections.singletonList;
    -import static java.util.function.Function.identity;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    -import static org.assertj.core.api.Assertions.assertThatNullPointerException;
    -import static org.assertj.core.api.Assertions.assertThatThrownBy;
    -import static org.assertj.core.api.Assertions.tuple;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -
    -public class ConversantAdapterTest extends VertxTest {
    -
    -    private static final String BIDDER = "conversant";
    -    private static final String COOKIE_FAMILY = BIDDER;
    -    private static final String ENDPOINT_URL = "http://exchange.org/";
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private UidsCookie uidsCookie;
    -
    -    private AdapterRequest adapterRequest;
    -    private PreBidRequestContext preBidRequestContext;
    -    private ExchangeCall exchangeCall;
    -    private ConversantAdapter adapter;
    -
    -    @Before
    -    public void setUp() {
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUid1");
    -
    -        adapterRequest = givenBidder(identity(), identity());
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -        adapter = new ConversantAdapter(COOKIE_FAMILY, ENDPOINT_URL, jacksonMapper);
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnNullArguments() {
    -        assertThatNullPointerException().isThrownBy(() -> new ConversantAdapter(null, null, jacksonMapper));
    -        assertThatNullPointerException().isThrownBy(() -> new ConversantAdapter(COOKIE_FAMILY, null, jacksonMapper));
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException()
    -                .isThrownBy(() -> new ConversantAdapter(COOKIE_FAMILY, "invalid_url", jacksonMapper))
    -                .withMessage("URL supplied is not valid: invalid_url");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedHeaders() {
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).flatExtracting(r -> r.getHeaders().entries())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple("Content-Type", "application/json;charset=utf-8"),
    -                        tuple("Accept", "application/json"));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfParamsMissingInAtLeastOneAdUnitBid() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(identity(), identity()),
    -                givenAdUnitBid(builder -> builder.params(null), identity())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Conversant params section is missing");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamsCouldNotBeParsed() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("secure", new TextNode("non-integer"));
    -        adapterRequest = givenBidder(builder -> builder.params(params), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessageStartingWith("Cannot deserialize value of type");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamPublisherIdIsMissing() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("site_id", null);
    -        adapterRequest = givenBidder(builder -> builder.params(params), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Missing site id");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithValidVideoApiFromAdUnitParam() {
    -        // given
    -        adapterRequest = givenBidder(builder -> builder
    -                        .mediaTypes(singleton(MediaType.video))
    -                        .video(Video.builder().mimes(singletonList("mime1")).build()),
    -                builder -> builder.api(asList(1, 3, 6, 100)));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .flatExtracting(r -> r.getPayload().getImp()).isNotNull()
    -                .flatExtracting(imp -> imp.getVideo().getApi())
    -                .containsOnly(1, 3, 6);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithValidVideoProtocolsFromAdUnitParam() {
    -        // given
    -        adapterRequest = givenBidder(builder -> builder
    -                        .mediaTypes(singleton(MediaType.video))
    -                        .video(Video.builder().mimes(singletonList("mime1")).build()),
    -                builder -> builder.protocols(asList(1, 5, 10, 100)));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .flatExtracting(r -> r.getPayload().getImp()).isNotNull()
    -                .flatExtracting(imp -> imp.getVideo().getProtocols())
    -                .containsOnly(1, 5, 10);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithVideoProtocolsFromAdUnitFieldIfParamIsMissing() {
    -        // given
    -        adapterRequest = givenBidder(builder -> builder
    -                        .mediaTypes(singleton(MediaType.video))
    -                        .video(Video.builder().mimes(singletonList("mime1")).protocols(singletonList(200)).build()),
    -                builder -> builder.protocols(null));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .flatExtracting(r -> r.getPayload().getImp()).isNotNull()
    -                .flatExtracting(imp -> imp.getVideo().getProtocols())
    -                .containsOnly(200);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithVideoAdPositionFromAdUnitParam() {
    -        // given
    -        adapterRequest = givenBidder(builder -> builder
    -                        .mediaTypes(singleton(MediaType.video))
    -                        .video(Video.builder().mimes(singletonList("mime1")).build()),
    -                builder -> builder.position(0));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .flatExtracting(r -> r.getPayload().getImp()).isNotNull()
    -                .extracting(imp -> imp.getVideo().getPos())
    -                .containsOnly(0);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithNullVideoAdPositionIfInvalidAdUnitParam() {
    -        // given
    -        adapterRequest = givenBidder(builder -> builder
    -                        .mediaTypes(singleton(MediaType.video))
    -                        .video(Video.builder().mimes(singletonList("mime1")).build()),
    -                builder -> builder.position(100));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .flatExtracting(r -> r.getPayload().getImp()).isNotNull()
    -                .extracting(imp -> imp.getVideo().getPos())
    -                .hasSize(1)
    -                .containsNull();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                                .adUnitCode("adUnitCode1")
    -                                .mediaTypes(emptySet()),
    -                        identity())));
    -
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("openRTB bids need at least one Imp");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsVideoAndMimesListIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                                .adUnitCode("adUnitCode1")
    -                                .mediaTypes(singleton(MediaType.video))
    -                                .video(Video.builder()
    -                                        .mimes(emptyList())
    -                                        .build()),
    -                        identity())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid AdUnit: VIDEO media type with no video data");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder
    -                        .bidderCode(BIDDER)
    -                        .adUnitCode("adUnitCode1")
    -                        .sizes(singletonList(Format.builder().w(300).h(250).build())),
    -                paramsBuilder -> paramsBuilder
    -                        .siteId("siteId42")
    -                        .secure(12)
    -                        .tagId("tagId42")
    -                        .position(3)
    -                        .bidfloor(BigDecimal.valueOf(1.03))
    -                        .mobile(87)
    -                        .mimes(singletonList("mime42"))
    -                        .api(singletonList(10))
    -                        .protocols(singletonList(50))
    -                        .maxduration(40));
    -
    -        preBidRequestContext = givenPreBidRequestContext(
    -                builder -> builder
    -                        .referer("http://www.example.com")
    -                        .domain("example.com")
    -                        .ip("192.168.144.1")
    -                        .ua("userAgent1"),
    -                builder -> builder
    -                        .timeoutMillis(1500L)
    -                        .tid("tid1")
    -                        .user(User.builder()
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .containsOnly(BidRequest.builder()
    -                        .id("tid1")
    -                        .at(1)
    -                        .tmax(1500L)
    -                        .imp(singletonList(Imp.builder()
    -                                .id("adUnitCode1")
    -                                .tagid("tagId42")
    -                                .banner(Banner.builder()
    -                                        .w(300)
    -                                        .h(250)
    -                                        .format(singletonList(Format.builder()
    -                                                .w(300)
    -                                                .h(250)
    -                                                .build()))
    -                                        .pos(3)
    -                                        .build())
    -                                .displaymanager("prebid-s2s")
    -                                .displaymanagerver("1.0.1")
    -                                .bidfloor(BigDecimal.valueOf(1.03))
    -                                .secure(12)
    -                                .build()))
    -                        .site(Site.builder()
    -                                .domain("example.com")
    -                                .page("http://www.example.com")
    -                                .id("siteId42")
    -                                .mobile(87)
    -                                .build())
    -                        .device(Device.builder()
    -                                .ua("userAgent1")
    -                                .ip("192.168.144.1")
    -                                .build())
    -                        .user(User.builder()
    -                                .buyeruid("buyerUid1")
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null)))
    -                        .source(Source.builder()
    -                                .fd(1)
    -                                .tid("tid1")
    -                                .build())
    -                        .build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldSetAppIdFromParamsSiteId() {
    -        // given
    -        adapterRequest = givenBidder(
    -                identity(),
    -                paramsBuilder -> paramsBuilder.siteId("siteId42"));
    -        preBidRequestContext = givenPreBidRequestContext(
    -                identity(),
    -                builder -> builder
    -                        .app(App.builder().id("to_be_changed").build()));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .extracting(BidRequest::getApp)
    -                .extracting(App::getId)
    -                .containsOnly("siteId42");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldThrowPrebidExceptionIfAppHasBlankOrMissingId() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder
    -                .app(App.builder().build()));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Missing app id");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithOneRequestIfMultipleAdUnitsInPreBidRequest() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1"), identity()),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"), identity())));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(2)
    -                .extracting(Imp::getId).containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldFailIfBidImpIdDoesNotMatchAdUnitCode() {
    -        // given
    -        adapterRequest = givenBidder(builder -> builder.adUnitCode("adUnitCode"), identity());
    -
    -        exchangeCall = givenExchangeCall(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("anotherAdUnitCode").build()))
    -                        .build())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Unknown ad unit code 'anotherAdUnitCode'");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithOneRequestWithOneImpIfAdUnitContainsBannerAndVideoMediaTypes() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                                .mediaTypes(EnumSet.of(MediaType.video, MediaType.banner))
    -                                .video(Video.builder()
    -                                        .mimes(singletonList("Mime1"))
    -                                        .playbackMethod(1)
    -                                        .build()),
    -                        identity())));
    -
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(1)
    -                .extracting(imp -> imp.getVideo() == null, imp -> imp.getBanner() == null)
    -                .containsOnly(tuple(false, false));
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .price(new BigDecimal("8.43"))
    -                                        .adm("adm")
    -                                        .crid("crid")
    -                                        .w(300)
    -                                        .h(250)
    -                                        .dealid("dealId")
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids)
    -                .containsExactly(org.prebid.server.proto.response.Bid.builder()
    -                        .code("adUnitCode")
    -                        .price(new BigDecimal("8.43"))
    -                        .adm("adm")
    -                        .creativeId("crid")
    -                        .width(300)
    -                        .height(250)
    -                        .bidder(BIDDER)
    -                        .bidId("bidId")
    -                        .mediaType(MediaType.banner)
    -                        .build());
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnEmptyBidsIfEmptyOrNullBidResponse() {
    -        // given
    -        adapterRequest = givenBidder(identity(), identity());
    -
    -        exchangeCall = givenExchangeCall(identity(), br -> br.seatbid(null));
    -
    -        // when and then
    -        assertThat(adapter.extractBids(adapterRequest, exchangeCall)).isEmpty();
    -        assertThat(adapter.extractBids(adapterRequest, ExchangeCall.empty(null))).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnMultipleBidBuildersIfMultipleAdUnitsInPreBidRequestAndBidsInResponse() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1"), identity()),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"), identity())));
    -
    -        exchangeCall = givenExchangeCall(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(asList(Bid.builder().impid("adUnitCode1").build(),
    -                                        Bid.builder().impid("adUnitCode2").build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(2)
    -                .extracting(org.prebid.server.proto.response.Bid::getCode)
    -                .containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    private static AdapterRequest givenBidder(
    -            Function adUnitBidBuilderCustomizer,
    -            Function paramsBuilderCustomizer) {
    -
    -        return AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(adUnitBidBuilderCustomizer, paramsBuilderCustomizer)));
    -    }
    -
    -    private static AdUnitBid givenAdUnitBid(
    -            Function adUnitBidBuilderCustomizer,
    -            Function paramsBuilderCustomizer) {
    -
    -        // params
    -        final ConversantParamsBuilder paramsBuilder = ConversantParams.builder()
    -                .siteId("siteId1")
    -                .secure(42)
    -                .tagId("tagId1")
    -                .position(2)
    -                .bidfloor(BigDecimal.valueOf(7.32))
    -                .mobile(64)
    -                .mimes(singletonList("mime1"))
    -                .api(singletonList(1))
    -                .protocols(singletonList(5))
    -                .maxduration(30);
    -        final ConversantParamsBuilder paramsBuilderCustomized = paramsBuilderCustomizer
    -                .apply(paramsBuilder);
    -        final ConversantParams params = paramsBuilderCustomized.build();
    -
    -        // ad unit bid
    -        final AdUnitBidBuilder adUnitBidBuilderMinimal = AdUnitBid.builder()
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .params(mapper.valueToTree(params))
    -                .mediaTypes(singleton(MediaType.banner));
    -        final AdUnitBidBuilder adUnitBidBuilderCustomized = adUnitBidBuilderCustomizer.apply(
    -                adUnitBidBuilderMinimal);
    -
    -        return adUnitBidBuilderCustomized.build();
    -    }
    -
    -    private PreBidRequestContext givenPreBidRequestContext(
    -            Function preBidRequestContextBuilderCustomizer,
    -            Function preBidRequestBuilderCustomizer) {
    -
    -        final PreBidRequestBuilder preBidRequestBuilderMinimal = PreBidRequest.builder()
    -                .accountId("accountId");
    -        final PreBidRequest preBidRequest = preBidRequestBuilderCustomizer.apply(preBidRequestBuilderMinimal).build();
    -
    -        final PreBidRequestContextBuilder preBidRequestContextBuilderMinimal =
    -                PreBidRequestContext.builder()
    -                        .preBidRequest(preBidRequest)
    -                        .uidsCookie(uidsCookie);
    -        return preBidRequestContextBuilderCustomizer.apply(preBidRequestContextBuilderMinimal).build();
    -    }
    -
    -    private static ExchangeCall givenExchangeCall(
    -            Function bidRequestBuilderCustomizer,
    -            Function bidResponseBuilderCustomizer) {
    -
    -        final BidRequestBuilder bidRequestBuilderMinimal = BidRequest.builder();
    -        final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(bidRequestBuilderMinimal).build();
    -
    -        final BidResponseBuilder bidResponseBuilderMinimal = BidResponse.builder();
    -        final BidResponse bidResponse = bidResponseBuilderCustomizer.apply(bidResponseBuilderMinimal).build();
    -
    -        return ExchangeCall.success(bidRequest, bidResponse, BidderDebug.builder().build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java b/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java
    index 7761eba41e0..eda5e2ab484 100644
    --- a/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/conversant/ConversantBidderTest.java
    @@ -23,11 +23,11 @@
     import org.prebid.server.proto.openrtb.ext.request.conversant.ExtImpConversant;
     
     import java.math.BigDecimal;
    +import java.util.Collections;
     import java.util.List;
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -39,17 +39,20 @@
     public class ConversantBidderTest extends VertxTest {
     
         private static final String ENDPOINT_URL = "https://test.endpoint.com";
    +    private static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}"
    +            + "-[0-9a-fA-F]{12}";
     
         private ConversantBidder conversantBidder;
     
         @Before
         public void setUp() {
    -        conversantBidder = new ConversantBidder(ENDPOINT_URL, jacksonMapper);
    +        conversantBidder = new ConversantBidder(ENDPOINT_URL, false, jacksonMapper);
         }
     
         @Test
         public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException().isThrownBy(() -> new ConversantBidder("invalid_url", jacksonMapper));
    +        assertThatIllegalArgumentException()
    +                .isThrownBy(() -> new ConversantBidder("invalid_url", false, jacksonMapper));
         }
     
         @Test
    @@ -65,9 +68,6 @@ public void makeHttpRequestsShouldSkipInvalidImpAndAddErrorIfImpHasNoBannerOrVid
             final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput(
    -                        "Invalid MediaType. Conversant supports only Banner and Video. Ignoring ImpID=123"));
             assertThat(result.getValue()).hasSize(1);
         }
     
    @@ -82,8 +82,8 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(2);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Impression[0] missing ext.bidder object"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -101,8 +101,8 @@ public void makeHttpRequestsShouldReturnErrorIfRequestHasSiteAndImpExtSiteIdIsNu
             final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Missing site id"));
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Impression[0] requires ext.bidder.site_id"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -123,7 +123,7 @@ public void makeHttpRequestsShouldSetSiteIdFromImpExtForSiteRequest() {
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .extracting(BidRequest::getSite)
                     .extracting(Site::getId)
    -                .containsOnly("site id");
    +                .containsExactly("site id");
         }
     
         @Test
    @@ -143,7 +143,7 @@ public void makeHttpRequestsShouldSetAppIdFromExtSiteIdIfAppIdIsNullOrEmpty() {
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .extracting(BidRequest::getApp)
                     .extracting(App::getId)
    -                .containsOnly("site id");
    +                .containsExactly("site id");
         }
     
         @Test
    @@ -164,31 +164,11 @@ public void makeHttpRequestsShouldSetSiteIdFromExtSiteIdIfSiteIdIsNullOrEmpty()
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .extracting(BidRequest::getSite)
                     .extracting(Site::getId)
    -                .containsOnly("site id");
    +                .containsExactly("site id");
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetSiteMobileFromImpExtIfPresent() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                requestBuilder -> requestBuilder.site(Site.builder().build()),
    -                identity(),
    -                extBuilder -> extBuilder.mobile(1));
    -
    -        // when
    -        final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .extracting(BidRequest::getSite)
    -                .extracting(Site::getMobile)
    -                .containsOnly(1);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldSetImpDisplaymanagerAndDisplaymanagerver() {
    +    public void makeHttpRequestsShouldSetImpDisplayManagerAndDisplayManagerVer() {
             // given
             final BidRequest bidRequest = givenBidRequest(identity());
     
    @@ -201,11 +181,11 @@ public void makeHttpRequestsShouldSetImpDisplaymanagerAndDisplaymanagerver() {
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getDisplaymanager, Imp::getDisplaymanagerver)
    -                .containsOnly(tuple("prebid-s2s", "1.0.1"));
    +                .containsExactly(tuple("prebid-s2s", "2.0.0"));
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetImpBidfloorFromImpExtIfPresent() {
    +    public void makeHttpRequestsShouldSetImpBidFloorFromImpExtIfPresent() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
    @@ -220,7 +200,7 @@ public void makeHttpRequestsShouldSetImpBidfloorFromImpExtIfPresent() {
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getBidfloor)
    -                .containsOnly(BigDecimal.valueOf(9.99));
    +                .containsExactly(BigDecimal.valueOf(9.99));
         }
     
         @Test
    @@ -239,7 +219,7 @@ public void makeHttpRequestsShouldSetImpTagIdFromImpExtIfPresent() {
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getTagid)
    -                .containsOnly("tag id");
    +                .containsExactly("tag id");
         }
     
         @Test
    @@ -258,7 +238,7 @@ public void makeHttpRequestsShouldChangeImpSecureFromImpExtIfPresent() {
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getSecure)
    -                .containsOnly(1);
    +                .containsExactly(1);
         }
     
         @Test
    @@ -277,14 +257,15 @@ public void makeHttpRequestsShouldNotChangeImpSecure() {
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getSecure)
    -                .containsOnly(1);
    +                .containsExactly(1);
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetImpBannerAndVideoPosFromImpExtIfPresent() {
    +    public void makeHttpRequestsShouldSetImpForBannerOnlyFromImpExtWhenVideoIsPresent() {
             // given
    +        final Video requestVideo = Video.builder().pos(1).build();
             final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder.video(Video.builder().pos(1).build()),
    +                impBuilder -> impBuilder.banner(Banner.builder().pos(1).build()).video(requestVideo),
                     extBuilder -> extBuilder.position(5));
     
             // when
    @@ -296,16 +277,35 @@ public void makeHttpRequestsShouldSetImpBannerAndVideoPosFromImpExtIfPresent() {
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getBanner, Imp::getVideo)
    -                .containsOnly(tuple(
    +                .containsExactly(tuple(
                             Banner.builder().pos(5).build(),
    -                        Video.builder().pos(5).build()));
    +                        requestVideo));
         }
     
         @Test
    -    public void makeHttpRequestsShouldNotChangeVideoPosFromImpExtIfNotInRange() {
    +    public void makeHttpRequestsShouldSetPosBannerToNullIfExtPosIsNull() {
             // given
             final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder.video(Video.builder().pos(7).build()),
    +                impBuilder -> impBuilder.banner(Banner.builder().pos(23).build()),
    +                extBuilder -> extBuilder.position(null));
    +
    +        // when
    +        final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .containsExactly(Banner.builder().pos(null).build());
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetPosBannerToNullIfExtPosIsNotValid() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(Banner.builder().pos(23).build()),
                     extBuilder -> extBuilder.position(9));
     
             // when
    @@ -316,9 +316,27 @@ public void makeHttpRequestsShouldNotChangeVideoPosFromImpExtIfNotInRange() {
             assertThat(result.getValue()).hasSize(1)
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getVideo)
    -                .extracting(Video::getPos)
    -                .containsOnly(7);
    +                .extracting(Imp::getBanner)
    +                .containsExactly(Banner.builder().pos(null).build());
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetPosBannerToPosFromExt() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(Banner.builder().pos(23).build()),
    +                extBuilder -> extBuilder.position(7));
    +
    +        // when
    +        final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .containsExactly(Banner.builder().pos(7).build());
         }
     
         @Test
    @@ -358,7 +376,27 @@ public void makeHttpRequestsShouldSetVideoMimesFromImpExtIfPresent() {
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getVideo)
                     .flatExtracting(Video::getMimes)
    -                .containsOnly("mime");
    +                .containsExactly("mime");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotSetMimesFromExtIfExtMimesNotPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.video(Video.builder().mimes(singletonList("videoMimes")).build()),
    +                extBuilder -> extBuilder.mimes(Collections.emptyList()));
    +
    +        // when
    +        final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getVideo)
    +                .flatExtracting(Video::getMimes)
    +                .containsExactly("videoMimes");
         }
     
         @Test
    @@ -378,7 +416,7 @@ public void makeHttpRequestsShouldSetVideoMaxdurationFromImpExtIfPresent() {
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getVideo)
                     .extracting(Video::getMaxduration)
    -                .containsOnly(1000);
    +                .containsExactly(1000);
         }
     
         @Test
    @@ -398,7 +436,7 @@ public void makeHttpRequestsShouldChangeVideoApiFromImpExtIfPresent() {
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getVideo)
                     .flatExtracting(Video::getApi)
    -                .containsOnly(2, 5);
    +                .containsExactly(2, 5);
         }
     
         @Test
    @@ -438,7 +476,7 @@ public void makeHttpRequestsShouldChangeVideoProtocolsFromImpExtIfPresent() {
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getVideo)
                     .flatExtracting(Video::getProtocols)
    -                .containsOnly(1, 10);
    +                .containsExactly(1, 10);
         }
     
         @Test
    @@ -470,9 +508,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             final Result> result = conversantBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -486,7 +526,8 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces
             final Result> result = conversantBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badServerResponse("Empty bid request"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -500,7 +541,8 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso
             final Result> result = conversantBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badServerResponse("Empty bid request"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -519,7 +561,22 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfNoBidsFromSeatArePresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder()
    +                        .seatbid(singletonList(SeatBid.builder().build())).build()));
    +
    +        // when
    +        final Result> result = conversantBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsOnly(BidderError.badServerResponse("Empty bids array"));
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    @@ -538,12 +595,67 @@ public void makeBidsShouldReturnVideoBidIfRequestImpHasVideo() throws JsonProces
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldUpdateBidWithUUIDIfGenerateBidIdIsTrue() throws JsonProcessingException {
    +        // given
    +        conversantBidder = new ConversantBidder(ENDPOINT_URL, true, jacksonMapper);
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(builder -> builder.id("123")
    +                        .banner(Banner.builder().build())),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = conversantBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getId)
    +                .element(0)
    +                .matches(id -> id.matches(UUID_REGEX), "matches UUID format");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetImpBidFloorFromImpExtIfPresentAndImpBidFloorIsInvalid() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.bidfloor(new BigDecimal(-1.00)),
    +                extBuilder -> extBuilder.bidfloor(BigDecimal.ONE));
    +
    +        // when
    +        final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor)
    +                .containsExactly(BigDecimal.ONE);
         }
     
         @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(conversantBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +    public void makeHttpRequestsShouldNotSetImpBidFloorFromImpExt() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.bidfloor(new BigDecimal(-1.00)),
    +                extBuilder -> extBuilder.bidfloor(new BigDecimal(-2.00)));
    +
    +        // when
    +        final Result>> result = conversantBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor)
    +                .containsExactly(new BigDecimal(-1.00));
         }
     
         private static BidRequest givenBidRequest(
    @@ -576,7 +688,6 @@ private static Imp givenImp(
     
             return impCustomizer.apply(Imp.builder()
                     .id("123")
    -                .banner(Banner.builder().build())
                     .ext(mapper.valueToTree(ExtPrebid.of(null,
                             extCustomizer.apply(ExtImpConversant.builder().siteId("site id")).build()))))
                     .build();
    @@ -584,6 +695,7 @@ private static Imp givenImp(
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/cpmstar/CpmStarBidderTest.java b/src/test/java/org/prebid/server/bidder/cpmstar/CpmStarBidderTest.java
    index d9ff6ba5f73..578d917525f 100644
    --- a/src/test/java/org/prebid/server/bidder/cpmstar/CpmStarBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/cpmstar/CpmStarBidderTest.java
    @@ -25,7 +25,6 @@
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -211,25 +210,6 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = cpmStarBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(cpmStarBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
    @@ -254,6 +234,7 @@ private static Imp givenImp(Function impCustomiz
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             final Bid bid = bidCustomizer.apply(Bid.builder()).build();
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder().bid(singletonList(bid))
                             .build()))
                     .build();
    diff --git a/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java b/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java
    new file mode 100644
    index 00000000000..0cb909a5833
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java
    @@ -0,0 +1,395 @@
    +package org.prebid.server.bidder.criteo;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Regs;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.request.User;
    +import com.iab.openrtb.response.Bid;
    +import io.vertx.core.MultiMap;
    +import org.assertj.core.api.Assertions;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.criteo.model.CriteoGdprConsent;
    +import org.prebid.server.bidder.criteo.model.CriteoPublisher;
    +import org.prebid.server.bidder.criteo.model.CriteoRequest;
    +import org.prebid.server.bidder.criteo.model.CriteoRequestSlot;
    +import org.prebid.server.bidder.criteo.model.CriteoResponse;
    +import org.prebid.server.bidder.criteo.model.CriteoResponseSlot;
    +import org.prebid.server.bidder.criteo.model.CriteoUser;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtImpCriteo;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    +import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    +import org.prebid.server.proto.openrtb.ext.response.BidType;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.math.BigDecimal;
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.tuple;
    +
    +public class CriteoBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.endpoint.com";
    +    private static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}"
    +            + "-[0-9a-fA-F]{12}";
    +
    +    private CriteoBidder criteoBidder;
    +
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
    +    @Before
    +    public void setUp() {
    +        criteoBidder = new CriteoBidder(ENDPOINT_URL, jacksonMapper, false);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        Assertions.assertThatIllegalArgumentException().isThrownBy(() ->
    +                new CriteoBidder("invalid_url", jacksonMapper, false));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestShouldThrowErrorIfImpsNetworkIdIsDifferent() {
    +        // given
    +        final BidRequest bidRequest =
    +                BidRequest.builder()
    +                        .imp(Arrays.asList(
    +                                Imp.builder()
    +                                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpCriteo.of(1, 1))))
    +                                        .build(),
    +                                Imp.builder()
    +                                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpCriteo.of(1, 2))))
    +                                        .build()))
    +                        .build();
    +
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsExactly(BidderError.badInput("Bid request has slots coming with several "
    +                        + "network IDs which is not allowed"));
    +        assertThat(result.getValue()).hasSize(0);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors())
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(error.getMessage()).startsWith("Cannot deserialize instance of");
    +                });
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldResolveSlotSizesFromBannerFormat() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(Banner.builder().format(singletonList(Format.builder()
    +                        .w(222)
    +                        .h(333)
    +                        .build()))
    +                        .build()));
    +
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(CriteoRequest::getSlots)
    +                .flatExtracting(CriteoRequestSlot::getSizes)
    +                .containsExactly("222x333");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldGenerateSlotIdIfGenerateIdPropertyIsTrue() {
    +        // given
    +        criteoBidder = new CriteoBidder(ENDPOINT_URL, jacksonMapper, true);
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(CriteoRequest::getSlots)
    +                .extracting(CriteoRequestSlot::getSlotId)
    +                .allSatisfy(slotId -> assertThat(slotId).matches(UUID_REGEX));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldBuildRequestWithCriteoPublisher() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(CriteoRequest::getPublisher)
    +                .containsExactly(CriteoPublisher.builder()
    +                        .siteId("siteId")
    +                        .url("www.criteo.com")
    +                        .networkId(1)
    +                        .build());
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateSpecificForAndroidDeviceId() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(givenImp(identity())))
    +                .device(Device.builder().os("android").build()).build();
    +
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(CriteoRequest::getUser)
    +                .extracting(CriteoUser::getDeviceOs, CriteoUser::getDeviceIdType)
    +                .containsExactly(tuple("android", "gaid"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateSpecificForUnknownDeviceId() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(givenImp(identity())))
    +                .device(Device.builder().os("somethingNew").build()).build();
    +
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(CriteoRequest::getUser)
    +                .extracting(CriteoUser::getDeviceOs, CriteoUser::getDeviceIdType)
    +                .containsExactly(tuple("somethingNew", "unknown"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldBuildRequestWithCriteoUser() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(CriteoRequest::getUser)
    +                .containsExactly(CriteoUser.builder()
    +                        .cookieId("buyerid")
    +                        .deviceId("ifa")
    +                        .deviceIdType("idfa")
    +                        .deviceOs("ios")
    +                        .ip("255.255.255.255")
    +                        .userAgent("userAgent")
    +                        .uspIab("1N--")
    +                        .build());
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldBuildRequestWithCriteoGdprConsent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(CriteoRequest::getGdprConsent)
    +                .containsExactly(CriteoGdprConsent.builder()
    +                        .consentData("consent")
    +                        .gdprApplies(true)
    +                        .build());
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetCookieUidHeader() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getHeaders)
    +                .flatExtracting(MultiMap::entries)
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .contains(tuple(HttpUtil.COOKIE_HEADER.toString(), "uid=buyerid"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetUserAgentAndForwarderForHeadersIfBidRequestDeviceIsNotNull() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = criteoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getHeaders)
    +                .flatExtracting(MultiMap::entries)
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .contains(
    +                        tuple(HttpUtil.USER_AGENT_HEADER.toString(), "userAgent"),
    +                        tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "255.255.255.255"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = HttpCall.success(
    +                HttpRequest.builder().payload(null).build(),
    +                HttpResponse.of(200, null, "invalid"),
    +                null);
    +
    +        // when
    +        final Result> result = criteoBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnCorrectBidderBids() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(identity());
    +
    +        // when
    +        final Result> result = criteoBidder.makeBids(httpCall, givenBidRequest(identity()));
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .contains(BidderBid.of(
    +                        Bid.builder()
    +                                .id("slot_id")
    +                                .impid("imp_id")
    +                                .price(BigDecimal.valueOf(0.05))
    +                                .adm("creative")
    +                                .w(300)
    +                                .h(300)
    +                                .crid("creative-id").build(), BidType.banner, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .id("bid-request-id")
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .user(User.builder().buyeruid("buyerid").ext(ExtUser.builder().consent("consent").build()).build())
    +                .device(Device.builder().os("ios").ifa("ifa").ip("255.255.255.255").ua("userAgent").build())
    +                .site(Site.builder().id("siteId").page("www.criteo.com").build())
    +                .regs(Regs.of(null, ExtRegs.of(1, "1N--")))
    +                .build();
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("imp_id")
    +                .banner(Banner.builder()
    +                        .id("banner_id")
    +                        .h(300)
    +                        .w(300)
    +                        .build()
    +                )
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpCriteo.of(1, 1)))))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(
    +            Function responseCustomizer) throws JsonProcessingException {
    +        final CriteoResponse.CriteoResponseBuilder responseBuilder =
    +                CriteoResponse.builder()
    +                        .id("response-id")
    +                        .slots(singletonList(
    +                                CriteoResponseSlot.builder()
    +                                        .arbitrageId("slot_id")
    +                                        .impId("imp_id")
    +                                        .width(300)
    +                                        .height(300)
    +                                        .networkId(1)
    +                                        .zoneId(1)
    +                                        .cpm(BigDecimal.valueOf(0.05))
    +                                        .creative("creative")
    +                                        .creativeCode("creative-id")
    +                                        .currency("USD")
    +                                        .build()));
    +
    +        final String body = mapper.writeValueAsString(
    +                responseCustomizer.apply(responseBuilder).build());
    +
    +        return HttpCall.success(
    +                HttpRequest.builder().build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/datablocks/DatablocksBidderTest.java b/src/test/java/org/prebid/server/bidder/datablocks/DatablocksBidderTest.java
    index a9fcc53e661..3c5eaee19f8 100644
    --- a/src/test/java/org/prebid/server/bidder/datablocks/DatablocksBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/datablocks/DatablocksBidderTest.java
    @@ -24,7 +24,6 @@
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    @@ -271,11 +270,6 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessi
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(datablocksBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(Object extImpDatablocks) {
             return BidRequest.builder()
                     .imp(singletonList(Imp.builder()
    diff --git a/src/test/java/org/prebid/server/bidder/decenterads/DecenteradsBidderTest.java b/src/test/java/org/prebid/server/bidder/decenterads/DecenteradsBidderTest.java
    new file mode 100644
    index 00000000000..79ff3cdc87a
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/decenterads/DecenteradsBidderTest.java
    @@ -0,0 +1,232 @@
    +package org.prebid.server.bidder.decenterads;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Audio;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.decenterads.ExtImpDecenterads;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +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;
    +
    +public class DecenteradsBidderTest extends VertxTest {
    +
    +    public static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private DecenteradsBidder decenteradsBidder;
    +
    +    @Before
    +    public void setUp() {
    +        decenteradsBidder = new DecenteradsBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestPerImp() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        Imp.builder()
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDecenterads.of("somePubId"))))
    +                                .build())));
    +
    +        // when
    +        final Result>> result = decenteradsBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(2)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .hasSize(2);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorOfEveryNotValidImp() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(Imp.builder()
    +                                .id("123")
    +                                .banner(Banner.builder().build())
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    +                                .build(),
    +                        givenImp(identity())))
    +                .build();
    +
    +        // when
    +        final Result>> result = decenteradsBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("bidder parameters required"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpExt() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = decenteradsBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(mapper.valueToTree(ExtImpDecenterads.of("somePubId")));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = decenteradsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = decenteradsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(givenImp(identity())))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = decenteradsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = decenteradsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnAudioBidIfAudioIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").audio(Audio.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = decenteradsBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer) {
    +        return givenBidRequest(impCustomizer, identity());
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .video(Video.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDecenterads.of("somePubId"))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/deepintent/DeepintentBidderTest.java b/src/test/java/org/prebid/server/bidder/deepintent/DeepintentBidderTest.java
    new file mode 100644
    index 00000000000..b10e41233d0
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/deepintent/DeepintentBidderTest.java
    @@ -0,0 +1,335 @@
    +package org.prebid.server.bidder.deepintent;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Native;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.deepintent.ExtImpDeepintent;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +
    +public class DeepintentBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.com/prebid/bid";
    +    private static final String DISPLAY_MANAGER = "di_prebid";
    +    private static final String DISPLAY_MANAGER_VERSION = "2.0.0";
    +    private static final String IMP_EXT_TAG_ID = "testTagID";
    +    private static final String CURRENCY = "USD";
    +
    +    private DeepintentBidder deepintentBidder;
    +
    +    @Before
    +    public void setUp() {
    +        deepintentBidder = new DeepintentBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new DeepintentBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .id("impId")
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +
    +        // when
    +        final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors()).allSatisfy(bidderError -> {
    +            assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
    +            assertThat(bidderError.getMessage()).isEqualTo("Impression id=impId, has invalid Ext");
    +        });
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpBannerIsNull() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.id("impId").banner(null));
    +
    +        // when
    +        final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors()).allSatisfy(bidderError -> {
    +            assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
    +            assertThat(bidderError.getMessage()).isEqualTo("We need a Banner Object in the request, imp : impId");
    +        });
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpBannerHasNoSizeParametersPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.id("impId").banner(Banner.builder().build()));
    +
    +        // when
    +        final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors()).allSatisfy(bidderError -> {
    +            assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
    +            assertThat(bidderError.getMessage()).isEqualTo("At least one size is required, imp : impId");
    +        });
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetMissedBannerSizeFromBannerFormat() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
    +                .banner(Banner.builder().format(singletonList(Format.builder().w(77).h(88).build())).build()));
    +
    +        // when
    +        final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(0);
    +        final Imp expectedImp = expectedImp(impBuilder -> impBuilder
    +                .banner(Banner.builder().w(77).h(88)
    +                        .format(singletonList(Format.builder().w(77).h(88).build()))
    +                        .build()));
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .containsExactly(expectedImp);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetDislplayManagerAndVersionAndTagIdToRequestImp() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(0);
    +        final Imp expectedImp = expectedImp(identity());
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .containsExactly(expectedImp);
    +    }
    +
    +    @Test
    +    public void makeRequestShouldCreateRequestForEveryValidImp() {
    +        // given
    +        final Imp firstImp = Imp.builder()
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of(IMP_EXT_TAG_ID))))
    +                .build();
    +        final Imp secondImp = Imp.builder()
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    +                .build();
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(Arrays.asList(firstImp, secondImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        final Imp expectedFirstImp = expectedImp(identity());
    +
    +        assertThat(result.getValue())
    +                .hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .containsExactly(expectedFirstImp);
    +    }
    +
    +    @Test
    +    public void makeRequestShouldCreateSeparateRequestForEveryImp() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of("firstImpTagId")))));
    +        final Imp secondImp = givenImp(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of("secondImpTagId")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(Arrays.asList(firstImp, secondImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = deepintentBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(0);
    +        final Imp expectedFirstImp =
    +                expectedImp(impBuilder ->
    +                        impBuilder.ext(mapper.valueToTree(
    +                                ExtPrebid.of(null, ExtImpDeepintent.of("firstImpTagId"))))
    +                                .tagid("firstImpTagId"));
    +
    +        final Imp expectedSecondImp =
    +                expectedImp(impBuilder ->
    +                        impBuilder.ext(mapper.valueToTree(
    +                                ExtPrebid.of(null, ExtImpDeepintent.of("secondImpTagId"))))
    +                                .tagid("secondImpTagId"));
    +
    +        assertThat(result.getValue())
    +                .hasSize(2)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .containsOnly(expectedFirstImp, expectedSecondImp);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = deepintentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors()).allSatisfy(error -> {
    +            assertThat(error.getMessage()).contains("Failed to decode: Unrecognized token");
    +            assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = deepintentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = deepintentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void shouldReturnErrorIfBidImpIdNotFoundInImps() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("notFoundId"))));
    +
    +        // when
    +        final Result> result = deepintentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badServerResponse("Failed to find impression with id: notFoundId"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = deepintentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, CURRENCY));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of(IMP_EXT_TAG_ID)))))
    +                .build();
    +    }
    +
    +    private Imp expectedImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .displaymanager(DISPLAY_MANAGER)
    +                .displaymanagerver(DISPLAY_MANAGER_VERSION)
    +                .tagid(IMP_EXT_TAG_ID)
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpDeepintent.of(IMP_EXT_TAG_ID)))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur(CURRENCY)
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/dmx/DmxBidderTest.java b/src/test/java/org/prebid/server/bidder/dmx/DmxBidderTest.java
    index 179b380399e..a8c9992dd9e 100644
    --- a/src/test/java/org/prebid/server/bidder/dmx/DmxBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/dmx/DmxBidderTest.java
    @@ -1,11 +1,15 @@
     package org.prebid.server.bidder.dmx;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.iab.openrtb.request.App;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
     import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Publisher;
    +import com.iab.openrtb.request.Site;
     import com.iab.openrtb.request.User;
     import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
    @@ -14,6 +18,7 @@
     import org.junit.Before;
     import org.junit.Test;
     import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.dmx.model.DmxPublisherExtId;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderError;
     import org.prebid.server.bidder.model.HttpCall;
    @@ -21,14 +26,16 @@
     import org.prebid.server.bidder.model.HttpResponse;
     import org.prebid.server.bidder.model.Result;
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtPublisher;
    +import org.prebid.server.proto.openrtb.ext.request.ExtUser;
     import org.prebid.server.proto.openrtb.ext.request.dmx.ExtImpDmx;
    -import org.prebid.server.proto.openrtb.ext.response.BidType;
     
    +import java.math.BigDecimal;
    +import java.util.Arrays;
     import java.util.List;
     import java.util.function.Function;
     
     import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -54,109 +61,148 @@ public void creationShouldFailOnInvalidEndpointUrl() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfUserOrAppIsAbsent() {
    +    public void makeHttpRequestsShouldReturnErrorWhenUserAndAppIsAbsent() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(emptyList())
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder.app(null).user(null), identity());
     
             // when
             final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("No user id or app id found. Could not send request to DMX."));
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("No user id or app id found. Could not send request to DMX."));
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfUserIdIsEmpty() {
    +    public void makeHttpRequestsShouldReturnErrorWhenRequestContainsNoIdentifierIdIsEmpty() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder()
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                ExtImpDmx.builder()
    -                                        .tagId("tagId")
    -                                        .dmxId("dmxId")
    -                                        .memberId("memberId")
    -                                        .publisherId("publisherId")
    -                                        .sellerId("sellerId")
    -                                        .build())))
    -                        .build()))
    -                .user(User.builder().build())
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder
    +                        .app(App.builder().id(null).build())
    +                        .device(Device.builder().ifa(null).build())
    +                        .user(User.builder().id(null).ext(ExtUser.builder().eids(emptyList()).build()).build()),
    +                identity());
     
             // when
             final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage())
    -                .startsWith("This request contained no identifier");
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("This request contained no identifier"));
         }
     
         @Test
    -    public void makeHttpRequestsShouldModifyImpIfBannerFormatIsNotEmpty() {
    +    public void makeHttpRequestsShouldNotReturnErrorWhenRequestContainsNoAppIdentifierButHaveUser() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(
    -                        Imp.builder()
    -                                .id("id")
    -                                .banner(Banner.builder()
    -                                        .format(singletonList(Format.builder().w(300).h(500).build()))
    -                                        .build())
    -                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                        ExtImpDmx.builder()
    -                                                .tagId("tagId")
    -                                                .dmxId("dmxId")
    -                                                .memberId("memberId")
    -                                                .publisherId("publisherId")
    -                                                .sellerId("sellerId")
    -                                                .build())))
    -                                .build()))
    -                .user(User.builder().id("userId").build())
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder
    +                        .app(App.builder().id(null).build())
    +                        .device(Device.builder().ifa(null).build())
    +                        .user(User.builder().id("uid").ext(ExtUser.builder().eids(emptyList()).build()).build()),
    +                identity());
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(2);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorWhenExtPublisherIdAndMemberIdAreBlank() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpDmx.builder()
    +                                .memberId("")
    +                                .publisherId("")
    +                                .build()))));
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Missing Params for auction to be send"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldModifyImpWhenBannerFormatIsNotEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
     
             // when
             final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        final JsonNode expectedImpExt = mapper.valueToTree(ExtPrebid.of(null,
    -                ExtImpDmx.builder()
    -                        .tagId("tagId")
    -                        .dmxId("dmxId")
    -                        .memberId("memberId")
    -                        .publisherId("publisherId")
    -                        .sellerId("sellerId")
    -                        .build()));
             assertThat(result.getValue()).hasSize(1)
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getId, Imp::getTagid, Imp::getExt, Imp::getSecure)
    -                .containsOnly(tuple("id", "dmxId", expectedImpExt, 1));
    +                .extracting(Imp::getTagid, Imp::getSecure, Imp::getBidfloor)
    +                .containsOnly(tuple("dmxId", 1, BigDecimal.ONE));
         }
     
         @Test
    -    public void makeHttpRequestsShouldSkipImpIfTagIdAndDmxIdAreBlank() {
    +    public void makeHttpRequestsShouldGetSizeForBannerFromFirstFormatIfAnyOfBannerSizesAreMissed() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(
    -                        Imp.builder()
    -                                .id("id")
    -                                .banner(Banner.builder()
    -                                        .format(singletonList(Format.builder().w(300).h(500).build()))
    -                                        .build())
    -                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                        ExtImpDmx.builder()
    -                                                .tagId("")
    -                                                .dmxId("")
    -                                                .memberId("memberId")
    -                                                .publisherId("publisherId")
    -                                                .sellerId("sellerId")
    -                                                .build())))
    -                                .build()))
    -                .user(User.builder().id("userId").build())
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .extracting(Banner::getH, Banner::getW)
    +                .containsOnly(tuple(500, 300));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldModifyImpWhenVideoIsNotEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder.banner(null).video(Video.builder().build()));
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).hasSize(1)
    +                .extracting(Imp::getTagid, Imp::getSecure, Imp::getBidfloor)
    +                .containsOnly(tuple("dmxId", 1, BigDecimal.ONE));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSkipImpWhenExtDmxAndExtTagIdAndImpTagIdIsEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpDmx.builder()
    +                                .tagId("")
    +                                .dmxId("")
    +                                .memberId("memberId")
    +                                .publisherId("publisherId")
    +                                .build()))));
     
             // when
             final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    @@ -170,44 +216,199 @@ public void makeHttpRequestsShouldSkipImpIfTagIdAndDmxIdAreBlank() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +    public void makeHttpRequestsShouldUpdateImpTagIdFromTagIdWhenExtTagIdIsPresentAndDmxIdIsBlank() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(
    -                        Imp.builder()
    -                                .banner(Banner.builder().build())
    -                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    -                                .build()))
    -                .user(User.builder().id("userId").build())
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpDmx.builder()
    +                                .tagId("tagId")
    +                                .dmxId("")
    +                                .memberId("memberId")
    +                                .publisherId("publisherId")
    +                                .build()))));
     
             // when
             final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(2);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid)
    +                .containsExactly("tagId");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpTagIdFromDmxIdWhenExtTagIdAndDmxIdIsPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpDmx.builder()
    +                                .tagId("tagId")
    +                                .dmxId("dmxId")
    +                                .memberId("memberId")
    +                                .publisherId("publisherId")
    +                                .build()))));
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid)
    +                .containsExactly("dmxId");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldEnrichVideoWithNeededProtocolsIfProtocolsAreMissed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.video(Video.builder().protocols(null).build()));
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getVideo)
    +                .flatExtracting(Video::getProtocols)
    +                .containsAll(Arrays.asList(2, 3, 5, 6, 7, 8));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateAppPublisherWhenAppAndExtImpPublisherIdIsPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getApp)
    +                .flatExtracting(App::getPublisher)
    +                .containsExactly(expectedPublisher("publisherId", true));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateAppPublisherWhenAppAndMemberIdPresentAndImpExtPubIdIsEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpDmx.builder()
    +                                .memberId("memberId")
    +                                .publisherId(null)
    +                                .build()))));
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getApp)
    +                .flatExtracting(App::getPublisher)
    +                .containsExactly(expectedPublisher("memberId", true));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReplaceAppIdWithDeviceIfa() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder
    +                        .app(App.builder().id(null).build())
    +                        .device(Device.builder().ifa("ifa").build()),
    +                identity());
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getApp)
    +                .flatExtracting(App::getId)
    +                .containsExactly("ifa");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateSitePublisherWhenSiteAndExtImpPublisherIdIsPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder.site(Site.builder().build()), identity());
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getSite)
    +                .flatExtracting(Site::getPublisher)
    +                .containsExactly(expectedPublisher("publisherId", false));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateSitePublisherWhenSiteAndMemberIdPresentAndImpExtPubIdIsEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.site(Site.builder().publisher(Publisher.builder().build()).build()),
    +                builder -> builder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpDmx.builder()
    +                                .memberId("memberId")
    +                                .publisherId(null)
    +                                .build()))));
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getSite)
    +                .flatExtracting(Site::getPublisher)
    +                .containsExactly(expectedPublisher("memberId", true));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateSitePublisherWithoutExtWhenSiteAndMemberIdPresentAndImpExtPubIdIsEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.site(Site.builder().build()),
    +                builder -> builder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpDmx.builder()
    +                                .memberId("memberId")
    +                                .publisherId(null)
    +                                .build()))));
    +
    +        // when
    +        final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getSite)
    +                .flatExtracting(Site::getPublisher)
    +                .containsExactly(expectedPublisher("memberId", false));
         }
     
         @Test
         public void makeHttpRequestsShouldCreateCorrectURL() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(
    -                        Imp.builder()
    -                                .id("123")
    -                                .banner(Banner.builder().id("banner_id").build())
    -                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                        ExtImpDmx.builder()
    -                                                .tagId("tagId")
    -                                                .dmxId("dmxId")
    -                                                .memberId("memberId")
    -                                                .publisherId("publisherId")
    -                                                .sellerId("sellerId")
    -                                                .build())))
    -                                .build()))
    -                .user(User.builder().id("userId").build())
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(identity());
     
             // when
             final Result>> result = dmxBidder.makeHttpRequests(bidRequest);
    @@ -234,11 +435,9 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
         }
     
         @Test
    -    public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingException {
    +    public void makeBidsShouldReturnCorrectBidderBidForVideo() throws JsonProcessingException {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(givenImp(identity())))
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(builder -> builder.banner(null).video(Video.builder().build()));
     
             final HttpCall httpCall = givenHttpCall(null,
                     mapper.writeValueAsString(
    @@ -252,23 +451,26 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio
             final Result> result = dmxBidder.makeBids(httpCall, bidRequest);
     
             // then
    -        final String adm = ""
    -                + "";
    -        final BidderBid expected = BidderBid.of(Bid.builder().impid("123").adm(adm).nurl("nurl").build(), BidType.video,
    +        final String adm = ""
    +                + ""
    +                + "";
    +        final BidderBid expectedBidderBid = BidderBid.of(Bid.builder()
    +                        .impid("123")
    +                        .adm(adm)
    +                        .nurl("nurl")
    +                        .build(),
    +                video,
                     "USD");
     
    -        assertThat(result.getValue().get(0).getBid().getAdm()).isEqualTo(adm);
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).doesNotContainNull()
    -                .hasSize(1).element(0).isEqualTo(expected);
    +        assertThat(result.getValue()).doesNotContainNull().hasSize(1).first().isEqualTo(expectedBidderBid);
         }
     
         @Test
         public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(null,
    -                mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
     
             // when
             final Result> result = dmxBidder.makeBids(httpCall,
    @@ -321,9 +523,8 @@ public void makeBidsShouldReturnResponseWithErrorWhenIdIsNotFound() throws JsonP
             assertThat(result.getValue()).isEmpty();
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(dmxBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
         }
     
         private static BidRequest givenBidRequest(
    @@ -331,15 +532,27 @@ private static BidRequest givenBidRequest(
                 Function impCustomizer) {
     
             return bidRequestCustomizer.apply(BidRequest.builder()
    +                .app(App.builder().id("appId").build())
                     .imp(singletonList(givenImp(impCustomizer))))
                     .build();
         }
     
    +    private static Publisher expectedPublisher(String extPublisherId, boolean isAddExt) {
    +        final DmxPublisherExtId dmxPublisherExtId = DmxPublisherExtId.of(extPublisherId);
    +        final ObjectNode encodedPublisherExt = mapper.valueToTree(dmxPublisherExtId);
    +        final ExtPublisher extPublisher = ExtPublisher.empty();
    +        extPublisher.addProperty("dmx", encodedPublisherExt);
    +
    +        return Publisher.builder()
    +                .id(extPublisherId)
    +                .ext(isAddExt ? extPublisher : null)
    +                .build();
    +    }
    +
         private static Imp givenImp(Function impCustomizer) {
             return impCustomizer.apply(Imp.builder()
                     .id("123")
    -                .banner(Banner.builder().id("banner_id").build())
    -                .video(Video.builder().build())
    +                .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(500).build())).build())
                     .ext(mapper.valueToTree(ExtPrebid.of(null,
                             ExtImpDmx.builder()
                                     .tagId("tagId")
    @@ -347,6 +560,7 @@ private static Imp givenImp(Function impCustomiz
                                     .memberId("memberId")
                                     .publisherId("publisherId")
                                     .sellerId("sellerId")
    +                                .bidFloor(BigDecimal.ONE)
                                     .build()))))
                     .build();
         }
    @@ -354,7 +568,8 @@ private static Imp givenImp(Function impCustomiz
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
                     .cur("USD")
    -                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
                     .build();
         }
    diff --git a/src/test/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidderTest.java
    index 898848465dd..3d2a3120a73 100644
    --- a/src/test/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/emxdigital/EmxDigitalBidderTest.java
    @@ -1,12 +1,14 @@
     package org.prebid.server.bidder.emxdigital;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.App;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.Device;
     import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Imp;
     import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
     import com.iab.openrtb.response.SeatBid;
    @@ -30,13 +32,13 @@
     import java.util.Map;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
     import static org.assertj.core.api.Assertions.tuple;
     import static org.assertj.core.api.Assertions.within;
     import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
     
     public class EmxDigitalBidderTest extends VertxTest {
     
    @@ -44,22 +46,6 @@ public class EmxDigitalBidderTest extends VertxTest {
     
         private EmxDigitalBidder emxDigitalBidder;
     
    -    private static BidResponse givenBidResponse(
    -            Function bidCustomizer) {
    -        return BidResponse.builder()
    -                .seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    -                        .build()))
    -                .build();
    -    }
    -
    -    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    -        return HttpCall.success(
    -                HttpRequest.builder().payload(bidRequest).build(),
    -                HttpResponse.of(200, null, body),
    -                null);
    -    }
    -
         @Before
         public void setUp() {
             emxDigitalBidder = new EmxDigitalBidder(ENDPOINT_URL, jacksonMapper);
    @@ -234,6 +220,7 @@ public void makeHttpRequestsShouldModifyImpWhenExtImpEmaDigitalContainsRequiredV
     
             // then
             final Imp expectedImp = Imp.builder()
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2"))))
                     .banner(Banner.builder().w(100).h(100).build())
                     .tagid("123")
                     .secure(1)
    @@ -248,6 +235,187 @@ public void makeHttpRequestsShouldModifyImpWhenExtImpEmaDigitalContainsRequiredV
                     .containsOnly(expectedImp);
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldRemoveVast40ProtocolFromVideo() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .video(Video.builder()
    +                                .mimes(Collections.singletonList("someMime"))
    +                                .protocols(Arrays.asList(1, 7, 2))
    +                                .w(100)
    +                                .h(100)
    +                                .build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2"))))
    +                        .build()))
    +                .tmax(1000L)
    +                .build();
    +
    +        // when
    +        final Result>> result = emxDigitalBidder
    +                .makeHttpRequests(bidRequest);
    +
    +        // then
    +        final Imp expectedImp = Imp.builder()
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2"))))
    +                .video(Video.builder()
    +                        .mimes(Collections.singletonList("someMime"))
    +                        .protocols(Arrays.asList(1, 2))
    +                        .w(100)
    +                        .h(100)
    +                        .build())
    +                .tagid("123")
    +                .secure(0)
    +                .bidfloor(new BigDecimal("2"))
    +                .bidfloorcur("USD")
    +                .build();
    +
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .containsOnly(expectedImp);
    +    }
    +
    +    @Test
    +    public void shouldThrowExceptionIfVideoDoNotHaveAtLeastOneSizeParameter() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .video(Video.builder().mimes(Collections.singletonList("someMime")).build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2"))))
    +                        .build()))
    +                .tmax(1000L)
    +                .build();
    +
    +        // when
    +        final Result>> result = emxDigitalBidder
    +                .makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsOnly(BidderError.badInput("Video: Need at least one size to build request"));
    +    }
    +
    +    @Test
    +    public void shouldThrowExceptionIfVideoDoNotHaveAnyMimeParameter() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .video(Video.builder().build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2"))))
    +                        .build()))
    +                .tmax(1000L)
    +                .build();
    +
    +        // when
    +        final Result>> result = emxDigitalBidder
    +                .makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsOnly(BidderError.badInput("Video: missing required field mimes"));
    +    }
    +
    +    @Test
    +    public void requestSecureShouldBeOneIfPageStartsWithHttps() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().w(100).h(100).build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2"))))
    +                        .build()))
    +                .tmax(1000L)
    +                .site(Site.builder().page("https://exmaple/").build())
    +                .build();
    +
    +        // when
    +        final Result>> result = emxDigitalBidder
    +                .makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(request -> request.getImp().get(0).getSecure())
    +                .containsOnly(1);
    +    }
    +
    +    @Test
    +    public void requestSecureShouldBeOneIfUrlStartsWithHttps() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().w(100).h(100).build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2"))))
    +                        .build()))
    +                .tmax(1000L)
    +                .app(App.builder().domain("https://exmaple/").build())
    +                .build();
    +
    +        // when
    +        final Result>> result = emxDigitalBidder
    +                .makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getSecure)
    +                .containsOnly(1);
    +    }
    +
    +    @Test
    +    public void requestSecureShouldBe1IfStoreUrlStartsWithHttps() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().w(100).h(100).build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2"))))
    +                        .build()))
    +                .tmax(1000L)
    +                .app(App.builder().storeurl("https://exmaple/").build())
    +                .build();
    +
    +        // when
    +        final Result>> result = emxDigitalBidder
    +                .makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getSecure)
    +                .containsOnly(1);
    +    }
    +
    +    @Test
    +    public void requestSecureShouldBe0IfPageDoNotStartsWithHttps() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().w(100).h(100).build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("123", "2"))))
    +                        .build()))
    +                .tmax(1000L)
    +                .site(Site.builder().page("http://exmaple/").build())
    +                .build();
    +
    +        // when
    +        final Result>> result = emxDigitalBidder
    +                .makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getSecure)
    +                .containsOnly(0);
    +    }
    +
         @Test
         public void makeHttpRequestsShouldModifyBannerFormatAndWidthAndHeightWhenRequestBannerWidthAndHeightIsNull() {
             // given
    @@ -272,6 +440,7 @@ public void makeHttpRequestsShouldModifyBannerFormatAndWidthAndHeightWhenRequest
                     .format(singletonList(Format.builder().h(30).w(31).build())).build();
     
             final Imp expectedImp = Imp.builder()
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("1", "asd"))))
                     .banner(expectedBanner)
                     .tagid("1")
                     .secure(0)
    @@ -327,43 +496,6 @@ public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() {
                             tuple("Accept-Language", "fr"));
         }
     
    -    @Test
    -    public void makeHttpRequestsShouldSendRequestToTestUrlWithHeadersWhenTestIsOne() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder()
    -                        .banner(Banner.builder().w(1).h(1).build())
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpEmxDigital.of("1", "asd"))))
    -                        .build()))
    -                .device(Device.builder().ip("ip").ua("Agent").language("fr").dnt(1).build())
    -                .site(Site.builder().page("myPage").build())
    -                .test(1)
    -                .tmax(1000L)
    -                .build();
    -
    -        // when
    -        final Result>> result = emxDigitalBidder
    -                .makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(HttpRequest::getUri)
    -                .allSatisfy(uri -> assertThat(uri).isEqualTo("https://test.endpoint.com?t=1000&ts=2060541160"));
    -
    -        assertThat(result.getValue()).hasSize(1)
    -                .flatExtracting(r -> r.getHeaders().entries())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(
    -                        tuple("Content-Type", "application/json;charset=utf-8"),
    -                        tuple("Accept", "application/json"),
    -                        tuple("User-Agent", "Agent"),
    -                        tuple("X-Forwarded-For", "ip"),
    -                        tuple("Referer", "myPage"),
    -                        tuple("DNT", "1"),
    -                        tuple("Accept-Language", "fr"));
    -    }
    -
         @Test
         public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() {
             // given
    @@ -412,7 +544,7 @@ public void makeBidsShouldReturnEmptyListWhenBidResponseSeatBidIsNull()
         }
     
         @Test
    -    public void makeBidsShouldAlwaysReturnBannerBidWithChangedBidImpId() throws JsonProcessingException {
    +    public void makeBidsShouldReturnBannerBidWithChangedBidImpId() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(
                     BidRequest.builder()
    @@ -431,9 +563,61 @@ public void makeBidsShouldAlwaysReturnBannerBidWithChangedBidImpId() throws Json
         }
     
         @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(emxDigitalBidder.extractTargeting(mapper.createObjectNode()))
    -                .isEqualTo(emptyMap());
    +    public void makeBidsShouldReturnVideoBidIfAdmContainsVastPrefix() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.id("321").adm("> result = emxDigitalBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().id("321").adm(" httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.id("321").adm("> result = emxDigitalBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().id("321").adm(" bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
         }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +
     }
     
    diff --git a/src/test/java/org/prebid/server/bidder/engagebdr/EngagebdrBidderTest.java b/src/test/java/org/prebid/server/bidder/engagebdr/EngagebdrBidderTest.java
    index e60534c3e0c..61e4834bef5 100644
    --- a/src/test/java/org/prebid/server/bidder/engagebdr/EngagebdrBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/engagebdr/EngagebdrBidderTest.java
    @@ -25,7 +25,6 @@
     import java.util.List;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -230,11 +229,6 @@ public void makeBidsShouldReturnVideoWhenVideoProvided() throws JsonProcessingEx
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(engagebdrBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(Function impCustomizer) {
             return givenBidRequest(identity(), impCustomizer);
         }
    @@ -256,6 +250,7 @@ private static Imp givenImp(Function impCustomiz
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java b/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java
    index 693ecdc692a..3d4cefc9984 100644
    --- a/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java
    @@ -5,6 +5,7 @@
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Imp;
     import com.iab.openrtb.request.Site;
     import com.iab.openrtb.request.User;
    @@ -34,6 +35,7 @@
     import java.util.Map;
     import java.util.function.Function;
     
    +import static java.util.Arrays.asList;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -107,6 +109,26 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtClientIdIsBlank() {
             assertThat(result.getValue()).isEmpty();
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfEndpointUrlComposingFails() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                requestBuilder -> requestBuilder
    +                        .site(Site.builder().domain("invalid domain").build()),
    +                identity());
    +
    +        // when
    +        final Result>> result = eplanningBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getMessage())
    +                            .startsWith("Invalid url: https://eplanning.com/clientId/1/invalid domain/ROS");
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +                });
    +    }
    +
         @Test
         public void makeHttpRequestsShouldSendSingleGetRequestWithNullBody() {
             // given
    @@ -117,13 +139,13 @@ public void makeHttpRequestsShouldSendSingleGetRequestWithNullBody() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(HttpRequest::getBody)
    +        assertThat(result.getValue())
                     .hasSize(1)
    +                .extracting(HttpRequest::getBody)
                     .containsNull();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getMethod)
    -                .containsOnly(HttpMethod.GET);
    +                .containsExactly(HttpMethod.GET);
         }
     
         @Test
    @@ -136,11 +158,11 @@ public void makeHttpRequestsShouldSetCorrectHeaders() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getHeaders)
                     .flatExtracting(MultiMap::entries)
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(
    +                .containsExactly(
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
         }
    @@ -184,9 +206,9 @@ public void makeHttpRequestsShouldSetCorrectUriWithDefaults() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getUri)
    -                .containsOnly(
    +                .containsExactly(
                             "https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A1x1");
         }
     
    @@ -203,9 +225,9 @@ public void makeHttpRequestsShouldSetCorrectUriWithSitePageAndDomain() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getUri)
    -                .containsOnly(
    +                .containsExactly(
                             "https://eplanning.com/clientId/1/DOMAIN/ROS?r=pbs&ncb=1&ur=https%3A%2F%2Fwww.example.com&e="
                                     + "testadun_itco_de%3A1x1");
         }
    @@ -223,14 +245,14 @@ public void makeHttpRequestsShouldSetCorrectUriIfSiteDomainIsBlank() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getUri)
    -                .containsOnly("https://eplanning.com/clientId/1/www.example.com/ROS?r=pbs&ncb=1"
    +                .containsExactly("https://eplanning.com/clientId/1/www.example.com/ROS?r=pbs&ncb=1"
                             + "&ur=https%3A%2F%2Fwww.example.com&e=testadun_itco_de%3A1x1");
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetCorrectUriWithSizeString() {
    +    public void makeHttpRequestsShouldSetCorrectUriWithSizeStringFromBannerWAndH() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     impBuilder -> impBuilder
    @@ -244,12 +266,100 @@ public void makeHttpRequestsShouldSetCorrectUriWithSizeString() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getUri)
    -                .containsOnly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A"
    +                .containsExactly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A"
                             + "300x200");
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldSetCorrectUriWithSizeStringFromFormatForMobile() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.device(Device.builder().devicetype(1).build()),
    +                impBuilder -> impBuilder
    +                        .banner(Banner.builder()
    +                                .format(asList(Format.builder().w(300).h(50).build(),
    +                                        Format.builder().w(320).h(50).build()))
    +                                .build()));
    +
    +        // when
    +        final Result>> result = eplanningBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A"
    +                        + "320x50");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetCorrectUriWithSizeStringFromFormatForDesktop() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.device(Device.builder().devicetype(2).build()),
    +                impBuilder -> impBuilder
    +                        .banner(Banner.builder()
    +                                .format(asList(Format.builder().w(300).h(600).build(),
    +                                        Format.builder().w(728).h(90).build()))
    +                                .build()));
    +
    +        // when
    +        final Result>> result = eplanningBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A"
    +                        + "728x90");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldTolerateAndDropInvalidFormats() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.device(Device.builder().devicetype(2).build()),
    +                impBuilder -> impBuilder
    +                        .banner(Banner.builder()
    +                                .format(asList(Format.builder().w(null).h(600).build(),
    +                                        Format.builder().w(728).h(90).build()))
    +                                .build()));
    +
    +        // when
    +        final Result>> result = eplanningBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A"
    +                        + "728x90");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetUriWithSize1x1WhenSizeWasNotFoundInPriority() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.device(Device.builder().devicetype(2).build()),
    +                impBuilder -> impBuilder
    +                        .banner(Banner.builder()
    +                                .format(asList(Format.builder().w(301).h(600).build(),
    +                                        Format.builder().w(729).h(90).build()))
    +                                .build()));
    +
    +        // when
    +        final Result>> result = eplanningBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A"
    +                        + "1x1");
    +    }
    +
         @Test
         public void makeHttpRequestsShouldSetCorrectUriWithUserId() {
             // given
    @@ -263,9 +373,9 @@ public void makeHttpRequestsShouldSetCorrectUriWithUserId() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getUri)
    -                .containsOnly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A"
    +                .containsExactly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A"
                             + "1x1&uid=Buyer-ID");
         }
     
    @@ -282,10 +392,11 @@ public void makeHttpRequestsShouldSetCorrectUriWithDeviceIp() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getUri)
    -                .containsOnly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A1x1"
    -                        + "&ip=123.321.321.123");
    +                .containsExactly(
    +                        "https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de%3A1x1"
    +                                + "&ip=123.321.321.123");
         }
     
         @Test
    @@ -302,9 +413,9 @@ public void makeHttpRequestsShouldSetCorrectUriWithApp() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getUri)
    -                .containsOnly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&e=testadun_itco_de%3A1x1&"
    +                .containsExactly("https://eplanning.com/clientId/1/FILE/ROS?r=pbs&ncb=1&e=testadun_itco_de%3A1x1&"
                             + "appn=appName&appid=id&ifa=ifa&app=1");
         }
     
    @@ -433,12 +544,6 @@ public void makeBidsShouldNotCrashIfThereAreNoAds() throws JsonProcessingExcepti
             assertThat(result.getValue()).isEmpty();
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        // given, when and then
    -        assertThat(eplanningBidder.extractTargeting(mapper.createObjectNode())).isEmpty();
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
    diff --git a/src/test/java/org/prebid/server/bidder/epom/EpomBidderTest.java b/src/test/java/org/prebid/server/bidder/epom/EpomBidderTest.java
    new file mode 100644
    index 00000000000..f2b2d777405
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/epom/EpomBidderTest.java
    @@ -0,0 +1,291 @@
    +package org.prebid.server.bidder.epom;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Native;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class EpomBidderTest extends VertxTest {
    +
    +    public static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private EpomBidder epomBidder;
    +
    +    @Before
    +    public void setUp() {
    +        epomBidder = new EpomBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new EpomBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestWithAllImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        givenImp(identity()))));
    +
    +        // when
    +        final Result>> result = epomBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .hasSize(2);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfDeviceNotPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createObjectNode())))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = epomBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("ipv4 address is required field"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfDeviceIpNotPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().build())
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createObjectNode())))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = epomBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("ipv4 address is required field"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = epomBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = epomBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = epomBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("125"))));
    +
    +        // when
    +        final Result> result = epomBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("125").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = epomBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = epomBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNoBannerAndVideoButNativeIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().xNative(Native.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = epomBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(givenImp(identity())))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = epomBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = epomBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .device(Device.builder().ip("123.123.123.123").build())
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createObjectNode())))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/evolution/EvolutionBidderTest.java b/src/test/java/org/prebid/server/bidder/evolution/EvolutionBidderTest.java
    new file mode 100644
    index 00000000000..ff1322b9591
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/evolution/EvolutionBidderTest.java
    @@ -0,0 +1,174 @@
    +package org.prebid.server.bidder.evolution;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.response.BidType;
    +
    +import java.util.List;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.emptyList;
    +import static java.util.Collections.singletonList;
    +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;
    +
    +public class EvolutionBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "http://service.e-volution.ai/pbserver";
    +
    +    private EvolutionBidder evolutionBidder;
    +
    +    @Before
    +    public void setUp() {
    +        evolutionBidder = new EvolutionBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(
    +                () -> new EvolutionBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnOnlyOneRequestForAllImps() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(
    +                        Imp.builder().id("123").banner(Banner.builder().build()).build(),
    +                        Imp.builder().id("456").video(Video.builder().build()).build()))
    +                .build();
    +
    +        // when
    +        final Result>> requests = evolutionBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(requests.getErrors()).isEmpty();
    +        assertThat(requests.getValue()).hasSize(1);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = evolutionBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = evolutionBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty seatbid"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = evolutionBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty seatbid"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidResponseSeatBidIsEmpty() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().seatbid(emptyList()).build()));
    +
    +        // when
    +        final Result> result = evolutionBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty seatbid"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBidsWithValidTypes() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(
    +                                givenBid("123", banner),
    +                                givenBid("345", video),
    +                                givenBid("456", audio),
    +                                givenBid("567", xNative),
    +                                givenBid("789", "invalid_type"))));
    +
    +        // when
    +        final Result> result = evolutionBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(
    +                        BidderBid.of(givenBid("123", banner), banner, null),
    +                        BidderBid.of(givenBid("345", video), video, null),
    +                        BidderBid.of(givenBid("456", audio), audio, null),
    +                        BidderBid.of(givenBid("567", xNative), xNative, null),
    +                        BidderBid.of(givenBid("789", "invalid_type"), banner, null));
    +    }
    +
    +    private static BidResponse givenBidResponse(Bid... bids) {
    +        return BidResponse.builder()
    +                .seatbid(singletonList(SeatBid.builder().bid(asList(bids)).build()))
    +                .build();
    +    }
    +
    +    private static Bid givenBid(String impid, BidType bidType) {
    +        return Bid.builder().impid(impid).ext(
    +                mapper.createObjectNode().put("mediaType", bidType.getName())).build();
    +    }
    +
    +    private static Bid givenBid(String impid, String bidType) {
    +        return Bid.builder().impid(impid).ext(mapper.createObjectNode().put("mediaType", bidType)).build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/facebook/FacebookBidderTest.java b/src/test/java/org/prebid/server/bidder/facebook/FacebookBidderTest.java
    index dd1c7234d89..dcd1de2803c 100644
    --- a/src/test/java/org/prebid/server/bidder/facebook/FacebookBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/facebook/FacebookBidderTest.java
    @@ -48,28 +48,33 @@ public class FacebookBidderTest extends VertxTest {
         private static final String PLATFORM_ID = "101";
         private static final String APP_SECRET = "6237";
         private static final String DEFAULT_BID_CURRENCY = "USD";
    +    public static final String TIMEOUT_NOTIFICATION_URL_TEMPLATE = "https://url/?p=%s&a=%s&auction=%s&ortb_loss_code=2";
     
         private FacebookBidder facebookBidder;
     
         @Before
         public void setUp() {
    -        facebookBidder = new FacebookBidder(ENDPOINT_URL, PLATFORM_ID, APP_SECRET, jacksonMapper);
    +        facebookBidder = new FacebookBidder(
    +                ENDPOINT_URL, PLATFORM_ID, APP_SECRET, TIMEOUT_NOTIFICATION_URL_TEMPLATE, jacksonMapper);
         }
     
         @Test
         public void creationShouldFailOnBlankArguments() {
             assertThatIllegalArgumentException().isThrownBy(
    -                () -> new FacebookBidder(ENDPOINT_URL, " ", APP_SECRET, jacksonMapper))
    +                () -> new FacebookBidder(
    +                        ENDPOINT_URL, " ", APP_SECRET, TIMEOUT_NOTIFICATION_URL_TEMPLATE, jacksonMapper))
                     .withMessageStartingWith("No facebook platform-id specified.");
             assertThatIllegalArgumentException().isThrownBy(
    -                () -> new FacebookBidder(ENDPOINT_URL, PLATFORM_ID, " ", jacksonMapper))
    +                () -> new FacebookBidder(
    +                        ENDPOINT_URL, PLATFORM_ID, " ", TIMEOUT_NOTIFICATION_URL_TEMPLATE, jacksonMapper))
                     .withMessageStartingWith("No facebook app-secret specified.");
         }
     
         @Test
         public void creationShouldFailOnInvalidEndpoints() {
             assertThatIllegalArgumentException()
    -                .isThrownBy(() -> new FacebookBidder("invalid_url", PLATFORM_ID, APP_SECRET, jacksonMapper))
    +                .isThrownBy(() -> new FacebookBidder(
    +                        "invalid_url", PLATFORM_ID, APP_SECRET, TIMEOUT_NOTIFICATION_URL_TEMPLATE, jacksonMapper))
                     .withMessage("URL supplied is not valid: invalid_url");
         }
     
    @@ -687,6 +692,24 @@ public void makeBidsShouldReturnAudioBid() throws JsonProcessingException {
                     .containsOnly(BidType.audio);
         }
     
    +    @Test
    +    public void makeTimeoutNotificationShouldGenerateRequest() throws JsonProcessingException {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity(), identity()).toBuilder()
    +                .app(App.builder().publisher(Publisher.builder().id("test").build()).build())
    +                .build();
    +        final HttpRequest httpRequest = HttpRequest.builder()
    +                .body(mapper.writeValueAsString(bidRequest))
    +                .payload(bidRequest)
    +                .build();
    +
    +        // when
    +        final HttpRequest notification = facebookBidder.makeTimeoutNotification(httpRequest);
    +
    +        // then
    +        assertThat(notification.getUri()).isEqualTo("https://url/?p=101&a=test&auction=req1&ortb_loss_code=2");
    +    }
    +
         private static BidRequest givenBidRequest(
                 Function impCustomizer,
                 Function impExtCustomizer,
    @@ -719,6 +742,7 @@ private static HttpCall givenHttpCall(String body) {
     
         private static HttpCall givenHttpCall(Bid... bids) throws JsonProcessingException {
             return givenHttpCall(mapper.writeValueAsString(BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(asList(bids))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/gamma/GammaBidderTest.java b/src/test/java/org/prebid/server/bidder/gamma/GammaBidderTest.java
    index 594d22367e1..d6069fcd6fe 100644
    --- a/src/test/java/org/prebid/server/bidder/gamma/GammaBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/gamma/GammaBidderTest.java
    @@ -16,6 +16,8 @@
     import org.junit.Test;
     import org.prebid.server.VertxTest;
     import org.prebid.server.bidder.gamma.model.GammaBid;
    +import org.prebid.server.bidder.gamma.model.GammaBidResponse;
    +import org.prebid.server.bidder.gamma.model.GammaSeatBid;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderError;
     import org.prebid.server.bidder.model.HttpCall;
    @@ -30,7 +32,6 @@
     import java.util.function.Function;
     
     import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -232,20 +233,6 @@ public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() {
             assertThat(result.getValue()).isEmpty();
         }
     
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = gammaBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
         @Test
         public void makeBidsShouldSetAdmFromVastXmlIsPresentAndVideoType() throws JsonProcessingException {
             // given
    @@ -253,12 +240,14 @@ public void makeBidsShouldSetAdmFromVastXmlIsPresentAndVideoType() throws JsonPr
             final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build();
     
             final String adm = "ADM";
    -        final GammaBid bid = GammaBid.builder().id("impId").vastXml(adm).build();
    +        final Bid bid = Bid.builder().id("impId").build();
    +        final GammaBid gammaBid = GammaBid.builder().bid(bid).vastXml(adm).build();
             final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(
    -                BidResponse.builder()
    +                GammaBidResponse.builder()
                             .id("impId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .bid(singletonList(bid))
    +                        .cur("USD")
    +                        .seatbid(singletonList(GammaSeatBid.builder()
    +                                .bid(singletonList(gammaBid))
                                     .build()))
                             .build()));
     
    @@ -280,12 +269,14 @@ public void makeBidsShouldSetAdmFromVastXmlAndNurlFromVastUrlAndVideoType() thro
     
             final String adm = "ADM";
             final String nurl = "NURL";
    -        final GammaBid bid = GammaBid.builder().id("impId").vastXml(adm).vastUrl(nurl).build();
    +        final Bid bid = Bid.builder().id("impId").build();
    +        final GammaBid gammaBid = GammaBid.builder().bid(bid).vastXml(adm).vastUrl(nurl).build();
             final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(
    -                BidResponse.builder()
    +                GammaBidResponse.builder()
                             .id("impId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .bid(singletonList(bid))
    +                        .cur("USD")
    +                        .seatbid(singletonList(GammaSeatBid.builder()
    +                                .bid(singletonList(gammaBid))
                                     .build()))
                             .build()));
     
    @@ -328,6 +319,7 @@ public void makeBidsShouldReturnErrorWhenNoAdmAndNotVideoType() throws JsonProce
             final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(
                     BidResponse.builder()
                             .id("id")
    +                        .cur("USD")
                             .seatbid(singletonList(SeatBid.builder()
                                     .bid(singletonList(Bid.builder().build()))
                                     .build()))
    @@ -350,6 +342,7 @@ public void makeBidsShouldReturnBannerWhenNoProvided() throws JsonProcessingExce
             final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(
                     BidResponse.builder()
                             .id("id")
    +                        .cur("USD")
                             .seatbid(singletonList(SeatBid.builder()
                                     .bid(singletonList(bid))
                                     .build()))
    @@ -361,16 +354,11 @@ public void makeBidsShouldReturnBannerWhenNoProvided() throws JsonProcessingExce
             // then
             assertThat(result.getErrors()).isEmpty();
     
    -        final GammaBid expectedBid = GammaBid.builder().adm("ADM").build();
    +        final Bid expectedBid = Bid.builder().adm("ADM").build();
             assertThat(result.getValue())
                     .containsOnly(BidderBid.of(expectedBid, banner, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(gammaBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function impCustomizer) {
             return givenBidRequest(identity(), impCustomizer);
    diff --git a/src/test/java/org/prebid/server/bidder/gamoshi/GamoshiBidderTest.java b/src/test/java/org/prebid/server/bidder/gamoshi/GamoshiBidderTest.java
    index 8129edd448d..3d8ebf3b452 100644
    --- a/src/test/java/org/prebid/server/bidder/gamoshi/GamoshiBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/gamoshi/GamoshiBidderTest.java
    @@ -32,7 +32,6 @@
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -148,7 +147,7 @@ public void makeHttpRequestsShouldSetExpectedRequestUrlAndDefaultHeaders() {
                     .containsOnly("https://test.endpoint.com/r/supply/bidr?bidder=prebid-server");
             assertThat(result.getValue().get(0).getHeaders()).isNotNull()
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple("x-openrtb-version", "2.4"),
    +                .containsOnly(tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.4"),
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
         }
    @@ -302,11 +301,6 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws Js
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(gamoshiBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
    @@ -331,6 +325,7 @@ private static Imp givenImp(Function impCustomiz
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/grid/GridBidderTest.java b/src/test/java/org/prebid/server/bidder/grid/GridBidderTest.java
    index a843e233b9c..cbfa1d50094 100644
    --- a/src/test/java/org/prebid/server/bidder/grid/GridBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/grid/GridBidderTest.java
    @@ -12,6 +12,9 @@
     import org.junit.Test;
     import org.prebid.server.VertxTest;
     import org.prebid.server.bidder.grid.model.ExtImpGrid;
    +import org.prebid.server.bidder.grid.model.GridExtImp;
    +import org.prebid.server.bidder.grid.model.GridExtImpData;
    +import org.prebid.server.bidder.grid.model.GridExtImpDataAdServer;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderError;
     import org.prebid.server.bidder.model.HttpCall;
    @@ -23,7 +26,6 @@
     import java.util.List;
     import java.util.function.UnaryOperator;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.UnaryOperator.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -233,74 +235,26 @@ public void makeBidsShouldReturnErrorIfImpIdsFromBidAndRequestWereNotMatchedAndI
         }
     
         @Test
    -    public void makeBidsShouldReturnBidsWithCurrencyFromResponse() throws JsonProcessingException {
    +    public void modifyImpShouldChangeImpExt() {
             // given
    -        final HttpCall httpCall = givenHttpCall(
    -                BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
    -                        .cur(singletonList("NZD"))
    -                        .build(),
    -                mapper.writeValueAsString(givenBidResponse(bidResponseBuilder -> bidResponseBuilder.cur("JPY"),
    -                        bidBuilder -> bidBuilder.impid("123"))));
    -
    -        // when
    -        final Result> result = gridBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue())
    -                .extracting(BidderBid::getBidCurrency)
    -                .containsOnly("JPY");
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnBidsWithCurrencyFromRequest() throws JsonProcessingException {
    -        // given
    -        final HttpCall httpCall = givenHttpCall(
    -                BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
    -                        .cur(singletonList("JPY"))
    -                        .build(),
    -                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    -
    -        // when
    -        final Result> result = gridBidder.makeBids(httpCall, null);
    +        final GridExtImp gridExtImp = GridExtImp.builder()
    +                .data(GridExtImpData.of(null, GridExtImpDataAdServer.of("name", "adslot")))
    +                .build();
     
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue())
    -                .extracting(BidderBid::getBidCurrency)
    -                .containsOnly("JPY");
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnBidsWithDefaultCurrencyIfResponseAndRequestCurrenciesNotDefined()
    -            throws JsonProcessingException {
    -        // given
    -        final HttpCall httpCall = givenHttpCall(
    -                BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
    -                        .build(),
    -                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +        final Imp imp = Imp.builder().ext(mapper.valueToTree(gridExtImp)).build();
     
             // when
    -        final Result> result = gridBidder.makeBids(httpCall, null);
    +        final Imp modifiedImp = gridBidder.modifyImp(imp, ExtImpGrid.of(1));
     
             // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue())
    -                .extracting(BidderBid::getBidCurrency)
    -                .containsOnly("USD");
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(gridBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +        assertThat(mapper.convertValue(modifiedImp.getExt(), GridExtImp.class).getGpid())
    +                .isEqualTo(gridExtImp.getData().getAdServer().getAdSlot());
         }
     
         private static BidResponse givenBidResponse(UnaryOperator bidResponseCustomizer,
                                                     UnaryOperator bidCustomizer) {
             return bidResponseCustomizer.apply(BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build())))
    diff --git a/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java b/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java
    index 4430ffa394b..831ac28a777 100644
    --- a/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java
    @@ -1,12 +1,12 @@
     package org.prebid.server.bidder.gumgum;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.iab.openrtb.request.Audio;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.Format;
     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;
     import com.iab.openrtb.response.Bid;
    @@ -23,13 +23,14 @@
     import org.prebid.server.bidder.model.Result;
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.request.gumgum.ExtImpGumgum;
    +import org.prebid.server.proto.openrtb.ext.request.gumgum.ExtImpGumgumVideo;
     
     import java.math.BigDecimal;
    +import java.math.BigInteger;
     import java.util.List;
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -55,7 +56,7 @@ public void creationShouldFailOnInvalidEndpointUrl() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +    public void makeHttpRequestsShouldReturnErrorsIfImpExtCouldNotBeParsed() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     impBuilder -> impBuilder
    @@ -65,8 +66,15 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             final Result>> result = gumgumBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(2);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getErrors()).hasSize(2)
    +                .anySatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(error.getMessage()).isEqualTo("No valid impressions");
    +                })
    +                .anySatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(error.getMessage()).startsWith("Cannot deserialize instance");
    +                });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -74,17 +82,16 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
         public void makeHttpRequestsShouldReturnErrorIfNoValidImpressions() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(
    -                        givenImp(impBuilder -> impBuilder.banner(null).audio(Audio.builder().build())),
    -                        givenImp(impBuilder -> impBuilder.banner(null).xNative(Native.builder().build()))))
    +                .imp(singletonList(
    +                        givenImp(impBuilder -> impBuilder.video(Video.builder().build()))))
                     .build();
     
             // when
             final Result>> result = gumgumBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("No valid impressions"));
    +        assertThat(result.getErrors()).hasSize(2)
    +                .contains(BidderError.badInput("No valid impressions"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -94,7 +101,7 @@ public void makeHttpRequestsShouldReturnErrorIfVideoFieldsAreNotValid() {
             final BidRequest bidRequest = BidRequest.builder()
                     .imp(singletonList(Imp.builder()
                             .video(Video.builder().w(0).build())
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpGumgum.of("zone"))))
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpGumgum.of("zone", BigInteger.TEN, "irisId"))))
                             .build()))
                     .build();
     
    @@ -102,32 +109,41 @@ public void makeHttpRequestsShouldReturnErrorIfVideoFieldsAreNotValid() {
             final Result>> result = gumgumBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(2);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Invalid or missing video field(s)");
    -        assertThat(result.getErrors().get(1).getMessage()).startsWith("No valid impressions");
    +        assertThat(result.getErrors())
    +                .containsExactlyInAnyOrder(BidderError.badInput("Invalid or missing video field(s)"),
    +                        BidderError.badInput("No valid impressions"));
             assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    -    public void makeHttpRequestsShouldSkipImpressionsWithoutBannerOrVideo() {
    +    public void makeHttpRequestsShouldModifyVideoExtOfIrisIdIsPresent() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(
    -                        givenImp(impBuilder -> impBuilder.banner(null).video(null).audio(Audio.builder().build())),
    -                        givenImp(identity())))
    +                .imp(singletonList(Imp.builder()
    +                        .video(Video.builder()
    +                                .w(20)
    +                                .h(30)
    +                                .maxduration(12)
    +                                .minduration(34)
    +                                .placement(33)
    +                                .linearity(233)
    +                                .build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpGumgum.of("zone", BigInteger.TEN, "irisId"))))
    +                        .build()))
                     .build();
     
             // when
             final Result>> result = gumgumBidder.makeHttpRequests(bidRequest);
     
             // then
    +        final ObjectNode expectedVideoExt = mapper.valueToTree(ExtImpGumgumVideo.of("irisId"));
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .extracting(Banner::getId)
    -                .containsOnly("banner_id");
    +                .extracting(Imp::getVideo)
    +                .extracting(Video::getExt)
    +                .containsExactly(expectedVideoExt);
         }
     
         @Test
    @@ -145,34 +161,54 @@ public void makeHttpRequestsShouldNotChangeBannerWidthAndHeightIfPresent() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getBanner)
                     .extracting(Banner::getW, Banner::getH)
    -                .containsOnly(tuple(600, 900));
    +                .containsExactly(tuple(600, 900));
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetBannerWidthAndHeightFromfirstFormatIfAbsent() {
    +    public void makeHttpRequestsShouldUpdatePublisherIfPubIdIsPresent() {
             // given
    +        final Publisher sitePublisher = Publisher.builder().name("testPublisher").build();
             final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder().w(300).h(450).build()))
    -                                .build()));
    +                bidRequestBuilder -> bidRequestBuilder.site(Site.builder().publisher(sitePublisher).build()),
    +                impBuilder -> impBuilder.banner(Banner.builder().w(600).h(900).build()));
     
             // when
             final Result>> result = gumgumBidder.makeHttpRequests(bidRequest);
     
             // then
    +        final Publisher expectedPublisher = sitePublisher.toBuilder().id("10").build();
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPublisher)
    +                .containsExactly(expectedPublisher);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetBannerWidthAndHeightFromfirstFormatIfAbsent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(Banner.builder()
    +                        .format(singletonList(Format.builder().w(300).h(450).build()))
    +                        .build()));
    +
    +        // when
    +        final Result>> result = gumgumBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getBanner)
                     .extracting(Banner::getW, Banner::getH)
    -                .containsOnly(tuple(300, 450));
    +                .containsExactly(tuple(300, 450));
         }
     
         @Test
    @@ -186,7 +222,7 @@ public void makeHttpRequestsShouldNotMakeSiteIfAbsent() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .extracting(BidRequest::getSite)
                     .containsNull();
         }
    @@ -199,12 +235,9 @@ public void makeHttpRequestsShouldSetSiteIdFromLastValidImpExtZone() {
                     .imp(asList(
                             givenImp(impBuilder -> impBuilder
                                     .banner(Banner.builder().build())
    -                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpGumgum.of("ignored zone"))))),
    -                        givenImp(identity()),
    -                        givenImp(impBuilder -> impBuilder
    -                                .banner(null)
    -                                .audio(Audio.builder().build())
    -                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpGumgum.of("invalid imp")))))))
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                        ExtImpGumgum.of("ignored zone", BigInteger.TEN, "irisId"))))),
    +                        givenImp(identity())))
                     .build();
     
             // when
    @@ -212,11 +245,11 @@ public void makeHttpRequestsShouldSetSiteIdFromLastValidImpExtZone() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
                     .extracting(BidRequest::getSite)
                     .extracting(Site::getId)
    -                .containsOnly("zone");
    +                .containsExactly("zone");
         }
     
         @Test
    @@ -228,9 +261,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             final Result> result = gumgumBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -285,7 +320,7 @@ public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
                     .build();
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(expectedBid, video, "USD"));
    +                .containsExactly(BidderBid.of(expectedBid, video, "USD"));
         }
     
         @Test
    @@ -303,12 +338,31 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(gumgumBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +    public void makeBidsShouldTolerateWithNullSeatOrBidValues() throws JsonProcessingException {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
    +                .build();
    +
    +        final BidResponse bidResponse = BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(asList(SeatBid.builder()
    +                        .bid(asList(Bid.builder().id("123").build(), null))
    +                        .build(), null))
    +                .build();
    +
    +        final HttpCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse));
    +
    +        // when
    +        final Result> result = gumgumBidder.makeBids(httpCall, bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
         }
     
         private static BidRequest givenBidRequest(
    @@ -328,7 +382,7 @@ private static Imp givenImp(Function impCustomiz
             return impCustomizer.apply(Imp.builder()
                     .id("123")
                     .banner(Banner.builder().id("banner_id").build())
    -                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpGumgum.of("zone")))))
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpGumgum.of("zone", BigInteger.TEN, "irisId")))))
                     .build();
         }
     
    diff --git a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java
    index 642d34dd8d4..97358df6662 100644
    --- a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java
    @@ -1,8 +1,11 @@
     package org.prebid.server.bidder.improvedigital;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    +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.Video;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
     import com.iab.openrtb.response.SeatBid;
    @@ -20,11 +23,13 @@
     import java.util.List;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
    +import static java.util.Arrays.asList;
     import static java.util.Collections.singletonList;
     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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
     
     public class ImprovedigitalBidderTest extends VertxTest {
     
    @@ -58,8 +63,8 @@ public void makeHttpRequestsShouldNotModifyIncomingRequest() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .containsOnly(bidRequest);
    +                .extracting(HttpRequest::getPayload)
    +                .containsExactly(bidRequest);
         }
     
         @Test
    @@ -106,7 +111,28 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso
         }
     
         @Test
    -    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
    +    public void makeBidsShouldReturnErrorWhenSeatBidsCountIsMoreThanOne() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().seatbid(asList(SeatBid.builder()
    +                                .bid(singletonList(Bid.builder().build()))
    +                                .build(),
    +                        SeatBid.builder()
    +                                .bid(singletonList(Bid.builder().build()))
    +                                .build()
    +                )).build()));
    +
    +        // when
    +        final Result> result = improvedigitalBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsExactly(BidderError.badServerResponse("Unexpected SeatBid! Must be only one but have: 2"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBannerOrVideoNotPresent() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(
                     BidRequest.builder()
    @@ -118,19 +144,91 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessi
             // when
             final Result> result = improvedigitalBidder.makeBids(httpCall, null);
     
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badServerResponse("Unknown impression type for ID: \"123\""));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = improvedigitalBidder.makeBids(httpCall, null);
    +
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(improvedigitalBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = improvedigitalBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().xNative(Native.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = improvedigitalBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfImpNotFoundForId() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("456"))));
    +
    +        // when
    +        final Result> result = improvedigitalBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badServerResponse("Failed to find impression for ID: \"456\""));
    +        assertThat(result.getValue()).isEmpty();
         }
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java b/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java
    index 6c6bf8b2871..37bc15e0ef0 100644
    --- a/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java
    @@ -3,6 +3,7 @@
     import com.fasterxml.jackson.core.JsonProcessingException;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Imp;
     import com.iab.openrtb.request.Native;
     import com.iab.openrtb.request.Video;
    @@ -21,6 +22,8 @@
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.request.inmobi.ExtImpInmobi;
     
    +import java.util.Arrays;
    +import java.util.Collections;
     import java.util.List;
     import java.util.function.Function;
     
    @@ -34,6 +37,9 @@
     public class InmobiBidderTest extends VertxTest {
     
         private static final String ENDPOINT_URL = "https://test";
    +    private static final String IMP_ID = "123";
    +    private static final int FORMAT_W = 35;
    +    private static final int FORMAT_H = 37;
     
         private InmobiBidder inmobiBidder;
     
    @@ -48,34 +54,104 @@ public void creationShouldFailOnInvalidEndpointUrl() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed2() {
    +    public void makeHttpRequestsShouldReturnErrorIfPlcAttributeIsNotPresent() {
             // given
             final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder
    -                        .id("123")
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpInmobi.of("")))));
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpInmobi.of("   ")))));
             // when
             final Result>> result = inmobiBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage())
    -                .startsWith("'plc' is a required attribute for InMobi's bidder ext");
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("'plc' is a required attribute for InMobi's bidder ext"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfPlcAttributeIsNull() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpInmobi.of(null)))));
    +        // when
    +        final Result>> result = inmobiBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("'plc' is a required attribute for InMobi's bidder ext"));
         }
     
         @Test
         public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             // given
             final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder
    -                        .id("123")
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = inmobiBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("bad InMobi bidder ext"));
    +    }
    +
    +    @Test
    +    public void shouldSetBannerFormatWAndHValuesToBannerIfTheyAreNotPresentInBanner() {
    +        // given
    +        final Format bannerFormat = Format.builder().w(FORMAT_W).h(FORMAT_H).build();
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(Banner.builder()
    +                        .format(Collections.singletonList(bannerFormat))
    +                        .build()));
    +        // when
    +        final Result>> result = inmobiBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(0);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .containsExactly(Banner.builder()
    +                        .w(FORMAT_W)
    +                        .h(FORMAT_H)
    +                        .format(Collections.singletonList(bannerFormat))
    +                        .build());
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateOnlyFirstImpression() {
    +        // given
    +        final Format bannerFormat = Format.builder().w(FORMAT_W).h(FORMAT_H).build();
    +        final Imp firstImp = Imp.builder()
    +                .banner(Banner.builder().id("firstBanner").format(Collections.singletonList(bannerFormat)).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpInmobi.of("plc"))))
    +                .build();
    +        final Imp secondImp = Imp.builder()
    +                .banner(Banner.builder().id("secondBanner").format(Collections.singletonList(bannerFormat)).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpInmobi.of("plc"))))
    +                .build();
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(Arrays.asList(firstImp, secondImp))
    +                .build();
             // when
             final Result>> result = inmobiBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("bad InMobi bidder ext");
    +        final Banner firstExpectedBanner = Banner.builder()
    +                .id("firstBanner")
    +                .w(FORMAT_W)
    +                .h(FORMAT_H)
    +                .format(Collections.singletonList(bannerFormat))
    +                .build();
    +        final Banner secondExpectedBanner = Banner.builder()
    +                .id("secondBanner")
    +                .format(Collections.singletonList(bannerFormat))
    +                .build();
    +        assertThat(result.getErrors()).hasSize(0);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .containsExactly(firstExpectedBanner, secondExpectedBanner);
         }
     
         @Test
    @@ -87,9 +163,9 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             final Result> result = inmobiBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getErrors())
    +                .allMatch(error -> error.getMessage().startsWith("Failed to decode: Unrecognized token")
    +                        && error.getType().equals(BidderError.Type.bad_server_response));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -126,10 +202,10 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws
             // given
             final HttpCall httpCall = givenHttpCall(
                     BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .imp(singletonList(Imp.builder().id(IMP_ID).banner(Banner.builder().build()).build()))
                             .build(),
                     mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID))));
     
             // when
             final Result> result = inmobiBidder.makeBids(httpCall, null);
    @@ -137,17 +213,17 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +                .containsExactly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), banner, null));
         }
     
         @Test
         public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .imp(singletonList(Imp.builder().id(IMP_ID).video(Video.builder().build()).build()))
                             .build(),
                     mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID))));
     
             // when
             final Result> result = inmobiBidder.makeBids(httpCall, null);
    @@ -155,7 +231,7 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws Js
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, null));
    +                .containsExactly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), video, null));
         }
     
         @Test
    @@ -163,10 +239,10 @@ public void makeBidsShouldReturnBannerBidIfNativeIsPresentInRequestImp() throws
             // given
             final HttpCall httpCall = givenHttpCall(
                     BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .imp(singletonList(Imp.builder().id(IMP_ID).xNative(Native.builder().build()).build()))
                             .build(),
                     mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID))));
     
             // when
             final Result> result = inmobiBidder.makeBids(httpCall, null);
    @@ -174,22 +250,7 @@ public void makeBidsShouldReturnBannerBidIfNativeIsPresentInRequestImp() throws
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = inmobiBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    +                .containsExactly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), banner, null));
         }
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
    @@ -221,9 +282,9 @@ private static BidRequest givenBidRequest(
     
         private static Imp givenImp(Function impCustomizer) {
             return impCustomizer.apply(Imp.builder()
    -                .id("123")
    -                .banner(Banner.builder().id("banner_id").build()).ext(mapper.valueToTree(ExtPrebid.of(null,
    -                        ExtImpInmobi.of("plc")))))
    +                .id(IMP_ID)
    +                .banner(Banner.builder().id("bannerId").build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpInmobi.of("plc")))))
                     .build();
         }
     }
    diff --git a/src/test/java/org/prebid/server/bidder/interactiveoffers/InteractiveOffersBidderTest.java b/src/test/java/org/prebid/server/bidder/interactiveoffers/InteractiveOffersBidderTest.java
    new file mode 100644
    index 00000000000..e3fe5842d76
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/interactiveoffers/InteractiveOffersBidderTest.java
    @@ -0,0 +1,225 @@
    +package org.prebid.server.bidder.interactiveoffers;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.interactiveoffers.ExtImpInteractiveoffers;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +
    +public class InteractiveOffersBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.endpoint.com/";
    +
    +    private InteractiveOffersBidder interactiveOffersBidder;
    +
    +    @Before
    +    public void setup() {
    +        interactiveOffersBidder = new InteractiveOffersBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException()
    +                .isThrownBy(() -> new InteractiveOffersBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectPayload() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = interactiveOffersBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .containsExactly(bidRequest);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestWithAllImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        givenImp(identity()))));
    +
    +        // when
    +        final Result>> result = interactiveOffersBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .hasSize(2);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpExt() throws JsonProcessingException {
    +        // given
    +        final ObjectNode interactiveOffersExt = (ObjectNode) mapper.readTree("{\"bidder\":{\"partnerId\":\"abc123\"}}");
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpInteractiveoffers.of("abc123"))))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = interactiveOffersBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(interactiveOffersExt);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = interactiveOffersBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = interactiveOffersBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = interactiveOffersBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = interactiveOffersBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldThrowExceptionIfBannerIsNotPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().w(1).h(1).build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = interactiveOffersBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(BidderBid::getType)
    +                .containsExactly(banner);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer) {
    +        return givenBidRequest(impCustomizer, identity());
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpInteractiveoffers.of("35"))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java
    new file mode 100644
    index 00000000000..b85ca9d7c61
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/invibes/InvibesBidderTest.java
    @@ -0,0 +1,335 @@
    +package org.prebid.server.bidder.invibes;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.request.User;
    +import com.iab.openrtb.response.Bid;
    +import org.apache.commons.lang3.StringUtils;
    +import org.assertj.core.api.Assertions;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.invibes.model.InvibesBidParams;
    +import org.prebid.server.bidder.invibes.model.InvibesBidRequest;
    +import org.prebid.server.bidder.invibes.model.InvibesBidderResponse;
    +import org.prebid.server.bidder.invibes.model.InvibesPlacementProperty;
    +import org.prebid.server.bidder.invibes.model.InvibesTypedBid;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.invibes.ExtImpInvibes;
    +import org.prebid.server.proto.openrtb.ext.request.invibes.model.InvibesDebug;
    +
    +import java.util.Arrays;
    +import java.util.Collections;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
    +
    +public class InvibesBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://{{Host}}/test";
    +    private static final int BANNER_H = 12;
    +    private static final int BANNER_W = 15;
    +    private static final String PAGE_URL = "www.test.com";
    +    private static final int SECOND_BANNER_H = 23;
    +    private static final int SECOND_BANNER_W = 24;
    +    private static final String FIRST_PLACEMENT_ID = "12";
    +    private static final String SECOND_PLACEMENT_ID = "15";
    +    private static final int DEVICE_W = 77;
    +    private static final int DEVICE_H = 88;
    +    private static final String BUYER_UID = "someUid";
    +    private static final String IMP_ID = "123";
    +    private static final String CURRENCY = "EUR";
    +    private static final String BID_VERSION = "4";
    +
    +    private InvibesBidder invibesBidder;
    +
    +    @Before
    +    public void setUp() {
    +        invibesBidder = new InvibesBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        Assertions.assertThatIllegalArgumentException().isThrownBy(() ->
    +                new InvibesBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                impBuilder -> impBuilder.banner(Banner.builder().h(BANNER_H).w(BANNER_W).build()),
    +                ExtImpInvibes.of("12", 1003, InvibesDebug.of("test", true)));
    +
    +        // when
    +        final Result>> result = invibesBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getValue().get(0).getUri()).isEqualTo("https://bid3.videostep.com/test");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .site(Site.builder().page(PAGE_URL).build())
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = invibesBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Error parsing invibesExt parameters");
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorWhenBannerIsNull() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.site(Site.builder().page(PAGE_URL).build()),
    +                impBuilder -> impBuilder.id(IMP_ID).banner(null));
    +
    +        // when
    +        final Result>> result =
    +                invibesBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsOnly(
    +                        BidderError.badInput(String.format("Banner not specified in impression with id: %s", IMP_ID)));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorWhenSiteIsNotPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity(),
    +                impBuilder -> impBuilder.banner(Banner.builder().h(BANNER_H).w(BANNER_W).build()));
    +
    +        // when
    +        final Result>> result = invibesBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsOnly(BidderError.badInput("Site not specified"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void shouldCreateRequestWithDataFromEveryImpression() throws JsonProcessingException {
    +        // given
    +        final List imps = Arrays.asList(givenImp(
    +                impBuilder -> impBuilder
    +                        .banner(Banner.builder().h(BANNER_H).w(BANNER_W).build()),
    +                ExtImpInvibes.of(FIRST_PLACEMENT_ID, 15, InvibesDebug.of("test1", true))),
    +                givenImp(impBuilder -> impBuilder
    +                                .banner(Banner.builder().h(SECOND_BANNER_H).w(SECOND_BANNER_W).build()),
    +                        ExtImpInvibes.of(SECOND_PLACEMENT_ID, 1001, InvibesDebug.of("test2", false))));
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .site(Site.builder().page(PAGE_URL).build())
    +                .imp(imps)
    +                .build();
    +
    +        // when
    +        final Result>> result = invibesBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        final Format firstExpectedFormat = Format.builder().w(BANNER_W).h(BANNER_H).build();
    +        final Format secondExpectedFormat = Format.builder().w(SECOND_BANNER_W).h(SECOND_BANNER_H).build();
    +        final Map bidProperties = new HashMap<>();
    +        bidProperties.put(FIRST_PLACEMENT_ID, InvibesPlacementProperty.builder()
    +                .formats(Collections.singletonList(firstExpectedFormat))
    +                .build());
    +        bidProperties.put(SECOND_PLACEMENT_ID, InvibesPlacementProperty.builder()
    +                .formats(Collections.singletonList(secondExpectedFormat))
    +                .build());
    +
    +        InvibesBidParams expectedBidParams = InvibesBidParams.builder()
    +                .placementIds(Arrays.asList(FIRST_PLACEMENT_ID, SECOND_PLACEMENT_ID))
    +                .bidVersion(BID_VERSION)
    +                .properties(bidProperties)
    +                .build();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(invibesBidRequest ->
    +                        mapper.readValue(invibesBidRequest.getBidParamsJson(), InvibesBidParams.class))
    +                .containsOnly(expectedBidParams);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateInvibesBidRequestWithCorrectParams() throws JsonProcessingException {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder
    +                        .device(Device.builder().w(DEVICE_W).h(DEVICE_H).build())
    +                        .user(User.builder().buyeruid(BUYER_UID).build())
    +                        .site(Site.builder().page(PAGE_URL).build()),
    +                impBuilder -> impBuilder.banner(Banner.builder().h(BANNER_H).w(BANNER_W).build()));
    +
    +        // when
    +        final Result>> result = invibesBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final Map properties = new HashMap<>();
    +        properties.put(FIRST_PLACEMENT_ID, InvibesPlacementProperty.builder()
    +                .formats(Collections.singletonList(Format.builder().w(BANNER_W).h(BANNER_H).build())).build());
    +
    +        final InvibesBidParams invibesBidParams = InvibesBidParams.builder()
    +                .placementIds(Collections.singletonList(FIRST_PLACEMENT_ID))
    +                .bidVersion(BID_VERSION)
    +                .properties(properties)
    +                .build();
    +
    +        final InvibesBidRequest expectedRequest = InvibesBidRequest.builder()
    +                .bidParamsJson(mapper.writeValueAsString(invibesBidParams))
    +                .isTestBid(true)
    +                .location(PAGE_URL)
    +                .gdpr(true)
    +                .gdprConsent(StringUtils.EMPTY)
    +                .invibBVLog(true)
    +                .videoAdDebug(true)
    +                .lid(BUYER_UID)
    +                .bvid("test")
    +                .width(String.valueOf(DEVICE_W))
    +                .height(String.valueOf(DEVICE_H))
    +                .build();
    +
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .containsOnly(expectedRequest);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = invibesBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListWhenBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = invibesBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                InvibesBidRequest.builder().build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID))));
    +
    +        // when
    +        final Result> result = invibesBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), banner, CURRENCY));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIdBidResponseContainsError() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                InvibesBidRequest.builder().build(),
    +                mapper.writeValueAsString(InvibesBidderResponse.builder().error("someError").build()));
    +
    +        // when
    +        final Result> result = invibesBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .extracting(BidderError::getMessage)
    +                .containsOnly("Server error: someError.");
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    private static InvibesBidderResponse givenBidResponse(Function bidCustomizer) {
    +        return InvibesBidderResponse.builder()
    +                .typedBids(singletonList(InvibesTypedBid.builder()
    +                        .bid(bidCustomizer.apply(Bid.builder()).build())
    +                        .dealPriority(12)
    +                        .build()))
    +                .currency(CURRENCY)
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(InvibesBidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer,
    +            ExtImpInvibes extImpInvibes) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .site(Site.builder().page(PAGE_URL).build())
    +                .imp(singletonList(givenImp(impCustomizer, extImpInvibes))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(
    +                        givenImp(impCustomizer, ExtImpInvibes.of("12", 15,
    +                                InvibesDebug.of("test", true))))))
    +                .build();
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer,
    +                                ExtImpInvibes extImpInvibes) {
    +        return impCustomizer.apply(Imp.builder()
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, extImpInvibes))))
    +                .build();
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/ix/IxAdapterTest.java b/src/test/java/org/prebid/server/bidder/ix/IxAdapterTest.java
    deleted file mode 100644
    index 4832fe493c2..00000000000
    --- a/src/test/java/org/prebid/server/bidder/ix/IxAdapterTest.java
    +++ /dev/null
    @@ -1,515 +0,0 @@
    -package org.prebid.server.bidder.ix;
    -
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.Banner;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.BidRequest.BidRequestBuilder;
    -import com.iab.openrtb.request.Device;
    -import com.iab.openrtb.request.Format;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Publisher;
    -import com.iab.openrtb.request.Regs;
    -import com.iab.openrtb.request.Site;
    -import com.iab.openrtb.request.Source;
    -import com.iab.openrtb.request.User;
    -import com.iab.openrtb.response.Bid;
    -import com.iab.openrtb.response.BidResponse;
    -import com.iab.openrtb.response.BidResponse.BidResponseBuilder;
    -import com.iab.openrtb.response.SeatBid;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.auction.model.AdUnitBid.AdUnitBidBuilder;
    -import org.prebid.server.auction.model.AdapterRequest;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    -import org.prebid.server.auction.model.PreBidRequestContext.PreBidRequestContextBuilder;
    -import org.prebid.server.bidder.ix.proto.IxParams;
    -import org.prebid.server.bidder.model.AdapterHttpRequest;
    -import org.prebid.server.bidder.model.ExchangeCall;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.request.PreBidRequest;
    -import org.prebid.server.proto.request.PreBidRequest.PreBidRequestBuilder;
    -import org.prebid.server.proto.request.Video;
    -import org.prebid.server.proto.response.BidderDebug;
    -import org.prebid.server.proto.response.MediaType;
    -
    -import java.math.BigDecimal;
    -import java.util.ArrayList;
    -import java.util.EnumSet;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.function.Function;
    -import java.util.stream.Collectors;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptySet;
    -import static java.util.Collections.singleton;
    -import static java.util.Collections.singletonList;
    -import static java.util.function.Function.identity;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
    -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    -import static org.assertj.core.api.Assertions.assertThatThrownBy;
    -import static org.assertj.core.api.Assertions.tuple;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -
    -public class IxAdapterTest extends VertxTest {
    -
    -    private static final String BIDDER = "ix";
    -    private static final String COOKIE_FAMILY = BIDDER;
    -    private static final String ENDPOINT_URL = "http://exchange.org/";
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private UidsCookie uidsCookie;
    -
    -    private AdapterRequest adapterRequest;
    -    private PreBidRequestContext preBidRequestContext;
    -    private ExchangeCall exchangeCall;
    -    private IxAdapter adapter;
    -
    -    @Before
    -    public void setUp() {
    -        adapterRequest = givenBidder(identity());
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -        exchangeCall = givenExchangeCall(identity(), identity());
    -        adapter = new IxAdapter(COOKIE_FAMILY, ENDPOINT_URL, jacksonMapper);
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException()
    -                .isThrownBy(() -> new IxAdapter(COOKIE_FAMILY, "invalid_url", jacksonMapper))
    -                .withMessage("URL supplied is not valid: invalid_url");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedHeaders() {
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).flatExtracting(r -> r.getHeaders().entries())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple("Content-Type", "application/json;charset=utf-8"),
    -                        tuple("Accept", "application/json"));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAppIsPresentInPreBidRequest() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder
    -                .app(App.builder().build()));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("ix doesn't support apps");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfParamsMissingInAtLeastOneAdUnitBid() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(identity()),
    -                givenAdUnitBid(builder -> builder.params(null))));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("ix params section is missing");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamPublisherIdIsMissing() {
    -        // given
    -        adapterRequest = givenBidder(builder -> builder.params(mapper.valueToTree(IxParams.of(null))));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Missing siteId param");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .mediaTypes(emptySet()))));
    -
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid ad unit/imp");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldIgnoreVideoMediaTypeAndFail() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .mediaTypes(singleton(MediaType.video))
    -                        .video(Video.builder()
    -                                .mimes(emptyList())
    -                                .build()))));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid ad unit/imp");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder
    -                        .bidderCode(BIDDER)
    -                        .adUnitCode("adUnitCode1")
    -                        .instl(1)
    -                        .topframe(1)
    -                        .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                        .params(mapper.valueToTree(IxParams.of("486"))));
    -
    -        preBidRequestContext = givenPreBidRequestContext(
    -                builder -> builder
    -                        .referer("http://www.example.com")
    -                        .domain("example.com")
    -                        .ip("192.168.144.1")
    -                        .ua("userAgent1"),
    -                builder -> builder
    -                        .timeoutMillis(1500L)
    -                        .tid("tid1")
    -                        .user(User.builder()
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null))));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUid1");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .containsOnly(BidRequest.builder()
    -                        .id("tid1")
    -                        .at(1)
    -                        .tmax(1500L)
    -                        .imp(singletonList(Imp.builder()
    -                                .id("adUnitCode1")
    -                                .instl(1)
    -                                .tagid("adUnitCode1")
    -                                .banner(Banner.builder()
    -                                        .w(300)
    -                                        .h(250)
    -                                        .topframe(1)
    -                                        .format(singletonList(Format.builder()
    -                                                .w(300)
    -                                                .h(250)
    -                                                .build()))
    -                                        .build())
    -                                .build()))
    -                        .site(Site.builder()
    -                                .domain("example.com")
    -                                .page("http://www.example.com")
    -                                .publisher(Publisher.builder().id("486").build())
    -                                .build())
    -                        .device(Device.builder()
    -                                .ua("userAgent1")
    -                                .ip("192.168.144.1")
    -                                .build())
    -                        .user(User.builder()
    -                                .buyeruid("buyerUid1")
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null)))
    -                        .source(Source.builder()
    -                                .fd(1)
    -                                .tid("tid1")
    -                                .build())
    -                        .build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithOneRequestPerAdUnit() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1")),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(2)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(2)
    -                .extracting(Imp::getId).containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithOneRequestPerAdUnitsSize() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .sizes(asList(Format.builder().w(300).h(250).build(),
    -                                Format.builder().w(300).h(300).build()))
    -                )));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(2)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .containsOnly(Format.builder().w(300).h(250).build(),
    -                        Format.builder().w(300).h(300).build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldPrioritizeSlotsOverSizes() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .sizes(asList(Format.builder().w(300).h(250).build(),
    -                                Format.builder().w(300).h(300).build()))),
    -                givenAdUnitBid(builder -> builder
    -                        .adUnitCode("adUnitCode2")
    -                        .sizes(singletonList(Format.builder().w(600).h(480).build())))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(3)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .flatExtracting(Banner::getFormat)
    -                .containsOnly(Format.builder().w(300).h(250).build(),
    -                        Format.builder().w(600).h(480).build(),
    -                        Format.builder().w(300).h(300).build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithLimitedAmountOfRequests() {
    -        // given
    -        final List adUnitBids = new ArrayList<>();
    -        for (int i = 0; i < 21; i++) {
    -            final String id = String.valueOf(i);
    -            adUnitBids.add(givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode" + id)));
    -        }
    -        adapterRequest = AdapterRequest.of(BIDDER, adUnitBids);
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(20);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfResultingRequestsAreEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, emptyList());
    -
    -        // when and then
    -        assertThatExceptionOfType(PreBidException.class)
    -                .isThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .withMessage("Invalid ad unit/imp");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldFailIfBidImpIdDoesNotMatchAdUnitCode() {
    -        // given
    -        adapterRequest = givenBidder(builder -> builder.adUnitCode("adUnitCode"));
    -
    -        exchangeCall = givenExchangeCall(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("anotherAdUnitCode").build()))
    -                        .build())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Unknown ad unit code 'anotherAdUnitCode'");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnOneRequestWithBannerIfAdUnitContainsBannerAndVideoMediaTypes() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                        .mediaTypes(EnumSet.of(MediaType.video, MediaType.banner))
    -                        .video(Video.builder()
    -                                .mimes(singletonList("Mime1"))
    -                                .playbackMethod(1)
    -                                .build()))));
    -
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(1)
    -                .extracting(imp -> imp.getVideo() == null, imp -> imp.getBanner() == null)
    -                .containsOnly(tuple(true, false));
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnMultipleBidBuildersIfMultipleAdUnitsInPreBidRequestAndBidsInResponse() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1")),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"))));
    -
    -        exchangeCall = givenExchangeCall(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(asList(Bid.builder().impid("adUnitCode1").build(),
    -                                        Bid.builder().impid("adUnitCode2").build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(2)
    -                .extracting(org.prebid.server.proto.response.Bid::getCode)
    -                .containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"));
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .price(new BigDecimal("8.43"))
    -                                        .adm("adm")
    -                                        .crid("crid")
    -                                        .w(300)
    -                                        .h(250)
    -                                        .dealid("dealId")
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids)
    -                .containsExactly(org.prebid.server.proto.response.Bid.builder()
    -                        .code("adUnitCode")
    -                        .price(new BigDecimal("8.43"))
    -                        .adm("adm")
    -                        .creativeId("crid")
    -                        .width(300)
    -                        .height(250)
    -                        .bidder(BIDDER)
    -                        .bidId("bidId")
    -                        .dealId("dealId")
    -                        .mediaType(MediaType.banner)
    -                        .build());
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnEmptyBidsIfEmptyOrNullBidResponse() {
    -        // given
    -        adapterRequest = givenBidder(identity());
    -
    -        exchangeCall = givenExchangeCall(identity(), br -> br.seatbid(null));
    -
    -        // when and then
    -        assertThat(adapter.extractBids(adapterRequest, exchangeCall)).isEmpty();
    -        assertThat(adapter.extractBids(adapterRequest, ExchangeCall.empty(null))).isEmpty();
    -    }
    -
    -    private static AdapterRequest givenBidder(Function adUnitBidBuilderCustomizer) {
    -        return AdapterRequest.of(BIDDER, singletonList(givenAdUnitBid(adUnitBidBuilderCustomizer)));
    -    }
    -
    -    private static AdUnitBid givenAdUnitBid(Function adUnitBidBuilderCustomizer) {
    -        final AdUnitBidBuilder adUnitBidBuilderMinimal = AdUnitBid.builder()
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .params(mapper.valueToTree(IxParams.of("42")))
    -                .mediaTypes(singleton(MediaType.banner));
    -
    -        final AdUnitBidBuilder adUnitBidBuilderCustomized = adUnitBidBuilderCustomizer
    -                .apply(adUnitBidBuilderMinimal);
    -
    -        return adUnitBidBuilderCustomized.build();
    -    }
    -
    -    private PreBidRequestContext givenPreBidRequestContext(
    -            Function preBidRequestContextBuilderCustomizer,
    -            Function preBidRequestBuilderCustomizer) {
    -
    -        final PreBidRequestBuilder preBidRequestBuilderMinimal = PreBidRequest.builder()
    -                .accountId("accountId");
    -        final PreBidRequest preBidRequest = preBidRequestBuilderCustomizer.apply(preBidRequestBuilderMinimal).build();
    -
    -        final PreBidRequestContextBuilder preBidRequestContextBuilderMinimal =
    -                PreBidRequestContext.builder()
    -                        .preBidRequest(preBidRequest)
    -                        .uidsCookie(uidsCookie);
    -        return preBidRequestContextBuilderCustomizer.apply(preBidRequestContextBuilderMinimal).build();
    -    }
    -
    -    private static ExchangeCall givenExchangeCall(
    -            Function bidRequestBuilderCustomizer,
    -            Function bidResponseBuilderCustomizer) {
    -
    -        final BidRequestBuilder bidRequestBuilderMinimal = BidRequest.builder();
    -        final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(bidRequestBuilderMinimal).build();
    -
    -        final BidResponseBuilder bidResponseBuilderMinimal = BidResponse.builder();
    -        final BidResponse bidResponse = bidResponseBuilderCustomizer.apply(bidResponseBuilderMinimal).build();
    -
    -        return ExchangeCall.success(bidRequest, bidResponse, BidderDebug.builder().build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java b/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java
    index f4108617fe4..1c6b01e7603 100644
    --- a/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java
    @@ -1,20 +1,26 @@
     package org.prebid.server.bidder.ix;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.iab.openrtb.request.App;
    +import com.iab.openrtb.request.Audio;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.Format;
     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;
    +import com.iab.openrtb.request.ntv.EventTrackingMethod;
    +import com.iab.openrtb.request.ntv.EventType;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.EventTracker;
    +import com.iab.openrtb.response.Response;
     import com.iab.openrtb.response.SeatBid;
     import org.junit.Before;
     import org.junit.Test;
     import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.ix.model.response.NativeV11Wrapper;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderError;
     import org.prebid.server.bidder.model.HttpCall;
    @@ -24,23 +30,25 @@
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.request.ix.ExtImpIx;
     import org.prebid.server.proto.openrtb.ext.response.BidType;
    +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
    +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo;
     
     import java.util.ArrayList;
     import java.util.List;
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.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.banner;
    +import static org.assertj.core.api.Assertions.tuple;
     
     public class IxBidderTest extends VertxTest {
     
         private static final String ENDPOINT_URL = "http://exchange.org/";
         private static final int REQUEST_LIMIT = 20;
    +    private static final String SITE_ID = "site id";
     
         private IxBidder ixBidder;
     
    @@ -54,39 +62,6 @@ public void creationShouldFailOnInvalidEndpointUrl() {
             assertThatIllegalArgumentException().isThrownBy(() -> new IxBidder("invalid_url", jacksonMapper));
         }
     
    -    @Test
    -    public void makeHttpRequestsShouldReturnErrorIfRequestHasApp() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .app(App.builder().build())
    -                .build();
    -
    -        // when
    -        final Result>> result = ixBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("ix doesn't support apps"));
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpHasNoBanner() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder.banner(null).video(Video.builder().build()));
    -
    -        // when
    -        final Result>> result = ixBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(2)
    -                .containsOnly(
    -                        BidderError.badInput("Invalid MediaType. Ix supports only Banner type. Ignoring ImpID=123"),
    -                        BidderError.badInput("No valid impressions in the bid request"));
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
         @Test
         public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             // given
    @@ -104,30 +79,25 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtSiteIdIsNullOrBlank() {
    +    public void makeHttpRequestsShouldReturnWhenImpHasNoBanner() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(
    -                        givenImp(impBuilder -> impBuilder.ext(
    -                                mapper.valueToTree(ExtPrebid.of(null, ExtImpIx.of(null))))),
    -                        givenImp(impBuilder -> impBuilder.ext(
    -                                mapper.valueToTree(ExtPrebid.of(null, ExtImpIx.of("")))))))
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(null).video(Video.builder().build()));
     
             // when
             final Result>> result = ixBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(3)
    -                .containsOnly(BidderError.badInput("Missing siteId param"),
    -                        BidderError.badInput("No valid impressions in the bid request"));
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).isEmpty();
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetSitePublisherIdFromImpExt() {
    +    public void makeHttpRequestsShouldSetSitePublisherIdFromImpExtWhenSitePresent() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(identity());
    +        final Banner banner = Banner.builder().w(100).h(200).build();
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.site(Site.builder().build()),
    +                impBuilder -> impBuilder.banner(banner));
     
             // when
             final Result>> result = ixBidder.makeHttpRequests(bidRequest);
    @@ -139,28 +109,7 @@ public void makeHttpRequestsShouldSetSitePublisherIdFromImpExt() {
                     .extracting(BidRequest::getSite)
                     .extracting(Site::getPublisher)
                     .extracting(Publisher::getId)
    -                .containsOnly("site id");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldSetImpTagIdFromImpId() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder.banner(Banner.builder()
    -                        .id("123")
    -                        .format(singletonList(Format.builder().w(300).h(200).build()))
    -                        .build()));
    -
    -        // when
    -        final Result>> result = ixBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getTagid)
    -                .containsOnly("123");
    +                .containsOnly(SITE_ID);
         }
     
         @Test
    @@ -214,6 +163,7 @@ public void makeHttpRequestsShouldCreateOneRequestPerImp() {
             final BidRequest bidRequest = BidRequest.builder()
                     .imp(asList(
                             givenImp(impBuilder -> impBuilder
    +                                .id("123")
                                     .banner(Banner.builder()
                                             .format(singletonList(Format.builder().w(300).h(200).build())).build())),
                             givenImp(impBuilder -> impBuilder
    @@ -239,7 +189,8 @@ public void makeHttpRequestsShouldCreateOneRequestPerBannerFormat() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     impBuilder -> impBuilder.banner(Banner.builder()
    -                        .format(asList(Format.builder().w(300).h(200).build(),
    +                        .format(asList(
    +                                Format.builder().w(300).h(200).build(),
                                     Format.builder().w(600).h(400).build()))
                             .build()));
     
    @@ -250,11 +201,8 @@ public void makeHttpRequestsShouldCreateOneRequestPerBannerFormat() {
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(2)
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .extracting(BidRequest::getSite)
    -                .extracting(Site::getPublisher)
    -                .extracting(Publisher::getId)
    -                // both from same imp (same imp.ext.siteId)
    -                .containsOnly("site id");
    +                .flatExtracting(BidRequest::getImp)
    +                .hasSize(2);
         }
     
         @Test
    @@ -286,8 +234,7 @@ public void makeHttpRequestsShouldLimitImpsAmount() {
                 imps.add(givenImp(impBuilder -> impBuilder.banner(Banner.builder()
                         .format(singletonList(Format.builder().w(value).h(value).build())).build())));
             }
    -        final BidRequest bidRequest = givenBidRequest(requestBuilder -> requestBuilder.imp(imps),
    -                identity());
    +        final BidRequest bidRequest = givenBidRequest(requestBuilder -> requestBuilder.imp(imps), identity());
     
             // when
             final Result>> result = ixBidder.makeHttpRequests(bidRequest);
    @@ -297,6 +244,37 @@ public void makeHttpRequestsShouldLimitImpsAmount() {
             assertThat(result.getValue()).hasSize(REQUEST_LIMIT);
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldAcceptDifferentSiteIdAliases() {
    +        final List imps = asList(
    +                givenImp(impBuilder -> impBuilder
    +                        .id("123")
    +                        .ext(mapper.createObjectNode().set("bidder",
    +                                mapper.createObjectNode().put("siteid", "siteId1")))),
    +                givenImp(impBuilder -> impBuilder
    +                        .id("346")
    +                        .ext(mapper.createObjectNode().set("bidder",
    +                                mapper.createObjectNode().put("siteId", "siteId2")))),
    +                givenImp(impBuilder -> impBuilder
    +                        .id("678")
    +                        .ext(mapper.createObjectNode().set("bidder",
    +                                mapper.createObjectNode().put("siteID", "siteId3")))));
    +        final BidRequest bidRequest = givenBidRequest(requestBuilder ->
    +                requestBuilder.imp(imps).site(Site.builder().build()), identity());
    +
    +        // when
    +        final Result>> result = ixBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsExactlyInAnyOrder("siteId1", "siteId2", "siteId3");
    +    }
    +
         @Test
         public void makeHttpRequestsShouldLimitTotalAmountOfRequests() {
             // given
    @@ -304,7 +282,8 @@ public void makeHttpRequestsShouldLimitTotalAmountOfRequests() {
             for (int i = 0; i < 21; i++) {
                 int value = i;
                 imps.add(givenImp(impBuilder -> impBuilder.banner(Banner.builder()
    -                    .format(asList(Format.builder().w(value).h(value).build(),
    +                    .format(asList(
    +                            Format.builder().w(value).h(value).build(),
                                 Format.builder().w(value + 1).h(value).build()))
                         .build())));
             }
    @@ -323,16 +302,16 @@ public void makeHttpRequestsShouldLimitTotalAmountOfRequests() {
         public void makeHttpRequestsShouldPrioritizeFirstFormatPerImpOverOtherFormats() {
             // given
             final List imps = new ArrayList<>();
    -        for (int i = 0; i < 21; i++) {
    +        for (int i = 0; i <= REQUEST_LIMIT; i++) {
                 int priority = i;
                 int other = i + 25;
                 imps.add(givenImp(impBuilder -> impBuilder.banner(Banner.builder()
    -                    .format(asList(Format.builder().w(priority).h(priority).build(),
    +                    .format(asList(
    +                            Format.builder().w(priority).h(priority).build(),
                                 Format.builder().w(other).h(other).build()))
                         .build())));
             }
    -        final BidRequest bidRequest = givenBidRequest(requestBuilder -> requestBuilder.imp(imps),
    -                identity());
    +        final BidRequest bidRequest = givenBidRequest(requestBuilder -> requestBuilder.imp(imps), identity());
     
             // when
             final Result>> result = ixBidder.makeHttpRequests(bidRequest);
    @@ -397,14 +376,14 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso
         }
     
         @Test
    -    public void makeBidsShouldReturnBannerBidWithOriginalBidSize() throws JsonProcessingException {
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(
                     BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
                             .build(),
                     mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123").w(300).h(200))));
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
     
             // when
             final Result> result = ixBidder.makeBids(httpCall, null);
    @@ -412,16 +391,15 @@ public void makeBidsShouldReturnBannerBidWithOriginalBidSize() throws JsonProces
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").w(300).h(200).build(), banner, "USD"));
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), BidType.banner, "EUR"));
         }
     
         @Test
    -    public void makeBidsShouldReturnBannerBidWithRequestImpSizeWhenBidSizeIsEmpty() throws JsonProcessingException {
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessingException {
             // given
    -        final Banner banner = Banner.builder().w(300).h(200).build();
             final HttpCall httpCall = givenHttpCall(
                     BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().id("123").banner(banner).build()))
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
                             .build(),
                     mapper.writeValueAsString(
                             givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    @@ -432,19 +410,237 @@ public void makeBidsShouldReturnBannerBidWithRequestImpSizeWhenBidSizeIsEmpty()
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").w(300).h(200).build(), BidType.banner, "USD"));
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), BidType.xNative, "EUR"));
         }
     
         @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(ixBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +    public void makeBidsShouldReturnAudioBidIfAudioIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").audio(Audio.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = ixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), BidType.audio, "EUR"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfImpNotMatched() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("489"))));
    +
    +        // when
    +        final Result> result = ixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Unmatched impression id 489"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBidWithVideoExt() throws JsonProcessingException {
    +        // given
    +        final Video video = Video.builder().build();
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(video).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(
    +                                bidBuilder -> bidBuilder
    +                                        .impid("123")
    +                                        .ext(mapper.valueToTree(ExtBidPrebid.builder()
    +                                                .video(ExtBidPrebidVideo.of(1, "cat"))
    +                                                .build())))));
    +
    +        // when
    +        final Result> result = ixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getExt)
    +                .extracting(node -> mapper.treeToValue(node, ExtBidPrebid.class))
    +                .extracting(ExtBidPrebid::getVideo)
    +                .extracting(ExtBidPrebidVideo::getDuration, ExtBidPrebidVideo::getPrimaryCategory)
    +                .containsExactly(tuple(1, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnAdmContainingImageTrackersUrls() throws JsonProcessingException {
    +        // given
    +        final String adm = mapper.writeValueAsString(
    +                Response.builder()
    +                        .imptrackers(singletonList("impTracker"))
    +                        .eventtrackers(singletonList(
    +                                EventTracker.builder()
    +                                        .method(EventTrackingMethod.IMAGE.getValue())
    +                                        .url("eventUrl")
    +                                        .build()))
    +                        .build());
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder
    +                        .impid("123")
    +                        .adm(adm))));
    +
    +        // when
    +        final Result> result = ixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        final Response expectedNativeResponse = Response.builder()
    +                .imptrackers(asList("eventUrl", "impTracker"))
    +                .eventtrackers(singletonList(EventTracker.builder()
    +                        .method(EventTrackingMethod.IMAGE.getValue())
    +                        .url("eventUrl")
    +                        .build()))
    +                .build();
    +
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getAdm)
    +                .containsExactly(mapper.writeValueAsString(expectedNativeResponse));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnAdmContainingImpTrackersAndEventImpTrackersUrls() throws JsonProcessingException {
    +        // given
    +        final String adm = mapper.writeValueAsString(
    +                Response.builder()
    +                        .imptrackers(singletonList("impTracker"))
    +                        .eventtrackers(singletonList(
    +                                EventTracker.builder()
    +                                        .event(EventType.IMPRESSION.getValue())
    +                                        .url("eventUrl")
    +                                        .build()))
    +                        .build());
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder
    +                        .impid("123")
    +                        .adm(adm))));
    +
    +        // when
    +        final Result> result = ixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        final Response expectedNativeResponse = Response.builder()
    +                .imptrackers(asList("eventUrl", "impTracker"))
    +                .eventtrackers(singletonList(EventTracker.builder()
    +                        .event(EventType.IMPRESSION.getValue())
    +                        .url("eventUrl")
    +                        .build()))
    +                .build();
    +
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getAdm)
    +                .containsExactly(mapper.writeValueAsString(expectedNativeResponse));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnAdmContainingOnlyUniqueImpTrackersUrls() throws JsonProcessingException {
    +        // given
    +        final String adm = mapper.writeValueAsString(
    +                Response.builder()
    +                        .imptrackers(asList("impTracker", "someTracker"))
    +                        .eventtrackers(asList(
    +                                EventTracker.builder()
    +                                        .event(EventType.IMPRESSION.getValue())
    +                                        .url("eventUrl")
    +                                        .build(),
    +                                EventTracker.builder()
    +                                        .event(EventTrackingMethod.IMAGE.getValue())
    +                                        .url("someTracker")
    +                                        .build()))
    +                        .build());
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder
    +                        .impid("123")
    +                        .adm(adm))));
    +
    +        // when
    +        final Result> result = ixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getAdm)
    +                .extracting(bidAdm -> mapper.readValue(bidAdm, Response.class))
    +                .flatExtracting(Response::getImptrackers)
    +                .containsExactlyInAnyOrder("eventUrl", "someTracker", "impTracker");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnValidAdmIfNativeIsPresentInImpAndAdm12() throws JsonProcessingException {
    +        // given
    +        final String adm = mapper.writeValueAsString(NativeV11Wrapper.of(
    +                Response.builder()
    +                        .imptrackers(singletonList("ImpTracker"))
    +                        .eventtrackers(singletonList(
    +                                EventTracker.builder()
    +                                        .event(EventType.IMPRESSION.getValue())
    +                                        .method(EventTrackingMethod.IMAGE.getValue())
    +                                        .url("EventUrl")
    +                                        .build()))
    +                        .build()));
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder
    +                        .impid("123")
    +                        .adm(adm))));
    +
    +        // when
    +        final Result> result = ixBidder.makeBids(httpCall, null);
    +
    +        // then
    +        final NativeV11Wrapper expectedNativeResponse = NativeV11Wrapper.of(Response.builder()
    +                .imptrackers(asList("EventUrl", "ImpTracker"))
    +                .eventtrackers(singletonList(EventTracker.builder()
    +                        .method(EventTrackingMethod.IMAGE.getValue())
    +                        .event(EventType.IMPRESSION.getValue())
    +                        .url("EventUrl")
    +                        .build()))
    +                .build());
    +
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getAdm)
    +                .containsExactly(mapper.writeValueAsString(expectedNativeResponse));
         }
     
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
             return bidRequestCustomizer.apply(BidRequest.builder()
    -                .imp(singletonList(givenImp(impCustomizer))))
    +                        .imp(singletonList(givenImp(impCustomizer))))
                     .build();
         }
     
    @@ -454,14 +650,15 @@ private static BidRequest givenBidRequest(Function impCustomizer) {
             return impCustomizer.apply(Imp.builder()
    -                .id("123")
    -                .banner(Banner.builder().build())
    -                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpIx.of("site id")))))
    +                        .id("123")
    +                        .banner(Banner.builder().w(1).h(1).build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpIx.of(SITE_ID, null)))))
                     .build();
         }
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("EUR")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/jixie/JixieBidderTest.java b/src/test/java/org/prebid/server/bidder/jixie/JixieBidderTest.java
    new file mode 100644
    index 00000000000..de5d1312872
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/jixie/JixieBidderTest.java
    @@ -0,0 +1,233 @@
    +package org.prebid.server.bidder.jixie;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Native;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import io.netty.handler.codec.http.HttpHeaderValues;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.jixie.ExtImpJixie;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +
    +public class JixieBidderTest extends VertxTest {
    +
    +    public static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private JixieBidder jixieBidder;
    +
    +    @Before
    +    public void setUp() {
    +        jixieBidder = new JixieBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new JixieBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestWithAllImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        givenImp(identity()))));
    +
    +        // when
    +        final Result>> result = jixieBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .hasSize(2);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCorrectlyAddHeaders() {
    +        // given
    +        final Imp firstImp = givenImp(identity());
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ua("someUa").ip("someIp").build())
    +                .site(Site.builder().page("somePage").build())
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = jixieBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .flatExtracting(res -> res.getHeaders().entries())
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .containsExactlyInAnyOrder(
    +                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    +                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()),
    +                        tuple(HttpUtil.USER_AGENT_HEADER.toString(), "someUa"),
    +                        tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "someIp"),
    +                        tuple(HttpUtil.REFERER_HEADER.toString(), "somePage"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = jixieBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = jixieBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = jixieBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfAdmContainsVast() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.adm("contains > result = jixieBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().adm("contains  httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.adm("contains > result = jixieBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().adm("contains  httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(givenBidResponse(identity())));
    +
    +        // when
    +        final Result> result = jixieBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().build(), banner, "EUR"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer) {
    +        return givenBidRequest(impCustomizer, identity());
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpJixie.of("unit", "accountId", "jxProp1", "jxProp2"))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("EUR")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/kayzen/KayzenBidderTest.java b/src/test/java/org/prebid/server/bidder/kayzen/KayzenBidderTest.java
    new file mode 100644
    index 00000000000..4975b1bae8a
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/kayzen/KayzenBidderTest.java
    @@ -0,0 +1,266 @@
    +package org.prebid.server.bidder.kayzen;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.kayzen.ExtImpKayzen;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +import java.util.stream.Collectors;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class KayzenBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://bids-{{ZoneID}}.bidder.kayzen.io/?exchange={{AccountID}}";
    +
    +    private KayzenBidder kayzenBidder;
    +
    +    @Before
    +    public void setUp() {
    +        kayzenBidder = new KayzenBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new KayzenBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest((Function) impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = kayzenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Missing bidder ext in impression with id: 123"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldDeleteExtOfOnlyFirstImp() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                (Function) impBuilder -> impBuilder.ext(
    +                        mapper.valueToTree(givenPrebidKayzenExt("someZoneIdLongerThan7", "exchange"))),
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(givenPrebidKayzenExt(
    +                        "anotherZoneIdLongerThan7", "anotherExchange"))),
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(givenPrebidKayzenExt(
    +                        "oneMoreZoneIdLongerThan7", "oneMoreExchange"))));
    +
    +        // when
    +        final Result>> result = kayzenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactlyInAnyOrder(
    +                        null,
    +                        mapper.valueToTree(givenPrebidKayzenExt(
    +                                "anotherZoneIdLongerThan7", "anotherExchange")),
    +                        mapper.valueToTree(givenPrebidKayzenExt(
    +                                "oneMoreZoneIdLongerThan7", "oneMoreExchange")));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest((Function) impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(givenPrebidKayzenExt("someZoneId", "someExchange"))));
    +
    +        // when
    +        final Result>> result = kayzenBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://bids-someZoneId.bidder.kayzen.io/?exchange=someExchange");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = kayzenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = kayzenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = kayzenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(impBuilder -> impBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = kayzenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(impBuilder -> impBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = kayzenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(impBuilder -> impBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = kayzenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBidderBidIfBannerAndVideoAndNativeIsAbsentInRequestImp()
    +        throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(impBuilder -> impBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = kayzenBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    private static ExtPrebid givenPrebidKayzenExt(String zoneId, String exchange) {
    +        return ExtPrebid.of(null, ExtImpKayzen.of(zoneId, exchange));
    +    }
    +
    +    @SafeVarargs
    +    private static BidRequest givenBidRequest(Function... impCustomizers) {
    +        return givenBidRequest(identity(), impCustomizers);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function... impCustomizers) {
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(Arrays.stream(impCustomizers).map(KayzenBidderTest::givenImp).collect(Collectors.toList())))
    +                .build();
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpKayzen.of("zoneId", "exchange")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/kidoz/KidozBidderTest.java b/src/test/java/org/prebid/server/bidder/kidoz/KidozBidderTest.java
    index 0d13cc35890..54174f99ac6 100644
    --- a/src/test/java/org/prebid/server/bidder/kidoz/KidozBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/kidoz/KidozBidderTest.java
    @@ -32,7 +32,6 @@
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -178,7 +177,7 @@ public void makeHttpRequestsShouldSetDefaultHeaders() {
             // then
             assertThat(result.getValue().get(0).getHeaders()).isNotNull()
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple("x-openrtb-version", "2.5"),
    +                .containsOnly(tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"),
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
         }
    @@ -338,25 +337,6 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
         }
     
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = kidozBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(kidozBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
    @@ -383,6 +363,7 @@ private static Imp givenImp(Function impCustomiz
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
                     .build();
    diff --git a/src/test/java/org/prebid/server/bidder/krushmedia/KrushmediaBidderTest.java b/src/test/java/org/prebid/server/bidder/krushmedia/KrushmediaBidderTest.java
    new file mode 100644
    index 00000000000..ee8904eac60
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/krushmedia/KrushmediaBidderTest.java
    @@ -0,0 +1,263 @@
    +package org.prebid.server.bidder.krushmedia;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.krushmedia.ExtImpKrushmedia;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class KrushmediaBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.com/prebid/bid&key={{AccountID}}";
    +
    +    private KrushmediaBidder krushmediaBidder;
    +
    +    @Before
    +    public void setUp() {
    +        krushmediaBidder = new KrushmediaBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new KrushmediaBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = krushmediaBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Error while unmarshalling bidder extension");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build()));
    +
    +        // when
    +        final Result>> result = krushmediaBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getUri)
    +                .containsOnly("https://test.com/prebid/bid&key=accountId");
    +    }
    +
    +    @Test
    +    public void shouldRemoveFirsImpExt() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build()));
    +
    +        // when
    +        final Result>> result = krushmediaBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsNull();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = krushmediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = krushmediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = krushmediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = krushmediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyBidderBidsFromSecondSeatBid() throws JsonProcessingException {
    +        // given
    +        final SeatBid firstSeatBId = SeatBid.builder()
    +                .bid(singletonList(Bid.builder()
    +                        .impid("123")
    +                        .build()))
    +                .build();
    +
    +        final SeatBid secondSeatBid = SeatBid.builder()
    +                .bid(singletonList(Bid.builder()
    +                        .impid("456")
    +                        .build()))
    +                .build();
    +
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(BidResponse.builder()
    +                        .seatbid(Arrays.asList(firstSeatBId, secondSeatBid))
    +                        .build()));
    +
    +        // when
    +        final Result> result = krushmediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = krushmediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = krushmediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().id("banner_id").build()).ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpKrushmedia.of("accountId")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/kubient/KubientBidderTest.java b/src/test/java/org/prebid/server/bidder/kubient/KubientBidderTest.java
    index 4f7760b03bc..500ede92f9b 100644
    --- a/src/test/java/org/prebid/server/bidder/kubient/KubientBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/kubient/KubientBidderTest.java
    @@ -22,7 +22,6 @@
     import java.util.List;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    @@ -179,13 +178,9 @@ public void makeBidsShouldReturnResponseWithErrorWhenIdIsNotFound() throws JsonP
             assertThat(result.getValue()).isEmpty();
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(kubientBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/lifestreet/LifestreetAdapterTest.java b/src/test/java/org/prebid/server/bidder/lifestreet/LifestreetAdapterTest.java
    deleted file mode 100644
    index 1a9cb74bbde..00000000000
    --- a/src/test/java/org/prebid/server/bidder/lifestreet/LifestreetAdapterTest.java
    +++ /dev/null
    @@ -1,457 +0,0 @@
    -package org.prebid.server.bidder.lifestreet;
    -
    -import com.fasterxml.jackson.databind.node.ObjectNode;
    -import com.fasterxml.jackson.databind.node.TextNode;
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.Banner;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.BidRequest.BidRequestBuilder;
    -import com.iab.openrtb.request.Device;
    -import com.iab.openrtb.request.Format;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Regs;
    -import com.iab.openrtb.request.Site;
    -import com.iab.openrtb.request.Source;
    -import com.iab.openrtb.request.User;
    -import com.iab.openrtb.response.Bid;
    -import com.iab.openrtb.response.BidResponse;
    -import com.iab.openrtb.response.BidResponse.BidResponseBuilder;
    -import com.iab.openrtb.response.SeatBid;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.auction.model.AdUnitBid.AdUnitBidBuilder;
    -import org.prebid.server.auction.model.AdapterRequest;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    -import org.prebid.server.auction.model.PreBidRequestContext.PreBidRequestContextBuilder;
    -import org.prebid.server.bidder.lifestreet.proto.LifestreetParams;
    -import org.prebid.server.bidder.model.AdapterHttpRequest;
    -import org.prebid.server.bidder.model.ExchangeCall;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.request.PreBidRequest;
    -import org.prebid.server.proto.request.PreBidRequest.PreBidRequestBuilder;
    -import org.prebid.server.proto.request.Video;
    -import org.prebid.server.proto.response.BidderDebug;
    -import org.prebid.server.proto.response.MediaType;
    -
    -import java.math.BigDecimal;
    -import java.util.EnumSet;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.function.Function;
    -import java.util.stream.Collectors;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptySet;
    -import static java.util.Collections.singleton;
    -import static java.util.Collections.singletonList;
    -import static java.util.function.Function.identity;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    -import static org.assertj.core.api.Assertions.assertThatNullPointerException;
    -import static org.assertj.core.api.Assertions.assertThatThrownBy;
    -import static org.assertj.core.api.Assertions.tuple;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -
    -public class LifestreetAdapterTest extends VertxTest {
    -
    -    private static final String BIDDER = "lifestreet";
    -    private static final String COOKIE_FAMILY = BIDDER;
    -    private static final String ENDPOINT_URL = "http://endpoint.org/";
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private UidsCookie uidsCookie;
    -
    -    private AdapterRequest adapterRequest;
    -    private PreBidRequestContext preBidRequestContext;
    -    private ExchangeCall exchangeCall;
    -    private LifestreetAdapter adapter;
    -
    -    @Before
    -    public void setUp() {
    -        adapterRequest = givenBidder(identity());
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -        adapter = new LifestreetAdapter(COOKIE_FAMILY, ENDPOINT_URL, jacksonMapper);
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnNullArguments() {
    -        assertThatNullPointerException().isThrownBy(() -> new LifestreetAdapter(null, null, jacksonMapper));
    -        assertThatNullPointerException().isThrownBy(() -> new LifestreetAdapter(COOKIE_FAMILY, null, jacksonMapper));
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException()
    -                .isThrownBy(() -> new LifestreetAdapter(COOKIE_FAMILY, "invalid_url", jacksonMapper))
    -                .withMessage("URL supplied is not valid: invalid_url");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedHeaders() {
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).flatExtracting(r -> r.getHeaders().entries())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple("Content-Type", "application/json;charset=utf-8"),
    -                        tuple("Accept", "application/json"));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfParamsMissingInAtLeastOneAdUnitBid() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(identity()),
    -                givenAdUnitBid(builder -> builder.params(null))));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Lifestreet params section is missing");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamSlotTagIsMissing() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("slot_tag", null);
    -        adapterRequest = givenBidder(builder -> builder.params(params));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Missing slot_tag param");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamSlotTagIsInvalid() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("slot_tag", new TextNode("invalid"));
    -        adapterRequest = givenBidder(builder -> builder.params(params));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid slot_tag param 'invalid'");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsVideoAndMimesListIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .mediaTypes(singleton(MediaType.video))
    -                        .video(Video.builder()
    -                                .mimes(emptyList())
    -                                .build()))));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid AdUnit: VIDEO media type with no video data");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .mediaTypes(emptySet()))));
    -
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid ad unit/imp");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder
    -                        .bidderCode(BIDDER)
    -                        .adUnitCode("adUnitCode")
    -                        .instl(1)
    -                        .topframe(1)
    -                        .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                        .params(mapper.valueToTree(LifestreetParams.of("slot.TAG"))));
    -
    -        preBidRequestContext = givenPreBidRequestContext(
    -                builder -> builder
    -                        .referer("http://www.example.com")
    -                        .domain("example.com")
    -                        .ip("192.168.144.1")
    -                        .ua("userAgent"),
    -                builder -> builder
    -                        .timeoutMillis(1500L)
    -                        .tid("tid")
    -                        .user(User.builder()
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null))));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUid");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .containsOnly(BidRequest.builder()
    -                        .id("tid")
    -                        .at(1)
    -                        .tmax(1500L)
    -                        .imp(singletonList(Imp.builder()
    -                                .id("adUnitCode")
    -                                .instl(1)
    -                                .tagid("slot.TAG")
    -                                .banner(Banner.builder()
    -                                        .w(300)
    -                                        .h(250)
    -                                        .topframe(1)
    -                                        .build())
    -                                .build()))
    -                        .site(Site.builder()
    -                                .domain("example.com")
    -                                .page("http://www.example.com")
    -                                .build())
    -                        .device(Device.builder()
    -                                .ua("userAgent")
    -                                .ip("192.168.144.1")
    -                                .build())
    -                        .user(User.builder()
    -                                .buyeruid("buyerUid")
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null)))
    -                        .source(Source.builder()
    -                                .fd(1)
    -                                .tid("tid")
    -                                .build())
    -                        .build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithAppFromPreBidRequest() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder
    -                .app(App.builder().id("appId").build()));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getApp().getId())
    -                .containsOnly("appId");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithUserFromPreBidRequestIfAppPresent() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder
    -                .app(App.builder().build())
    -                .user(User.builder().buyeruid("buyerUid").build()));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUidFromCookie");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getUser())
    -                .containsOnly(User.builder().buyeruid("buyerUid").build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnOneRequestWithOneImpIfAdUnitContainsBannerAndVideoMediaTypes() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                        .mediaTypes(EnumSet.of(MediaType.video, MediaType.banner))
    -                        .video(Video.builder()
    -                                .mimes(singletonList("Mime"))
    -                                .playbackMethod(1)
    -                                .build()))));
    -
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp())
    -                .extracting(imp -> imp.getVideo() == null, imp -> imp.getBanner() == null)
    -                .containsOnly(tuple(false, false));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithMultipleRequestsIfMultipleAdUnitsInPreBidRequest() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1")),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(2)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(2)
    -                .extracting(Imp::getId).containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId1").adUnitCode("adUnitCode1"));
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode1")
    -                                        .price(new BigDecimal("8.43"))
    -                                        .adm("adm1")
    -                                        .crid("crid1")
    -                                        .w(300)
    -                                        .h(250)
    -                                        .dealid("dealId1")
    -                                        .nurl("nurl1")
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids)
    -                .containsExactly(org.prebid.server.proto.response.Bid.builder()
    -                        .code("adUnitCode1")
    -                        .price(new BigDecimal("8.43"))
    -                        .adm("adm1")
    -                        .creativeId("crid1")
    -                        .width(300)
    -                        .height(250)
    -                        .dealId("dealId1")
    -                        .nurl("nurl1")
    -                        .bidder(BIDDER)
    -                        .bidId("bidId1")
    -                        .build());
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnEmptyBidsIfEmptyOrNullBidResponse() {
    -        // given
    -        adapterRequest = givenBidder(identity());
    -
    -        exchangeCall = givenExchangeCall(identity(), br -> br.seatbid(null));
    -
    -        // when and then
    -        assertThat(adapter.extractBids(adapterRequest, exchangeCall)).isEmpty();
    -        assertThat(adapter.extractBids(adapterRequest, ExchangeCall.empty(null))).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnOnlyFirstBidBuilderFromMultipleBidsInResponse() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1")),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"))));
    -
    -        exchangeCall = givenExchangeCall(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(asList(Bid.builder().impid("adUnitCode1").build(),
    -                                        Bid.builder().impid("adUnitCode2").build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(1)
    -                .extracting(org.prebid.server.proto.response.Bid::getCode)
    -                .containsOnly("adUnitCode1");
    -    }
    -
    -    private static AdapterRequest givenBidder(Function adUnitBidBuilderCustomizer) {
    -        return AdapterRequest.of(BIDDER, singletonList(givenAdUnitBid(adUnitBidBuilderCustomizer)));
    -    }
    -
    -    private static AdUnitBid givenAdUnitBid(Function adUnitBidBuilderCustomizer) {
    -        final AdUnitBidBuilder adUnitBidBuilderMinimal = AdUnitBid.builder()
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .params(mapper.valueToTree(LifestreetParams.of("slot.tag1")))
    -                .mediaTypes(singleton(MediaType.banner));
    -
    -        final AdUnitBidBuilder adUnitBidBuilderCustomized = adUnitBidBuilderCustomizer.apply(adUnitBidBuilderMinimal);
    -
    -        return adUnitBidBuilderCustomized.build();
    -    }
    -
    -    private PreBidRequestContext givenPreBidRequestContext(
    -            Function preBidRequestContextBuilderCustomizer,
    -            Function preBidRequestBuilderCustomizer) {
    -
    -        final PreBidRequestBuilder preBidRequestBuilderMinimal = PreBidRequest.builder()
    -                .accountId("accountId");
    -        final PreBidRequest preBidRequest = preBidRequestBuilderCustomizer.apply(preBidRequestBuilderMinimal).build();
    -
    -        final PreBidRequestContextBuilder preBidRequestContextBuilderMinimal =
    -                PreBidRequestContext.builder()
    -                        .preBidRequest(preBidRequest)
    -                        .uidsCookie(uidsCookie);
    -        return preBidRequestContextBuilderCustomizer.apply(preBidRequestContextBuilderMinimal).build();
    -    }
    -
    -    private static ExchangeCall givenExchangeCall(
    -            Function bidRequestBuilderCustomizer,
    -            Function bidResponseBuilderCustomizer) {
    -
    -        final BidRequestBuilder bidRequestBuilderMinimal = BidRequest.builder();
    -        final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(bidRequestBuilderMinimal).build();
    -
    -        final BidResponseBuilder bidResponseBuilderMinimal = BidResponse.builder();
    -        final BidResponse bidResponse = bidResponseBuilderCustomizer.apply(bidResponseBuilderMinimal).build();
    -
    -        return ExchangeCall.success(bidRequest, bidResponse, BidderDebug.builder().build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/lifestreet/LifestreetBidderTest.java b/src/test/java/org/prebid/server/bidder/lifestreet/LifestreetBidderTest.java
    deleted file mode 100644
    index 3dffcd363cc..00000000000
    --- a/src/test/java/org/prebid/server/bidder/lifestreet/LifestreetBidderTest.java
    +++ /dev/null
    @@ -1,341 +0,0 @@
    -package org.prebid.server.bidder.lifestreet;
    -
    -import com.fasterxml.jackson.core.JsonProcessingException;
    -import com.iab.openrtb.request.Audio;
    -import com.iab.openrtb.request.Banner;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.Format;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Video;
    -import com.iab.openrtb.response.Bid;
    -import com.iab.openrtb.response.BidResponse;
    -import com.iab.openrtb.response.SeatBid;
    -import org.junit.Before;
    -import org.junit.Test;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.bidder.model.BidderBid;
    -import org.prebid.server.bidder.model.BidderError;
    -import org.prebid.server.bidder.model.HttpCall;
    -import org.prebid.server.bidder.model.HttpRequest;
    -import org.prebid.server.bidder.model.HttpResponse;
    -import org.prebid.server.bidder.model.Result;
    -import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    -import org.prebid.server.proto.openrtb.ext.request.lifestreet.ExtImpLifestreet;
    -
    -import java.util.List;
    -import java.util.function.Function;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
    -import static java.util.Collections.singletonList;
    -import static java.util.function.Function.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.banner;
    -import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    -
    -public class LifestreetBidderTest extends VertxTest {
    -
    -    private static final String ENDPOINT_URL = "https://test.endpoint.com/adrequest";
    -
    -    private LifestreetBidder lifestreetBidder;
    -
    -    @Before
    -    public void setUp() {
    -        lifestreetBidder = new LifestreetBidder(ENDPOINT_URL, jacksonMapper);
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException().isThrownBy(() -> new LifestreetBidder("invalid_url", jacksonMapper));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpHasNoBannerOrVideo() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder.banner(null).audio(Audio.builder().build()));
    -
    -        // when
    -        final Result>> result = lifestreetBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput(
    -                        "Invalid MediaType. Lifestreet supports only Banner and Video. Ignoring ImpID=123"));
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    -
    -        // when
    -        final Result>> result = lifestreetBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtSlotTagIsNullOrBlank() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(
    -                        givenImp(impBuilder -> impBuilder
    -                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpLifestreet.of(null))))),
    -                        givenImp(impBuilder -> impBuilder
    -                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpLifestreet.of("")))))))
    -                .build();
    -
    -        // when
    -        final Result>> result = lifestreetBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(2)
    -                .containsOnly(BidderError.badInput("Missing slot_tag param"));
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtSlotTagIsInvalid() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpLifestreet.of("invalid.slot.tag")))));
    -
    -        // when
    -        final Result>> result = lifestreetBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Invalid slot_tag param 'invalid.slot.tag'"));
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldSetImpExtSlotTagToImpTagId() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(identity());
    -
    -        // when
    -        final Result>> result = lifestreetBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getTagid)
    -                .containsOnly("slot.tag");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldCreateOneRequestPerImp() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(
    -                        givenImp(identity()),
    -                        givenImp(impBuilder -> impBuilder.id("321"))))
    -                .build();
    -
    -        // when
    -        final Result>> result = lifestreetBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(2)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getId)
    -                .containsOnly("123", "321");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldSetBannerFormatNullIfBannerIsPresent() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
    -                .banner(Banner.builder()
    -                        .format(singletonList(Format.builder().w(300).h(250).build()))
    -                        .build()));
    -
    -        // when
    -        final Result>> result = lifestreetBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .extracting(Banner::getFormat)
    -                .containsNull();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldSetBannerWidthAndHeightFromFirstBannerFormatIfPresent() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
    -                .banner(Banner.builder()
    -                        .format(singletonList(Format.builder().w(300).h(250).build()))
    -                        .build()));
    -
    -        // when
    -        final Result>> result = lifestreetBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .containsOnly(Banner.builder().w(300).h(250).build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldNotModifyBannerWidthAndHeightIfFormatIsEmpty() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
    -                .banner(Banner.builder()
    -                        .format(emptyList())
    -                        .w(300).h(250)
    -                        .build()));
    -
    -        // when
    -        final Result>> result = lifestreetBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .containsOnly(Banner.builder().w(300).h(250).build());
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    -        // given
    -        final HttpCall httpCall = givenHttpCall(null, "invalid");
    -
    -        // when
    -        final Result> result = lifestreetBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    -        // given
    -        final HttpCall httpCall = givenHttpCall(null,
    -                mapper.writeValueAsString(null));
    -
    -        // when
    -        final Result> result = lifestreetBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    -        // given
    -        final HttpCall httpCall = givenHttpCall(null,
    -                mapper.writeValueAsString(BidResponse.builder().build()));
    -
    -        // when
    -        final Result> result = lifestreetBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
    -        // given
    -        final HttpCall httpCall = givenHttpCall(
    -                givenBidRequest(impBuilder -> impBuilder.id("123")
    -                        .banner(Banner.builder().build())),
    -                mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    -
    -        // when
    -        final Result> result = lifestreetBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnVideoBidIfRequestImpHasVideo() throws JsonProcessingException {
    -        // given
    -        final HttpCall httpCall = givenHttpCall(
    -                givenBidRequest(builder -> builder.id("123")
    -                        .video(Video.builder().build())
    -                        .banner(Banner.builder().build())),
    -                mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    -
    -        // when
    -        final Result> result = lifestreetBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(lifestreetBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
    -    private static BidRequest givenBidRequest(
    -            Function bidRequestCustomizer,
    -            Function impCustomizer) {
    -        return bidRequestCustomizer.apply(BidRequest.builder()
    -                .imp(singletonList(givenImp(impCustomizer))))
    -                .build();
    -    }
    -
    -    private static BidRequest givenBidRequest(Function impCustomizer) {
    -        return givenBidRequest(identity(), impCustomizer);
    -    }
    -
    -    private static Imp givenImp(Function impCustomizer) {
    -        return impCustomizer.apply(Imp.builder()
    -                .id("123")
    -                .banner(Banner.builder().build())
    -                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpLifestreet.of("slot.tag")))))
    -                .build();
    -    }
    -
    -    private static BidResponse givenBidResponse(Function bidCustomizer) {
    -        return BidResponse.builder()
    -                .seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    -                        .build()))
    -                .build();
    -    }
    -
    -    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    -        return HttpCall.success(
    -                HttpRequest.builder().payload(bidRequest).build(),
    -                HttpResponse.of(200, null, body),
    -                null);
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/lockerdome/LockerdomeBidderTest.java b/src/test/java/org/prebid/server/bidder/lockerdome/LockerdomeBidderTest.java
    index 66f3f24016b..32f9595bb7e 100644
    --- a/src/test/java/org/prebid/server/bidder/lockerdome/LockerdomeBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/lockerdome/LockerdomeBidderTest.java
    @@ -23,7 +23,6 @@
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -192,11 +191,6 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(lockerdomeBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(Function impCustomizer) {
             return BidRequest.builder()
                     .imp(singletonList(givenImp(impCustomizer)))
    @@ -212,6 +206,7 @@ private static Imp givenImp(Function impCustomiz
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/logicad/LogicadBidderTest.java b/src/test/java/org/prebid/server/bidder/logicad/LogicadBidderTest.java
    index ec096c5a80a..1345ee88feb 100644
    --- a/src/test/java/org/prebid/server/bidder/logicad/LogicadBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/logicad/LogicadBidderTest.java
    @@ -23,7 +23,6 @@
     import java.util.List;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -179,11 +178,6 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(logicadBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer,
    diff --git a/src/test/java/org/prebid/server/bidder/loopme/LoopmeBidderTest.java b/src/test/java/org/prebid/server/bidder/loopme/LoopmeBidderTest.java
    new file mode 100644
    index 00000000000..63d8048338e
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/loopme/LoopmeBidderTest.java
    @@ -0,0 +1,198 @@
    +package org.prebid.server.bidder.loopme;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.loopme.ExtImpLoopme;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +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;
    +
    +public class LoopmeBidderTest extends VertxTest {
    +
    +    public static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private LoopmeBidder loopmeBidder;
    +
    +    @Before
    +    public void setUp() {
    +        loopmeBidder = new LoopmeBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestWithAllImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        givenImp(identity()))));
    +
    +        // when
    +        final Result>> result = loopmeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .hasSize(2);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = loopmeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = loopmeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(givenImp(identity())))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = loopmeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = loopmeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = loopmeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = loopmeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer) {
    +        return givenBidRequest(impCustomizer, identity());
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .video(Video.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpLoopme.of("somePubId"))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/lunamedia/LunamediaBidderTest.java b/src/test/java/org/prebid/server/bidder/lunamedia/LunamediaBidderTest.java
    index 6b0891f866c..145039f296e 100644
    --- a/src/test/java/org/prebid/server/bidder/lunamedia/LunamediaBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/lunamedia/LunamediaBidderTest.java
    @@ -33,7 +33,6 @@
     
     import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -71,8 +70,11 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() {
             final Result>> result = lunamediaBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(error.getMessage()).startsWith("Cannot deserialize instance");
    +                });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -85,8 +87,7 @@ public void makeHttpRequestsShouldReturnErrorWhenExtPubIdIsNull() {
             final Result>> result = lunamediaBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("No pubid value provided"));
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("No pubid value provided"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -99,8 +100,7 @@ public void makeHttpRequestsShouldReturnErrorWhenExtPubIdIsBlank() {
             final Result>> result = lunamediaBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("No pubid value provided"));
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("No pubid value provided"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -114,8 +114,8 @@ public void makeHttpRequestShouldReturnErrorWhenBannerFormatIsMissing() {
             final Result>> result = lunamediaBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Expected at least one banner.format entry or explicit w/h"));
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Expected at least one banner.format entry or explicit w/h"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -141,7 +141,7 @@ public void makeHttpRequestShouldReturnAllErrorsWithRequest() {
     
             // then
             assertThat(result.getErrors()).hasSize(5)
    -                .containsOnly(BidderError.badInput("No pubid value provided"),
    +                .containsExactlyInAnyOrder(BidderError.badInput("No pubid value provided"),
                             BidderError.badInput("No pubid value provided"),
                             BidderError.badInput("Expected at least one banner.format entry or explicit w/h"),
                             BidderError.badInput("Expected at least one banner.format entry or explicit w/h"),
    @@ -158,8 +158,8 @@ public void makeHttpRequestShouldReturnErrorWhenNoSupportedBidderTypeProvided()
             final Result>> result = lunamediaBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Unsupported impression has been received"));
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Unsupported impression has been received"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -173,15 +173,15 @@ public void makeHttpRequestsShouldFillMethodAndUrlAndExpectedHeaders() {
             final Result>> result = lunamediaBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getValue()).hasSize(1).element(0).isNotNull()
    +        assertThat(result.getValue()).element(0).isNotNull()
                     .returns(HttpMethod.POST, HttpRequest::getMethod)
                     .returns("http://test/get?pubid=pubid", HttpRequest::getUri);
             assertThat(result.getValue().get(0).getHeaders()).isNotNull()
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(
    +                .containsExactlyInAnyOrder(
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), "application/json;charset=utf-8"),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), "application/json"),
    -                        tuple("x-openrtb-version", "2.5"));
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
         }
     
         @Test
    @@ -202,10 +202,10 @@ public void makeHttpRequestShouldSetImpExtNullAndXnativeNull() {
                     .build();
     
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
    -                .containsOnly(expectedImp);
    +                .containsExactly(expectedImp);
         }
     
         @Test
    @@ -228,9 +228,9 @@ public void makeHttpRequestShouldSetTagidFromExtPlacement() {
     
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
    -                .containsOnly(expectedImp);
    +                .containsExactly(expectedImp);
         }
     
         @Test
    @@ -248,10 +248,10 @@ public void makeHttpRequestShouldSetBannerWidthHeightFromFirstFormat() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .flatExtracting(Imp::getBanner)
    -                .containsOnly(Banner.builder()
    +                .containsExactly(Banner.builder()
                             .format(emptyList())
                             .w(300).h(250).build());
         }
    @@ -273,10 +273,10 @@ public void makeHttpRequestShouldSetExtractFirstFormatToBannerWidthHeight() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .flatExtracting(Imp::getBanner)
    -                .containsOnly(Banner.builder()
    +                .containsExactly(Banner.builder()
                             .format(singletonList(Format.builder().w(400).h(200).build()))
                             .w(300).h(250).build());
         }
    @@ -298,9 +298,9 @@ public void makeHttpRequestShouldModifySite() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .extracting(BidRequest::getSite)
    -                .containsOnly(Site.builder().publisher(null).domain("").build());
    +                .containsExactly(Site.builder().publisher(null).domain("").build());
         }
     
         @Test
    @@ -319,9 +319,9 @@ public void makeHttpRequestShouldModifyApp() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .extracting(BidRequest::getApp)
    -                .containsOnly(App.builder().publisher(null).build());
    +                .containsExactly(App.builder().publisher(null).build());
         }
     
         @Test
    @@ -333,9 +333,11 @@ public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() {
             final Result> result = lunamediaBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -383,7 +385,7 @@ public void makeBidsShouldReturnBannerBidWhenTypeNotPresent() throws JsonProcess
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    @@ -401,12 +403,7 @@ public void makeBidsShouldReturnVideoBidWhenVideoPresent() throws JsonProcessing
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(lunamediaBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
         private static BidRequest givenBidRequest(
    @@ -445,6 +442,7 @@ private static Imp givenImp(Function impCustomiz
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/madvertise/MadvertiseBidderTest.java b/src/test/java/org/prebid/server/bidder/madvertise/MadvertiseBidderTest.java
    new file mode 100644
    index 00000000000..5b471b59000
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/madvertise/MadvertiseBidderTest.java
    @@ -0,0 +1,313 @@
    +package org.prebid.server.bidder.madvertise;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Format;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import io.netty.handler.codec.http.HttpHeaderValues;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.between.ExtImpBetween;
    +import org.prebid.server.proto.openrtb.ext.request.madvertise.ExtImpMadvertise;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +import java.util.stream.Collectors;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +
    +public class MadvertiseBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://mobile.mng-ads.com/bidrequest{{ZoneID}}";
    +
    +    private MadvertiseBidder madvertiseBidder;
    +
    +    @Before
    +    public void setUp() {
    +        madvertiseBidder = new MadvertiseBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new MadvertiseBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtContainEmptyZoneIdParam() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .id("123")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpMadvertise.of("")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = madvertiseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("The minLength of zone ID is 7; ImpID=123"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtContainNullZoneIdParam() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .id("123")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpMadvertise.of(null)))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = madvertiseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("The minLength of zone ID is 7; ImpID=123"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfMultipleImpExtHaveDifferentZoneIdParam() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .id("123")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpMadvertise.of("someZoneIdLongerThan7")))));
    +
    +        final Imp secondImp = givenImp(impBuilder -> impBuilder
    +                .id("124")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpMadvertise.of("anotherZoneIdLongerThan7")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(firstImp, secondImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = madvertiseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("There must be only one zone ID"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCorrectlyAddHeaders() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpMadvertise.of("someZoneIdLongerThan7")))));
    +
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .device(Device.builder().ua("someUa").dnt(5).ip("someIp").language("someLanguage").build())
    +                .site(Site.builder().page("somePage").build())
    +                .imp(singletonList(firstImp))
    +                .build();
    +
    +        // when
    +        final Result>> result = madvertiseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .flatExtracting(res -> res.getHeaders().entries())
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .containsExactlyInAnyOrder(
    +                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    +                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()),
    +                        tuple(HttpUtil.USER_AGENT_HEADER.toString(), "someUa"),
    +                        tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "someIp"),
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = madvertiseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Missing bidder ext in impression with id: 123"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.banner(Banner.builder()
    +                        .format(singletonList(
    +                                Format.builder()
    +                                        .w(300).h(500)
    +                                        .build()))
    +                        .build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpMadvertise.of("someZoneIdLongerThan7")))));
    +
    +        // when
    +        final Result>> result = madvertiseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://mobile.mng-ads.com/bidrequestsomeZoneIdLongerThan7");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = madvertiseBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = madvertiseBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = madvertiseBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfSixAndSevenAndSixteenIsAbsentInRequestImp()
    +            throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = madvertiseBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfImpAttrContainsSixOrSevenOrSixteen() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponses(asList(
    +                                bidBuilder -> bidBuilder.impid("123").attr(singletonList(6)),
    +                                bidBuilder -> bidBuilder.impid("124").attr(singletonList(16)),
    +                                bidBuilder -> bidBuilder.impid("125").attr(singletonList(7))))));
    +
    +        // when
    +        final Result> result = madvertiseBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactlyInAnyOrder(
    +                        BidderBid.of(Bid.builder().impid("123").attr(singletonList(6)).build(), video, null),
    +                        BidderBid.of(Bid.builder().impid("124").attr(singletonList(16)).build(), video, null),
    +                        BidderBid.of(Bid.builder().impid("125").attr(singletonList(7)).build(), video, null));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of("127.0.0.1", "pubId")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponses(List> bidCustomizers) {
    +        return BidResponse.builder()
    +                .seatbid(bidCustomizers.stream().map(customizer -> SeatBid.builder()
    +                        .bid(singletonList(customizer.apply(Bid.builder()).build()))
    +                        .build())
    +                        .collect(Collectors.toList()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/marsmedia/MarsmediaBidderTest.java b/src/test/java/org/prebid/server/bidder/marsmedia/MarsmediaBidderTest.java
    index 54382dc9be3..bc078b014a7 100644
    --- a/src/test/java/org/prebid/server/bidder/marsmedia/MarsmediaBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/marsmedia/MarsmediaBidderTest.java
    @@ -6,6 +6,7 @@
     import com.iab.openrtb.request.Device;
     import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Native;
     import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
    @@ -29,7 +30,7 @@
     import java.util.Map;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
    +import static java.util.Arrays.asList;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -64,8 +65,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             final Result>> result = marsmediaBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("ext.bidder not provided"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -80,8 +80,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtZoneIsBlank() {
     
             // then
             assertThat(result.getValue()).isEmpty();
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Zone is empty"));
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Zone is empty"));
         }
     
         @Test
    @@ -95,8 +94,8 @@ public void makeHttpRequestsShouldReturnErrorIfImpBannerHasNoSizeOrFormats() {
     
             // then
             assertThat(result.getValue()).isEmpty();
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("No valid banner format in the bid request"));
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("No valid banner format in the bid request"));
         }
     
         @Test
    @@ -110,22 +109,44 @@ public void makeHttpRequestsShouldReturnErrorIfThereAreNoValidImps() {
     
             // then
             assertThat(result.getValue()).isEmpty();
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("No valid impression in the bid request"));
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("No valid impression in the bid request"));
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnUnmodifiedBidRequest() {
    +    public void makeHttpRequestsShouldAddAtAttributeToOutgoingRequest() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(identity(),
    -                requestBuilder -> requestBuilder.at(1));
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = marsmediaBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getAt)
    +                .containsExactly(1);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldAddOnlyBannerAndVideoImp() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(givenImp(impBuilder -> impBuilder.id("123")),
    +                        givenImp(impBuilder -> impBuilder.id("456").banner(null).video(Video.builder().build())),
    +                        givenImp(impBuilder -> impBuilder.id("789").banner(null).xNative(Native.builder().build()))))
    +                .build();
     
             // when
             final Result>> result = marsmediaBidder.makeHttpRequests(bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue().get(0).getPayload()).isSameAs(bidRequest);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getId)
    +                .containsExactly("123", "456");
         }
     
         @Test
    @@ -141,12 +162,33 @@ public void makeHttpRequestsShouldReplaceBannerWidthAndHeightWithValuesFromFirst
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .extracting(Banner::getW, Banner::getH)
    +                .containsExactly(tuple(640, 480));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReplaceBannerWidthAndHeightWithZeroIfFormatValuesNotPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
    +                .banner(Banner.builder()
    +                        .format(singletonList(Format.builder().w(null).h(null).build()))
    +                        .build()));
    +
    +        // when
    +        final Result>> result = marsmediaBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getBanner)
                     .extracting(Banner::getW, Banner::getH)
    -                .containsOnly(tuple(640, 480));
    +                .containsExactly(tuple(0, 0));
         }
     
         @Test
    @@ -159,10 +201,10 @@ public void makeHttpRequestsShouldAlwaysSetRequestAtToOne() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getAt)
    -                .containsOnly(1);
    +                .containsExactly(1);
         }
     
         @Test
    @@ -175,17 +217,17 @@ public void makeHttpRequestsShouldSetExpectedRequestUriAndBasicHeaders() {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getUri)
    -                .containsOnly("https://test.endpoint.com/test&zone=zoneId");
    +                .containsExactly("https://test.endpoint.com/test&zone=zoneId");
             assertThat(result.getValue())
                     .extracting(HttpRequest::getHeaders)
                     .flatExtracting(MultiMap::entries)
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(
    +                .containsExactlyInAnyOrder(
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()),
    -                        tuple("x-openrtb-version", "2.5"));
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
         }
     
         @Test
    @@ -205,7 +247,7 @@ public void makeHttpRequestsShouldSetAdditionalHeadersIfRequestDeviceIsPresent()
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    +        assertThat(result.getValue())
                     .extracting(HttpRequest::getHeaders)
                     .flatExtracting(MultiMap::entries)
                     .extracting(Map.Entry::getKey, Map.Entry::getValue)
    @@ -225,9 +267,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             final Result> result = marsmediaBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -267,7 +311,7 @@ public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingExcept
                             .imp(singletonList(Imp.builder().id("123").build()))
                             .build(),
                     mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("125"))));
     
             // when
             final Result> result = marsmediaBidder.makeBids(httpCall, null);
    @@ -275,7 +319,21 @@ public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingExcept
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("125").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfFirstSeatIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder().build(),
    +                mapper.writeValueAsString(BidResponse.builder().seatbid(singletonList(null)).build()));
    +
    +        // when
    +        final Result> result = marsmediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    @@ -294,12 +352,7 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessing
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(marsmediaBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
         private static BidRequest givenBidRequest(
    @@ -324,6 +377,7 @@ private static Imp givenImp(Function impModifier
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/medianet/MedianetBidderTest.java b/src/test/java/org/prebid/server/bidder/medianet/MedianetBidderTest.java
    new file mode 100644
    index 00000000000..4c14bb97729
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/medianet/MedianetBidderTest.java
    @@ -0,0 +1,145 @@
    +package org.prebid.server.bidder.medianet;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +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.banner;
    +
    +public class MedianetBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.media.net?src=external.prebidserver.com";
    +
    +    private MedianetBidder medianetBidder;
    +
    +    @Before
    +    public void setup() {
    +        medianetBidder = new MedianetBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException()
    +                .isThrownBy(() -> new MedianetBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotModifyIncomingRequest() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest();
    +
    +        // when
    +        final Result>> result;
    +        result = medianetBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +            .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +            .containsExactly(bidRequest);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = sampleHttpCall(givenBidRequest(), "invalid response");
    +
    +        // when
    +        final Result> result = medianetBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allMatch(error -> error.getType() == BidderError.Type.bad_server_response
    +                    && error.getMessage().startsWith("Failed to decode: Unrecognized token"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall;
    +        httpCall = sampleHttpCall(givenBidRequest(), mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = medianetBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall;
    +        httpCall = sampleHttpCall(null, mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = medianetBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = sampleHttpCall(
    +                givenBidRequest(),
    +                mapper.writeValueAsString(sampleBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = medianetBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    private static BidResponse sampleBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +            .cur("USD")
    +            .seatbid(singletonList(SeatBid.builder()
    +                .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                .build()))
    +            .build();
    +    }
    +
    +    private static HttpCall sampleHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +            HttpRequest.builder().payload(bidRequest).build(),
    +            HttpResponse.of(200, null, body),
    +            null);
    +    }
    +
    +    private static BidRequest givenBidRequest() {
    +        return BidRequest.builder()
    +                .id("request_id")
    +                .imp(singletonList(Imp.builder()
    +                    .id("imp_id")
    +                    .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createObjectNode())))
    +                    .build()))
    +                .build();
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/mgid/MgidBidderTest.java b/src/test/java/org/prebid/server/bidder/mgid/MgidBidderTest.java
    index 2bbcc5ab955..8da915a3cca 100644
    --- a/src/test/java/org/prebid/server/bidder/mgid/MgidBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/mgid/MgidBidderTest.java
    @@ -23,7 +23,6 @@
     import java.util.List;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    @@ -36,21 +35,6 @@ public class MgidBidderTest extends VertxTest {
     
         private MgidBidder mgidBidder;
     
    -    private static BidResponse givenBidResponse(Function bidCustomizer) {
    -        return BidResponse.builder()
    -                .seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    -                        .build()))
    -                .build();
    -    }
    -
    -    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    -        return HttpCall.success(
    -                HttpRequest.builder().payload(bidRequest).build(),
    -                HttpResponse.of(200, null, body),
    -                null);
    -    }
    -
         @Before
         public void setUp() {
             mgidBidder = new MgidBidder(ENDPOINT_URL, jacksonMapper);
    @@ -76,8 +60,11 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             final Result>> result = mgidBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(error.getMessage()).startsWith("Cannot deserialize instance");
    +                });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -126,7 +113,7 @@ public void makeHttpRequestsShouldSetTagidToIncomingRequestWhenImpExtHasBlankPla
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getTagid)
    -                .containsOnly(impId);
    +                .containsExactly(impId);
         }
     
         @Test
    @@ -153,7 +140,7 @@ public void makeHttpRequestsShouldSetTagidToIncomingRequestWhenImpExtHasNotBlank
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getTagid)
    -                .containsOnly(expectedTagId);
    +                .containsExactly(expectedTagId);
         }
     
         @Test
    @@ -193,7 +180,7 @@ public void makeHttpRequestsShouldSetBidFloorCurAndBidFloorToIncomingRequestWhen
     
             assertThat(result.getValue()).hasSize(1)
                     .extracting(HttpRequest::getBody)
    -                .containsOnly(mapper.writeValueAsString(expected));
    +                .containsExactly(mapper.writeValueAsString(expected));
         }
     
         @Test
    @@ -232,7 +219,7 @@ public void makeHttpRequestsShouldSetBidFloorCurAndBidFloorToRequestWhenImpExtHa
     
             assertThat(result.getValue()).hasSize(1)
                     .extracting(HttpRequest::getBody)
    -                .containsOnly(mapper.writeValueAsString(expected));
    +                .containsExactly(mapper.writeValueAsString(expected));
         }
     
         @Test
    @@ -270,7 +257,7 @@ public void makeHttpRequestsShouldNotModifyIncomingRequestWhenImpExtNotContainsP
     
             assertThat(result.getValue()).hasSize(1)
                     .extracting(HttpRequest::getBody)
    -                .containsOnly(mapper.writeValueAsString(expected));
    +                .containsExactly(mapper.writeValueAsString(expected));
         }
     
         @Test
    @@ -282,9 +269,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             final Result> result = mgidBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -332,7 +321,7 @@ public void makeBidsShouldReturnBannerBidWhenBannerIsPresentAndExtCrtypeIsNotSet
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    @@ -352,7 +341,7 @@ public void makeBidsShouldReturnXNativeBidWhenBidIsPresentAndExtCrtypeIsNative()
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").ext(crtypeNode).build(), xNative, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").ext(crtypeNode).build(), xNative, "USD"));
         }
     
         @Test
    @@ -372,11 +361,22 @@ public void makeBidsShouldReturnBannerBidWhenBidIsPresentAndExtCrtypeIsBlank() t
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").ext(crtypeNode).build(), banner, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").ext(crtypeNode).build(), banner, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(mgidBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
         }
     }
    diff --git a/src/test/java/org/prebid/server/bidder/mobfoxpb/MobfoxpbBidderTest.java b/src/test/java/org/prebid/server/bidder/mobfoxpb/MobfoxpbBidderTest.java
    new file mode 100644
    index 00000000000..afafb34c6a1
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/mobfoxpb/MobfoxpbBidderTest.java
    @@ -0,0 +1,283 @@
    +package org.prebid.server.bidder.mobfoxpb;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.mobfoxpb.ExtImpMobfoxpb;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +import java.util.function.UnaryOperator;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class MobfoxpbBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.endpoint.com?c=__route__&m=__method__&key=__key__";
    +
    +    private MobfoxpbBidder mobfoxpbBidder;
    +
    +    @Before
    +    public void setUp() {
    +        mobfoxpbBidder = new MobfoxpbBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new MobfoxpbBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +
    +        // when
    +        final Result>> result = mobfoxpbBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).allSatisfy(error -> {
    +            assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +            assertThat(error.getMessage()).startsWith("Cannot deserialize instance");
    +        });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtDoesNotContainRequiredAttributes() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpMobfoxpb.of("", null)))));
    +
    +        // when
    +        final Result>> result = mobfoxpbBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(
    +                        BidderError.badInput("Invalid or non existing key and tagId, atleast one should be present"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetKeyRtbRouteAndMethodToUrlIfKeyParamIsPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = mobfoxpbBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://test.endpoint.com?c=rtb&m=req&key=key");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetNativeRouteAndMethodToUrlIfKeyParamIsPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpMobfoxpb.of("tagId", null))))
    +        );
    +
    +        // when
    +        final Result>> result = mobfoxpbBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://test.endpoint.com?c=o&m=ortb&key=__key__");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSendOnlyOneImp() {
    +        // given
    +        final Imp firstImp = givenImp(impBuilder -> impBuilder.id("firstImpId"));
    +        final List imps = Arrays.asList(firstImp, givenImp(identity()), givenImp(identity()));
    +        final BidRequest bidRequest =
    +                BidRequest.builder()
    +                        .imp(imps)
    +                        .build();
    +
    +        // when
    +        final Result>> result = mobfoxpbBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getId)
    +                .containsExactly("firstImpId");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = mobfoxpbBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).allSatisfy(error -> {
    +            assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +            assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()),
    +                mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = mobfoxpbBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()),
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = mobfoxpbBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build())),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = mobfoxpbBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(impBuilder -> impBuilder.video(Video.builder().build())),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = mobfoxpbBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(impBuilder -> impBuilder.xNative(Native.builder().build())),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = mobfoxpbBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnResponseWithErrorWhenIdIsNotFound() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("no"))));
    +
    +        // when
    +        final Result> result = mobfoxpbBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsOnly(BidderError.badInput("Failed to find impression \"no\""));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpMobfoxpb.of("tagId", "key")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java b/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java
    index 0bf9bb83aec..d8d7635dad3 100644
    --- a/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java
    @@ -5,6 +5,7 @@
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Native;
     import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
    @@ -24,11 +25,12 @@
     import java.util.List;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
    +import static java.util.Arrays.asList;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
     import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
     import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
     
    @@ -50,25 +52,28 @@ public void creationShouldFailOnInvalidEndpointUrl() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldCreateCorrectURLWhenTagidSrcIsAnyValue() {
    +    public void makeHttpRequestsShouldCreateCorrectURLWhenTagidSrcEqualsExt() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     impBuilder -> impBuilder
                             .banner(Banner.builder()
                                     .format(singletonList(Format.builder().w(300).h(500).build()))
    -                                .build()));
    +                                .build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                ExtImpMobilefuse.of(1, 2, "ext")))));
     
             // when
             final Result>> result = mobilefuseBidder.makeHttpRequests(bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1);
    -        assertThat(result.getValue().get(0).getUri()).isEqualTo("https://test.endpoint.com/openrtb?pub_id=2");
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://test.endpoint.com/openrtb?pub_id=2&tagid_src=ext");
         }
     
         @Test
    -    public void makeHttpRequestsShouldCreateCorrectURLWhenTagidSrcEqualsExt() {
    +    public void makeHttpRequestsShouldSetPubIdToZeroIfPublisherIdNotPresentInRequest() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     impBuilder -> impBuilder
    @@ -76,16 +81,16 @@ public void makeHttpRequestsShouldCreateCorrectURLWhenTagidSrcEqualsExt() {
                                     .format(singletonList(Format.builder().w(300).h(500).build()))
                                     .build())
                             .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                ExtImpMobilefuse.of(1, 2, "ext")))));
    +                                ExtImpMobilefuse.of(1, null, null)))));
     
             // when
             final Result>> result = mobilefuseBidder.makeHttpRequests(bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1);
    -        assertThat(result.getValue().get(0).getUri())
    -                .isEqualTo("https://test.endpoint.com/openrtb?pub_id=2&tagid_src=ext");
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://test.endpoint.com/openrtb?pub_id=0");
         }
     
         @Test
    @@ -106,12 +111,96 @@ public void makeHttpRequestsShouldModifyImpWithEmptyVideoWhenBannerAndVideoAreNo
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getVideo)
                     .containsNull();
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldCreateRequestWithOnlyFirstValidImp() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(givenImp(identity()),
    +                        givenImp(impBuilder -> impBuilder.id("456"))))
    +                .build();
    +
    +        // when
    +        final Result>> result = mobilefuseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getId)
    +                .containsExactly("123");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfNoValidExtFound() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
    +                .id("456")
    +                .banner(null)
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +
    +        // when
    +        final Result>> result = mobilefuseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Invalid ExtImpMobilefuse value"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfNoValidImpsFound() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(
    +                        givenImp(impBuilder -> impBuilder
    +                                .id("456")
    +                                .banner(null)
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))),
    +                        givenImp(impBuilder -> impBuilder.banner(null).xNative(Native.builder().build()))))
    +                .build();
    +
    +        // when
    +        final Result>> result = mobilefuseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("No valid imps"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateRequestUsingParamsFromFirstValidExt() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(
    +                        givenImp(impBuilder -> impBuilder
    +                                .id("456")
    +                                .banner(null)
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))),
    +                        givenImp(impBuilder -> impBuilder.banner(null).xNative(Native.builder().build())),
    +                        givenImp(impBuilder -> impBuilder
    +                                .id("789")
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))))))
    +                .build();
    +
    +        // when
    +        final Result>> result = mobilefuseBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getId, Imp::getTagid)
    +                .containsExactly(tuple("789", "1"));
    +    }
    +
         @Test
         public void makeHttpRequestsShouldModifyImpWhenBannerOrVideoAreNotEmpty() {
             // given
    @@ -129,15 +218,10 @@ public void makeHttpRequestsShouldModifyImpWhenBannerOrVideoAreNotEmpty() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getExt)
    -                .containsNull();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getTagid)
    -                .contains("1");
    +                .extracting(Imp::getExt, Imp::getTagid)
    +                .containsExactly(tuple(null, "1"));
         }
     
         @Test
    @@ -149,10 +233,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             final Result> result = mobilefuseBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage())
    -                .startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                });
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -200,7 +285,7 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         @Test
    @@ -218,26 +303,7 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws Js
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    -    }
    -
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = mobilefuseBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(mobilefuseBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
         private static BidRequest givenBidRequest(
    @@ -256,7 +322,8 @@ private static BidRequest givenBidRequest(Function impCustomizer) {
             return impCustomizer.apply(Imp.builder()
                     .id("123")
    -                .banner(Banner.builder().id("banner_id").build()).ext(mapper.valueToTree(ExtPrebid.of(null,
    +                .banner(Banner.builder().id("banner_id").build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
                             ExtImpMobilefuse.of(1, 2, "tagidSrc")))))
                     .build();
         }
    diff --git a/src/test/java/org/prebid/server/bidder/nanointeractive/NanointeractiveBidderTest.java b/src/test/java/org/prebid/server/bidder/nanointeractive/NanointeractiveBidderTest.java
    index 0f9b5058178..eadcbf4c809 100644
    --- a/src/test/java/org/prebid/server/bidder/nanointeractive/NanointeractiveBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/nanointeractive/NanointeractiveBidderTest.java
    @@ -30,7 +30,6 @@
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -190,11 +189,6 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces
             assertThat(result.getValue()).isEmpty();
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(nanointeractiveBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer) {
    diff --git a/src/test/java/org/prebid/server/bidder/ninthdecimal/NinthdecimalBidderTest.java b/src/test/java/org/prebid/server/bidder/ninthdecimal/NinthdecimalBidderTest.java
    index 0804773b52e..5cf4114b2ac 100644
    --- a/src/test/java/org/prebid/server/bidder/ninthdecimal/NinthdecimalBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/ninthdecimal/NinthdecimalBidderTest.java
    @@ -33,7 +33,6 @@
     
     import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -181,7 +180,7 @@ public void makeHttpRequestsShouldFillMethodAndUrlAndExpectedHeaders() {
                     .containsOnly(
                             tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), "application/json;charset=utf-8"),
                             tuple(HttpUtil.ACCEPT_HEADER.toString(), "application/json"),
    -                        tuple("x-openrtb-version", "2.5"));
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"));
         }
     
         @Test
    @@ -404,11 +403,6 @@ public void makeBidsShouldReturnVideoBidWhenVideoPresent() throws JsonProcessing
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(ninthdecimalBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer,
    diff --git a/src/test/java/org/prebid/server/bidder/nobid/NobidBidderTest.java b/src/test/java/org/prebid/server/bidder/nobid/NobidBidderTest.java
    new file mode 100644
    index 00000000000..f24d9357f7d
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/nobid/NobidBidderTest.java
    @@ -0,0 +1,196 @@
    +package org.prebid.server.bidder.nobid;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +
    +public class NobidBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.com/prebid/bid&key={{AccountID}}";
    +
    +    private NobidBidder nobidBidder;
    +
    +    @Before
    +    public void setUp() {
    +        nobidBidder = new NobidBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new NobidBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = nobidBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors())
    +                .allMatch(error -> error.getType() == BidderError.Type.bad_server_response
    +                        && error.getMessage().startsWith("Failed to decode: Unrecognized token"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = nobidBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = nobidBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = nobidBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBidFromEverySeatBid() throws JsonProcessingException {
    +        // given
    +        final SeatBid firstSeatBId = SeatBid.builder()
    +                .bid(singletonList(Bid.builder()
    +                        .impid("123")
    +                        .build()))
    +                .build();
    +
    +        final SeatBid secondSeatBid = SeatBid.builder()
    +                .bid(singletonList(Bid.builder()
    +                        .impid("456")
    +                        .build()))
    +                .build();
    +
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(Arrays.asList(Imp.builder().id("123").banner(Banner.builder().build()).build(),
    +                                Imp.builder().id("456").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(BidResponse.builder()
    +                        .cur("USD")
    +                        .seatbid(Arrays.asList(firstSeatBId, secondSeatBid))
    +                        .build()));
    +
    +        // when
    +        final Result> result = nobidBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"),
    +                        BidderBid.of(Bid.builder().impid("456").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = nobidBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfImpWasNotFound() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("125"))));
    +
    +        // when
    +        final Result> result = nobidBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Failed to find impression 125"));
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/onetag/OnetagBidderTest.java b/src/test/java/org/prebid/server/bidder/onetag/OnetagBidderTest.java
    new file mode 100644
    index 00000000000..1ad6bceec87
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/onetag/OnetagBidderTest.java
    @@ -0,0 +1,362 @@
    +package org.prebid.server.bidder.onetag;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.onetag.ExtImpOnetag;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class OnetagBidderTest extends VertxTest {
    +
    +    public static final String ENDPOINT_URL = "https://test.endpoint.com/{{publisherId}}";
    +
    +    private OnetagBidder onetagBidder;
    +
    +    @Before
    +    public void setUp() {
    +        onetagBidder = new OnetagBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new OnetagBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = onetagBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getUri)
    +                .containsExactly("https://test.endpoint.com/somePubId");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestWithAllImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        givenImp(identity()))));
    +
    +        // when
    +        final Result>> result = onetagBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .hasSize(2);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCanNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(Imp.builder()
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
    +                                .build(),
    +                        givenImp(identity())))
    +                .build();
    +
    +        // when
    +        final Result>> result = onetagBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(error.getMessage()).startsWith("Cannot deserialize instance");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpExt() {
    +        // given
    +        final ObjectNode oneTagExt = mapper.createObjectNode();
    +        oneTagExt.put("someField", "someName");
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpOnetag.of("somePubId", oneTagExt))))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = onetagBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(oneTagExt);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfPubIdNotPresent() {
    +        // given
    +        final ObjectNode oneTagExt = mapper.createObjectNode();
    +        oneTagExt.put("someField", "someName");
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpOnetag.of("", oneTagExt))))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = onetagBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("The publisher ID must not be empty"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfPubIdAreDifferentInImpExts() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        Imp.builder()
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpOnetag.of("anotherPubId",
    +                                        mapper.createObjectNode()))))
    +                                .build())));
    +
    +        // when
    +        final Result>> result = onetagBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("There must be only one publisher ID"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = onetagBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = onetagBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = onetagBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfImpWasNotFound() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("125"))));
    +
    +        // when
    +        final Result> result = onetagBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(
    +                        BidderError.badServerResponse("The impression with ID 125 is not present into the request"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = onetagBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = onetagBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNoBannerAndVideoButNativeIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().xNative(Native.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = onetagBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(givenImp(identity())))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = onetagBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = onetagBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer) {
    +        return givenBidRequest(impCustomizer, identity());
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpOnetag.of("somePubId", mapper.createObjectNode()))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java
    index 19ec9ccb617..7e94f1f7475 100644
    --- a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java
    @@ -201,16 +201,17 @@ public void makeHttpRequestsShouldReturnResultWithExpectedFieldsSet() {
                     .imp(asList(
                             Imp.builder()
                                     .id("impId1")
    +                                .bidfloor(BigDecimal.valueOf(0.5))
                                     .banner(Banner.builder().build())
                                     .ext(mapper.valueToTree(
                                             ExtPrebid.of(null,
                                                     ExtImpOpenx.builder()
    -                                                        .customFloor(BigDecimal.valueOf(0.1))
                                                             .customParams(givenCustomParams("foo1", singletonList("bar1")))
                                                             .delDomain("se-demo-d.openx.net")
                                                             .unit("unitId").build()))).build(),
                             Imp.builder()
                                     .id("impId2")
    +                                .bidfloor(BigDecimal.valueOf(0.5))
                                     .banner(Banner.builder().build())
                                     .ext(mapper.valueToTree(
                                             ExtPrebid.of(null,
    @@ -237,7 +238,6 @@ public void makeHttpRequestsShouldReturnResultWithExpectedFieldsSet() {
                                     .ext(mapper.valueToTree(
                                             ExtPrebid.of(null,
                                                     ExtImpOpenx.builder()
    -                                                        .customFloor(BigDecimal.valueOf(0.1))
                                                             .customParams(givenCustomParams("foo4", "bar4"))
                                                             .platform("PLATFORM")
                                                             .unit("unitId").build()))).build(),
    @@ -266,7 +266,7 @@ public void makeHttpRequestsShouldReturnResultWithExpectedFieldsSet() {
                                                     .id("impId1")
                                                     .banner(Banner.builder().build())
                                                     .tagid("unitId")
    -                                                .bidfloor(BigDecimal.valueOf(0.1))
    +                                                .bidfloor(BigDecimal.valueOf(0.5))
                                                     .ext(mapper.valueToTree(
                                                             ExtImpOpenx.builder()
                                                                     .customParams(
    @@ -278,7 +278,7 @@ public void makeHttpRequestsShouldReturnResultWithExpectedFieldsSet() {
                                                     .id("impId2")
                                                     .banner(Banner.builder().build())
                                                     .tagid("unitId")
    -                                                .bidfloor(BigDecimal.valueOf(0.1))
    +                                                .bidfloor(BigDecimal.valueOf(0.5))
                                                     .ext(mapper.valueToTree(
                                                             ExtImpOpenx.builder()
                                                                     .customParams(
    @@ -328,7 +328,6 @@ public void makeHttpRequestsShouldReturnResultWithExpectedFieldsSet() {
                                                     .id("impId4")
                                                     .video(Video.builder().build())
                                                     .tagid("unitId")
    -                                                .bidfloor(BigDecimal.valueOf(0.1))
                                                     .ext(mapper.valueToTree(
                                                             ExtImpOpenx.builder()
                                                                     .customParams(
    @@ -344,6 +343,52 @@ public void makeHttpRequestsShouldReturnResultWithExpectedFieldsSet() {
                                     .build());
         }
     
    +    @Test
    +    public void makeHttpRequestShouldReturnResultWithCustomBidFloorIfImpBidFloorIsZero() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .bidfloor(BigDecimal.ZERO)
    +                        .video(Video.builder().build())
    +                        .ext(mapper.valueToTree(
    +                                ExtPrebid.of(null, ExtImpOpenx.builder().customFloor(BigDecimal.valueOf(123)).build()))
    +                        ).build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = openxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor)
    +                .containsExactly(BigDecimal.valueOf(123));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestShouldReturnResultWithCustomBidFloorIfImpBidFloorIsNegative() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .bidfloor(BigDecimal.ZERO.subtract(BigDecimal.ONE))
    +                        .video(Video.builder().build())
    +                        .ext(mapper.valueToTree(
    +                                ExtPrebid.of(null, ExtImpOpenx.builder().customFloor(BigDecimal.valueOf(123)).build()))
    +                        ).build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = openxBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor)
    +                .containsExactly(BigDecimal.valueOf(123));
    +    }
    +
         @Test
         public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             // given
    @@ -462,11 +507,6 @@ public void makeBidsShouldReturnResultContainingEmptyValueAndErrorsWhenSeatBidEm
                     .containsOnly(Collections.emptyList(), Collections.emptyList());
         }
     
    -    @Test
    -    public void extractTargeting() {
    -        assertThat(openxBidder.extractTargeting(mapper.createObjectNode())).isEmpty();
    -    }
    -
         private static Map givenCustomParams(String key, Object values) {
             return singletonMap(key, mapper.valueToTree(values));
         }
    diff --git a/src/test/java/org/prebid/server/bidder/orbidder/OrbidderBidderTest.java b/src/test/java/org/prebid/server/bidder/orbidder/OrbidderBidderTest.java
    index 650641a7f2f..22d716976bf 100644
    --- a/src/test/java/org/prebid/server/bidder/orbidder/OrbidderBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/orbidder/OrbidderBidderTest.java
    @@ -22,7 +22,6 @@
     import java.util.function.Function;
     
     import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.Collections.singletonMap;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -44,20 +43,6 @@ public void creationShouldFailOnInvalidEndpointUrl() {
             assertThatIllegalArgumentException().isThrownBy(() -> new OrbidderBidder("invalid_url", jacksonMapper));
         }
     
    -    @Test
    -    public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() {
    -        // given
    -        final HttpCall httpCall = HttpCall
    -                .success(null, HttpResponse.of(204, null, null), null);
    -
    -        // when
    -        final Result> result = orbidderBidder.makeBids(httpCall, null);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
         @Test
         public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             // given
    @@ -107,11 +92,6 @@ public void makeBidsShouldReturnErrorsWhenBidsEmptyList()
                     .containsOnly(Collections.emptyList(), Collections.emptyList());
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(orbidderBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
                     .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    diff --git a/src/test/java/org/prebid/server/bidder/outbrain/OutbrainBidderTest.java b/src/test/java/org/prebid/server/bidder/outbrain/OutbrainBidderTest.java
    new file mode 100644
    index 00000000000..c6ef4f1fed9
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/outbrain/OutbrainBidderTest.java
    @@ -0,0 +1,345 @@
    +package org.prebid.server.bidder.outbrain;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.App;
    +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.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.EventTracker;
    +import com.iab.openrtb.response.Response;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.outbrains.ExtImpOutbrain;
    +import org.prebid.server.proto.openrtb.ext.request.outbrains.ExtImpOutbrainPublisher;
    +
    +import java.util.List;
    +import java.util.function.Function;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class OutbrainBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private OutbrainBidder outbrainBidder;
    +
    +    @Before
    +    public void setUp() {
    +        outbrainBidder = new OutbrainBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new OutbrainBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateOneSingleRequestWithAllImps() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(givenImp(identity()), givenImp(identity())))
    +                .build();
    +
    +        // when
    +        final Result>> result = outbrainBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .hasSize(2);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldModifyImpTagIdOnlyIfItPresentInExt() {
    +        // given
    +        final Imp firstImp = Imp.builder()
    +                .id("123")
    +                .tagid("123")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpOutbrain.of(
    +                        ExtImpOutbrainPublisher.of("testId", "testName", "testDomain"),
    +                        null, singletonList("testBcat"), singletonList("testBadv")))))
    +                .build();
    +
    +        final Imp thirdImp = Imp.builder()
    +                .id("789")
    +                .tagid("789")
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpOutbrain.of(
    +                        ExtImpOutbrainPublisher.of("testId", "testName", "testDomain"),
    +                        "098", singletonList("testBcat"), singletonList("testBadv")))))
    +                .build();
    +
    +        final BidRequest bidRequest = BidRequest.builder().imp(asList(firstImp, thirdImp)).build();
    +
    +        // when
    +        final Result>> result = outbrainBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .containsExactlyInAnyOrder(firstImp, thirdImp.toBuilder().tagid("098").build());
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdatePresentedAppWithPublisherParamsFromExt() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.app(App.builder().build()),
    +                identity());
    +
    +        // when
    +        final Result>> result = outbrainBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final App expectedApp = App.builder()
    +                .publisher(Publisher.builder().id("testId").name("testName").domain("testDomain").build())
    +                .build();
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getApp)
    +                .containsExactly(expectedApp);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdatePresentedSiteWithPublisherParamsFromExt() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.site(Site.builder().build()),
    +                identity());
    +
    +        // when
    +        final Result>> result = outbrainBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final Site expectedSite = Site.builder()
    +                .publisher(Publisher.builder().id("testId").name("testName").domain("testDomain").build())
    +                .build();
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getSite)
    +                .containsExactly(expectedSite);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = outbrainBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Impression id=123, has invalid Ext"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = outbrainBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = outbrainBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = outbrainBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = outbrainBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().xNative(Native.builder().build()).id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = outbrainBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnResponseWithErrorWhenIdIsNotFound() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("12"))));
    +
    +        // when
    +        final Result> result = outbrainBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badServerResponse("Failed to find native/banner impression \"12\""));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidWithModifiedImpIfNativeIsPresent() throws JsonProcessingException {
    +        // given
    +        final String impUrl = "impUrl";
    +        final String jsUrl = "jsUrl";
    +        final EventTracker impTracker = EventTracker.builder()
    +                .event(1)
    +                .method(1)
    +                .url(impUrl)
    +                .build();
    +        final EventTracker jsTracker = EventTracker.builder()
    +                .event(1)
    +                .method(2)
    +                .url(jsUrl)
    +                .build();
    +
    +        final List eventTrackers = asList(impTracker, jsTracker);
    +
    +        final Response nativeResponse = Response.builder()
    +                .eventtrackers(eventTrackers)
    +                .build();
    +
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().xNative(Native.builder().build()).id("123").build())).build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123")
    +                                .adm(jacksonMapper.encode(nativeResponse)))));
    +
    +        // when
    +        final Result> result = outbrainBidder.makeBids(httpCall, null);
    +
    +        // then
    +        final String expectedAdm = jacksonMapper.encode(Response.builder()
    +                .eventtrackers(null)
    +                .jstracker(String.format("", jsUrl))
    +                .imptrackers(singletonList(impUrl))
    +                .build());
    +
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getAdm)
    +                .containsExactly(expectedAdm);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().w(23).h(25).build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpOutbrain.of(
    +                        ExtImpOutbrainPublisher.of("testId", "testName", "testDomain"),
    +                        "tagId", singletonList("testBcat"), singletonList("testBadv"))))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/pangle/PangleBidderTest.java b/src/test/java/org/prebid/server/bidder/pangle/PangleBidderTest.java
    new file mode 100644
    index 00000000000..7b45bfb4ffc
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/pangle/PangleBidderTest.java
    @@ -0,0 +1,613 @@
    +package org.prebid.server.bidder.pangle;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.iab.openrtb.request.Audio;
    +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.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import io.netty.handler.codec.http.HttpHeaderValues;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.bidder.pangle.model.BidExt;
    +import org.prebid.server.bidder.pangle.model.NetworkIds;
    +import org.prebid.server.bidder.pangle.model.PangleBidExt;
    +import org.prebid.server.bidder.pangle.model.WrappedImpExtBidder;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.pangle.ExtImpPangle;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
    +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;
    +
    +public class PangleBidderTest extends VertxTest {
    +
    +    public static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private PangleBidder pangleBidder;
    +
    +    @Before
    +    public void setUp() {
    +        pangleBidder = new PangleBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new PangleBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldMakeOneRequestPerImp() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                requestBuilder -> requestBuilder.imp(Arrays.asList(
    +                        givenImp(identity()),
    +                        givenImp(identity()))));
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(2)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .hasSize(2);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpExtWithAdTypeSevenIfVideoIsPresentAndIsRewardedIsOne() {
    +        // given
    +        final ExtImpPrebid extPrebid = ExtImpPrebid.builder().isRewardedInventory(1).build();
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .video(Video.builder().build())
    +                        .ext(mapper.valueToTree(WrappedImpExtBidder.of(extPrebid, ExtImpPangle.of(
    +                                "token", null, null), null, true, null)))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedExt = mapper
    +                .valueToTree(WrappedImpExtBidder.of(extPrebid, ExtImpPangle.of("token", null, null), 7, true, null));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(expectedExt);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpExtWithAdTypeEightIfVideoIsPresentAndInstlIsOne() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .instl(1)
    +                        .video(Video.builder().build())
    +                        .ext(mapper.valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                                "token", null, null), null, true, null)))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedExt = mapper
    +                .valueToTree(WrappedImpExtBidder.of(null,
    +                        ExtImpPangle.of("token", null, null), 8, true, null));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(expectedExt);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpExtWithAdTypeTwoIfBannerIsPresentAndInstlIsOne() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .instl(1)
    +                        .banner(Banner.builder().build())
    +                        .ext(mapper.valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                                "token", null, null), null, true, null)))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedExt = mapper
    +                .valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of("token", null, null), 2, true, null));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(expectedExt);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpExtWithAdTypeOneIfBannerIsPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().build())
    +                        .ext(mapper.valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                                "token", null, null), null, true, null)))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedExt = mapper
    +                .valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                        "token", null, null), 1, true, null));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(expectedExt);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpExtWithAdTypeFiveIfNativeRequestIsPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .xNative(Native.builder().request("someRequest").build())
    +                        .ext(mapper.valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                                "token", null, null), null, true, null)))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedExt = mapper
    +                .valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                        "token", null, null), 5, true, null));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(expectedExt);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorForNotSupportedAdType() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .audio(Audio.builder().build())
    +                        .ext(mapper.valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                                "token", null, null), null, true, null)))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("not a supported adtype"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCorrectlyAddHeaders() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .flatExtracting(res -> res.getHeaders().entries())
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .containsExactlyInAnyOrder(
    +                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    +                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()),
    +                        tuple("TOKEN", "token"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(BidResponse.builder()
    +                        .seatbid(singletonList(SeatBid.builder()
    +                                .bid(singletonList(null))
    +                                .build()))
    +                        .build()));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(
    +                        BidderError.badServerResponse("the bid request object is not present"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidExtIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(givenBidResponse(identity())));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(
    +                        BidderError.badServerResponse("missing pangleExt/adtype in bid ext"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidExtAdTypeIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bid -> bid.ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(null)))))));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(
    +                        BidderError.badServerResponse("missing pangleExt/adtype in bid ext"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfAdTypeIsOne() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(givenBidResponse(bid ->
    +                        bid.ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(1)))))));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder()
    +                        .ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(1))))
    +                        .build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfAdTypeIsTwo() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(givenBidResponse(bid ->
    +                        bid.ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(2)))))));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder()
    +                        .ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(2))))
    +                        .build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfAdTypeIsFive() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(givenBidResponse(bid ->
    +                        bid.ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(5)))))));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder()
    +                        .ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(5))))
    +                        .build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVide0BidIfAdTypeIsSeven() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(givenBidResponse(bid ->
    +                        bid.ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(7)))))));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder()
    +                        .ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(7))))
    +                        .build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVide0BidIfAdTypeIsEight() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(givenBidResponse(bid ->
    +                        bid.ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(8)))))));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsExactly(BidderBid.of(Bid.builder()
    +                        .ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(8))))
    +                        .build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorForUnrecognizedAdType() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(givenBidResponse(bid ->
    +                        bid.ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(777)))))));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(
    +                        BidderError.badServerResponse("unrecognized adtype in response"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorsAndResult() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(BidResponse.builder()
    +                        .cur("USD")
    +                        .seatbid(singletonList(SeatBid.builder()
    +                                .bid(Arrays.asList(Bid.builder().build(),
    +                                        Bid.builder()
    +                                                .ext(mapper.valueToTree(PangleBidExt.of(BidExt.of(8))))
    +                                                .build()))
    +                                .build()))
    +                        .build()));
    +
    +        // when
    +        final Result> result = pangleBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getErrors()).hasSize(1);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldUpdateImpExtWithNetworkIds() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().w(1).h(1).build())
    +                        .ext(mapper.valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                                "token", "2", "2"),
    +                                null, true, NetworkIds.of("1", "1"))))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedExt = mapper
    +                .valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                        "token", "2", "2"),
    +                        1, true, NetworkIds.of("2", "2")));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(expectedExt);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldAddErrorAndSkipImpressionIfAppIdIsNotPresentAndPlacementIdIsPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().w(1).h(1).build())
    +                        .ext(mapper.valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                                "token", null, "1"),
    +                                null, true, NetworkIds.of("1", "1"))))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldAddErrorAndSkipImpressionIfPlacementIdIsNotPresentAndAppIdIsPresent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().w(1).h(1).build())
    +                        .ext(mapper.valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                                "token", "1", null),
    +                                null, true, NetworkIds.of("1", "1"))))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotUpdateImpExtWithNetworkIdsIfNetworkIdsAreAbsent() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .banner(Banner.builder().w(1).h(1).build())
    +                        .ext(mapper.valueToTree(WrappedImpExtBidder.of(null, ExtImpPangle.of(
    +                                "token", null, null),
    +                                null, true, null)))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pangleBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedExt = mapper
    +                .valueToTree(WrappedImpExtBidder.of(null,
    +                        ExtImpPangle.of("token", null, null), 1, true, null));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(expectedExt);
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer,
    +            Function requestCustomizer) {
    +        return requestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function impCustomizer) {
    +        return givenBidRequest(impCustomizer, identity());
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123"))
    +                .banner(Banner.builder().build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpPangle.of("token", null, null))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body), null);
    +    }
    +}
    +
    diff --git a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticAdapterTest.java b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticAdapterTest.java
    deleted file mode 100644
    index 76889624773..00000000000
    --- a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticAdapterTest.java
    +++ /dev/null
    @@ -1,649 +0,0 @@
    -package org.prebid.server.bidder.pubmatic;
    -
    -import com.fasterxml.jackson.databind.node.ObjectNode;
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.Banner;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.Device;
    -import com.iab.openrtb.request.Format;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Publisher;
    -import com.iab.openrtb.request.Regs;
    -import com.iab.openrtb.request.Site;
    -import com.iab.openrtb.request.Source;
    -import com.iab.openrtb.request.User;
    -import com.iab.openrtb.response.Bid;
    -import com.iab.openrtb.response.BidResponse;
    -import com.iab.openrtb.response.SeatBid;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.auction.model.AdapterRequest;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    -import org.prebid.server.bidder.model.AdapterHttpRequest;
    -import org.prebid.server.bidder.model.ExchangeCall;
    -import org.prebid.server.bidder.pubmatic.proto.PubmaticParams;
    -import org.prebid.server.bidder.pubmatic.proto.PubmaticRequestExt;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.request.PreBidRequest;
    -import org.prebid.server.proto.request.Video;
    -import org.prebid.server.proto.response.BidderDebug;
    -import org.prebid.server.proto.response.MediaType;
    -
    -import java.io.IOException;
    -import java.math.BigDecimal;
    -import java.util.EnumSet;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.function.Function;
    -import java.util.stream.Collectors;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptySet;
    -import static java.util.Collections.singleton;
    -import static java.util.Collections.singletonList;
    -import static java.util.Collections.singletonMap;
    -import static java.util.function.Function.identity;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    -import static org.assertj.core.api.Assertions.assertThatThrownBy;
    -import static org.assertj.core.api.Assertions.tuple;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -
    -public class PubmaticAdapterTest extends VertxTest {
    -
    -    private static final String BIDDER = "pubmatic";
    -    private static final String COOKIE_FAMILY = BIDDER;
    -    private static final String ENDPOINT_URL = "http://endpoint.org/";
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private UidsCookie uidsCookie;
    -
    -    private AdapterRequest adapterRequest;
    -    private PreBidRequestContext preBidRequestContext;
    -    private ExchangeCall exchangeCall;
    -    private PubmaticAdapter adapter;
    -
    -    @Before
    -    public void setUp() {
    -        adapterRequest = givenBidderCustomizable(identity());
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), identity());
    -        adapter = new PubmaticAdapter(COOKIE_FAMILY, ENDPOINT_URL, jacksonMapper);
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException()
    -                .isThrownBy(() -> new PubmaticAdapter(COOKIE_FAMILY, "invalid_url", jacksonMapper))
    -                .withMessage("URL supplied is not valid: invalid_url");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedHeaders() {
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).flatExtracting(r -> r.getHeaders().entries())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple("Content-Type", "application/json;charset=utf-8"),
    -                        tuple("Accept", "application/json"),
    -                        tuple("Set-Cookie", "KADUSERCOOKIE="));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfNoAtLeastOneValidAdunitBidParams() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBidCustomizable(builder -> builder.params(null))));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessageStartingWith("Incorrect adSlot / Publisher param, Error list: [");
    -    }
    -
    -    @Test
    -    public void requestBidsShouldSendBidRequestWithNotModifiedImpIfInvalidParams() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBidCustomizable(identity()),
    -                givenAdUnitBidCustomizable(builder -> builder.params(mapper.valueToTree(
    -                        PubmaticParams.of(null, null, null, null)))),
    -                givenAdUnitBidCustomizable(builder -> builder.params(mapper.valueToTree(
    -                        PubmaticParams.of("publisherID", null, null, null)))),
    -                givenAdUnitBidCustomizable(builder -> builder.params(mapper.valueToTree(
    -                        PubmaticParams.of("publisherID", "slot42", null, null)))),
    -                givenAdUnitBidCustomizable(builder -> builder.params(mapper.valueToTree(
    -                        PubmaticParams.of("publisherID", "slot42@200", null, null)))),
    -                givenAdUnitBidCustomizable(builder -> builder.params(mapper.valueToTree(
    -                        PubmaticParams.of("publisherID", "slot42@200xNonNumber", null, null))))));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUid");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).flatExtracting(r -> r.getPayload().getImp()).hasSize(6);
    -        assertThat(httpRequests).flatExtracting(r -> r.getPayload().getImp())
    -                .extracting(Imp::getTagid)
    -                .containsOnly("slot1", null, null, "slot42", null, null);
    -
    -        final List formats = singletonList(Format.builder().w(480).h(320).build());
    -        assertThat(httpRequests).flatExtracting(r -> r.getPayload().getImp())
    -                .extracting(Imp::getBanner).extracting(Banner::getFormat)
    -                .containsOnly(formats, formats, formats, formats, formats, formats);
    -
    -        assertThat(httpRequests).flatExtracting(r -> r.getPayload().getImp())
    -                .extracting(Imp::getBanner).extracting(Banner::getW)
    -                .containsOnly(300, 480, 480, 480, 480, 480);
    -
    -        assertThat(httpRequests).flatExtracting(r -> r.getPayload().getImp())
    -                .extracting(Imp::getBanner).extracting(Banner::getH)
    -                .containsOnly(250, 320, 320, 320, 320, 320);
    -    }
    -
    -    @Test
    -    public void requestBidsShouldTolerateAdSlotExtraSuffix() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder
    -                        .params(mapper.valueToTree(
    -                                PubmaticParams.of("publisherID", "slot42@200x150:zzz", null, null))));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUid");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).isNotEmpty();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBidCustomizable(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .mediaTypes(emptySet()))));
    -
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("openRTB bids need at least one Imp");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsVideoAndMimesListIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBidCustomizable(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .mediaTypes(singleton(MediaType.video))
    -                        .video(Video.builder()
    -                                .mimes(emptyList())
    -                                .build()))));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid AdUnit: VIDEO media type with no video data");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedFields() throws IOException {
    -        // given
    -        final ObjectNode wrapExt = mapper.valueToTree(singletonMap("key", 1));
    -        final Map keywords = singletonMap("key1", "value1");
    -
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder
    -                        .bidderCode(BIDDER)
    -                        .adUnitCode("adUnitCode1")
    -                        .instl(1)
    -                        .topframe(1)
    -                        .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                        .params(mapper.valueToTree(
    -                                PubmaticParams.of("publisherID", "slot42@200x150:zzz", wrapExt, keywords))));
    -
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(
    -                builder -> builder
    -                        .referer("http://www.example.com")
    -                        .domain("example.com")
    -                        .ip("192.168.144.1")
    -                        .ua("userAgent"),
    -                builder -> builder
    -                        .timeoutMillis(1500L)
    -                        .tid("tid")
    -                        .user(User.builder()
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null))));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUid");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .containsOnly(BidRequest.builder()
    -                        .id("tid")
    -                        .at(1)
    -                        .tmax(1500L)
    -                        .imp(singletonList(Imp.builder()
    -                                .id("adUnitCode1")
    -                                .instl(1)
    -                                .tagid("slot42")
    -                                .banner(Banner.builder()
    -                                        .w(200)
    -                                        .h(150)
    -                                        .format(singletonList(Format.builder().w(300).h(250).build()))
    -                                        .topframe(1)
    -                                        .build())
    -                                .ext(mapper.readValue("{\"key1\":\"value1\"}", ObjectNode.class))
    -                                .build()))
    -                        .site(Site.builder()
    -                                .domain("example.com")
    -                                .page("http://www.example.com")
    -                                .publisher(Publisher.builder().id("publisherID").domain("example.com").build())
    -                                .build())
    -                        .device(Device.builder()
    -                                .ua("userAgent")
    -                                .ip("192.168.144.1")
    -                                .build())
    -                        .user(User.builder()
    -                                .buyeruid("buyerUid")
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null)))
    -                        .source(Source.builder()
    -                                .fd(1)
    -                                .tid("tid")
    -                                .build())
    -                        .ext(jacksonMapper.fillExtension(
    -                                ExtRequest.empty(), PubmaticRequestExt.of(mapper.valueToTree(singletonMap("key", 1)))))
    -                        .build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestShouldNotChangeExtIfWrapExtIsMissing() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder
    -                        .params(mapper.valueToTree(PubmaticParams.of("publisherID", "slot42@200x150:zzz",
    -                                null, null))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .extracting(BidRequest::getExt)
    -                .containsNull();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestShouldNotChangeImpExtIfKeywordsAreMissing() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder
    -                        .params(mapper.valueToTree(PubmaticParams.of("publisherID", "slot42@200x150:zzz",
    -                                null, null))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getExt)
    -                .containsNull();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithAppFromPreBidRequest() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), builder -> builder
    -                .app(App.builder().id("appId").build()).user(User.builder().build()));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(r -> r.getPayload().getApp().getId())
    -                .containsOnly("appId");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithAppFromPreBidRequestWithNullCookie() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), builder -> builder
    -                .app(App.builder().id("appId").build()).user(User.builder().build()), null);
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(r -> r.getPayload().getApp().getId())
    -                .containsOnly("appId");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithUserFromPreBidRequestIfAppPresent() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), builder -> builder
    -                .app(App.builder().build())
    -                .user(User.builder().buyeruid("buyerUid").build()));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUidFromCookie");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(r -> r.getPayload().getUser())
    -                .containsOnly(User.builder().buyeruid("buyerUid").build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithOneRequestWithOneImpIfAdUnitContainsBannerAndVideoMediaTypes() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBidCustomizable(builder -> builder
    -                        .mediaTypes(EnumSet.of(MediaType.video, MediaType.banner))
    -                        .video(Video.builder()
    -                                .mimes(singletonList("Mime1"))
    -                                .playbackMethod(1)
    -                                .build()))));
    -
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), identity());
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp())
    -                .containsOnly(
    -                        Imp.builder()
    -                                .banner(Banner.builder().w(300).h(250)
    -                                        .format(singletonList(Format.builder().w(480).h(320).build())).build())
    -                                .tagid("slot1")
    -                                .video(com.iab.openrtb.request.Video.builder().w(480).h(320)
    -                                        .mimes(singletonList("Mime1")).playbackmethod(singletonList(1)).build())
    -                                .build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithOneRequestIfMultipleAdUnitsInPreBidRequest() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBidCustomizable(builder -> builder.adUnitCode("adUnitCode1")),
    -                givenAdUnitBidCustomizable(builder -> builder.adUnitCode("adUnitCode2"))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(2)
    -                .extracting(Imp::getId)
    -                .containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldFailIfBidImpIdDoesNotMatchAdUnitCode() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(builder -> builder.adUnitCode("adUnitCode"));
    -
    -        exchangeCall = givenExchangeCallCustomizable(identity(),
    -                builder -> builder.seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("anotherAdUnitCode").build()))
    -                        .build())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Unknown ad unit code 'anotherAdUnitCode'");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"));
    -
    -        exchangeCall = givenExchangeCallCustomizable(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .price(new BigDecimal("8.43"))
    -                                        .adm("adm")
    -                                        .crid("crid")
    -                                        .w(300)
    -                                        .h(250)
    -                                        .dealid("dealId")
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids)
    -                .containsExactly(org.prebid.server.proto.response.Bid.builder()
    -                        .code("adUnitCode")
    -                        .price(new BigDecimal("8.43"))
    -                        .adm("adm")
    -                        .creativeId("crid")
    -                        .width(300)
    -                        .height(250)
    -                        .bidder(BIDDER)
    -                        .bidId("bidId")
    -                        .dealId("dealId")
    -                        .mediaType(MediaType.banner)
    -                        .build());
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuilderWithBannerMediaTypeWhenCorrespondingImpWasNotFound() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(builder -> builder.adUnitCode("adUnitCode"));
    -
    -        exchangeCall = givenExchangeCallCustomizable(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().build())),
    -                bidResponseBuilder -> bidResponseBuilder.seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("adUnitCode").build())).build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(1)
    -                .extracting(org.prebid.server.proto.response.Bid::getMediaType)
    -                .containsExactly(MediaType.banner);
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuilderWithBannerMediaTypeWhenCorrespondingImpHasVideoTypeNull() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(builder -> builder.adUnitCode("adUnitCode"));
    -
    -        exchangeCall = givenExchangeCallCustomizable(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("adUnitCode").build())).build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(1)
    -                .extracting(org.prebid.server.proto.response.Bid::getMediaType)
    -                .containsExactly(MediaType.banner);
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuilderWithVideoMediaTypeWhenCorrespondingImpHasVideoType() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(builder -> builder.adUnitCode("adUnitCode"));
    -
    -        exchangeCall = givenExchangeCallCustomizable(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode")
    -                        .video(com.iab.openrtb.request.Video.builder().build()).build())),
    -                bidResponseBuilder -> bidResponseBuilder.seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("adUnitCode").build())).build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(1)
    -                .extracting(org.prebid.server.proto.response.Bid::getMediaType)
    -                .containsExactly(MediaType.video);
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnEmptyBidsIfEmptyOrNullBidResponse() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(identity());
    -
    -        exchangeCall = givenExchangeCallCustomizable(identity(), br -> br.seatbid(null));
    -
    -        // when and then
    -        assertThat(adapter.extractBids(adapterRequest, exchangeCall)).isEmpty();
    -        assertThat(adapter.extractBids(adapterRequest, ExchangeCall.empty(null))).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnMultipleBidBuildersIfMultipleAdUnitsInPreBidRequestAndBidsInResponse() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBidCustomizable(builder -> builder.adUnitCode("adUnitCode1")),
    -                givenAdUnitBidCustomizable(builder -> builder.adUnitCode("adUnitCode2"))));
    -
    -        exchangeCall = givenExchangeCallCustomizable(
    -                bidRequestBuilder -> bidRequestBuilder.imp(asList(Imp.builder().id("adUnitCode1").build(),
    -                        Imp.builder().id("adUnitCode2").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(asList(Bid.builder().impid("adUnitCode1").build(),
    -                                        Bid.builder().impid("adUnitCode2").build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(2)
    -                .extracting(org.prebid.server.proto.response.Bid::getCode)
    -                .containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    private static AdapterRequest givenBidderCustomizable(
    -            Function adUnitBidBuilderCustomizer) {
    -        return AdapterRequest.of(BIDDER, singletonList(givenAdUnitBidCustomizable(adUnitBidBuilderCustomizer)));
    -    }
    -
    -    private static AdUnitBid givenAdUnitBidCustomizable(
    -            Function adUnitBidBuilderCustomizer) {
    -        final AdUnitBid.AdUnitBidBuilder adUnitBidBuilderMinimal = AdUnitBid.builder()
    -                .sizes(singletonList(Format.builder().w(480).h(320).build()))
    -                .params(mapper.valueToTree(PubmaticParams.of("publisherId1", "slot1@300x250:zzz", null, null)))
    -                .mediaTypes(singleton(MediaType.banner));
    -
    -        final AdUnitBid.AdUnitBidBuilder adUnitBidBuilderCustomized = adUnitBidBuilderCustomizer
    -                .apply(adUnitBidBuilderMinimal);
    -
    -        return adUnitBidBuilderCustomized.build();
    -    }
    -
    -    private PreBidRequestContext givenPreBidRequestContextCustomizable(
    -            Function preBidRequestContextBuilderCustomizer,
    -            Function
    -                    preBidRequestBuilderCustomizer) {
    -        return givenPreBidRequestContextCustomizable(preBidRequestContextBuilderCustomizer,
    -                preBidRequestBuilderCustomizer, uidsCookie);
    -    }
    -
    -    private PreBidRequestContext givenPreBidRequestContextCustomizable(
    -            Function preBidRequestContextBuilderCustomizer,
    -            Function
    -                    preBidRequestBuilderCustomizer, UidsCookie uidsCookie) {
    -
    -        final PreBidRequest.PreBidRequestBuilder preBidRequestBuilderMinimal = PreBidRequest.builder()
    -                .accountId("accountId");
    -        final PreBidRequest preBidRequest = preBidRequestBuilderCustomizer.apply(preBidRequestBuilderMinimal).build();
    -
    -        final PreBidRequestContext.PreBidRequestContextBuilder preBidRequestContextBuilderMinimal =
    -                PreBidRequestContext.builder()
    -                        .preBidRequest(preBidRequest)
    -                        .uidsCookie(uidsCookie);
    -        return preBidRequestContextBuilderCustomizer.apply(preBidRequestContextBuilderMinimal).build();
    -    }
    -
    -    private static ExchangeCall givenExchangeCallCustomizable(
    -            Function bidRequestBuilderCustomizer,
    -            Function bidResponseBuilderCustomizer) {
    -
    -        final BidRequest.BidRequestBuilder bidRequestBuilderMinimal = BidRequest.builder();
    -        final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(bidRequestBuilderMinimal).build();
    -
    -        final BidResponse.BidResponseBuilder bidResponseBuilderMinimal = BidResponse.builder();
    -        final BidResponse bidResponse = bidResponseBuilderCustomizer.apply(bidResponseBuilderMinimal).build();
    -
    -        return ExchangeCall.success(bidRequest, bidResponse, BidderDebug.builder().build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java
    index ba07245942a..4668726aaeb 100644
    --- a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java
    @@ -6,9 +6,11 @@
     import com.iab.openrtb.request.Audio;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Imp;
     import com.iab.openrtb.request.Publisher;
     import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
     import com.iab.openrtb.response.SeatBid;
    @@ -22,9 +24,11 @@
     import org.prebid.server.bidder.model.HttpRequest;
     import org.prebid.server.bidder.model.HttpResponse;
     import org.prebid.server.bidder.model.Result;
    -import org.prebid.server.bidder.pubmatic.proto.PubmaticBidExt;
    -import org.prebid.server.bidder.pubmatic.proto.PubmaticRequestExt;
    -import org.prebid.server.bidder.pubmatic.proto.VideoCreativeInfo;
    +import org.prebid.server.bidder.pubmatic.model.request.PubmaticBidderImpExt;
    +import org.prebid.server.bidder.pubmatic.model.request.PubmaticExtData;
    +import org.prebid.server.bidder.pubmatic.model.request.PubmaticExtDataAdServer;
    +import org.prebid.server.bidder.pubmatic.model.response.PubmaticBidExt;
    +import org.prebid.server.bidder.pubmatic.model.response.VideoCreativeInfo;
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
     import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic;
    @@ -40,7 +44,6 @@
     
     import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.Collections.singletonMap;
     import static java.util.function.Function.identity;
    @@ -110,7 +113,7 @@ public void makeHttpRequestsShouldReturnErrorIfAdSlotIsInvalid() {
     
             // then
             assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Invalid adSlot provided"));
    +                .containsOnly(BidderError.badInput("Invalid adSlot 'invalid ad slot@'"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -126,7 +129,7 @@ public void makeHttpRequestsShouldReturnErrorIfAdSlotHasInvalidSizes() {
     
             // then
             assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Invalid size provided in adSlot"));
    +                .containsOnly(BidderError.badInput("Invalid size provided in adSlot 'slot@300x200x100'"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -142,7 +145,7 @@ public void makeHttpRequestsShouldReturnErrorIfAdSlotHasInvalidWidth() {
     
             // then
             assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Invalid size provided in adSlot"));
    +                .containsOnly(BidderError.badInput("Invalid width provided in adSlot 'slot@widthx200'"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -158,25 +161,7 @@ public void makeHttpRequestsShouldReturnErrorIfAdSlotHasInvalidHeight() {
     
             // then
             assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Invalid size provided in adSlot"));
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnErrorIfKeywordsAreInvalid() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                identity(),
    -                extImpPubmaticBuilder -> extImpPubmaticBuilder
    -                        .keywords(singletonList(ExtImpPubmaticKeyVal.of("\"", singletonList("\"")))));
    -
    -        // when
    -        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage())
    -                .startsWith("Failed to create keywords with error: Unexpected character");
    +                .containsOnly(BidderError.badInput("Invalid height provided in adSlot 'slot@300xHeight:1'"));
             assertThat(result.getValue()).isEmpty();
         }
     
    @@ -209,7 +194,7 @@ public void makeHttpRequestsShouldSetAudioToNullIfPresent() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getAudio).containsNull();
         }
    @@ -225,17 +210,95 @@ public void makeHttpRequestsShouldSetBannerWidthAndHeightFromAdSlot() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getBanner)
                     .extracting(Banner::getH).containsOnly(250);
             assertThat(result.getValue())
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getBanner)
                     .extracting(Banner::getW).containsOnly(300);
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldSetBannerWidthAndHeightFromFormatIfMissedOriginalsOrInAdSlot() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder()
    +                .format(singletonList(Format.builder().w(100).h(200).build()))
    +                .build()), extImpPubmaticBuilder -> extImpPubmaticBuilder.adSlot(null));
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .extracting(Banner::getH).containsOnly(200);
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .extracting(Banner::getW).containsOnly(100);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetTagIdForBannerImpsWithSymbolsFromAdSlotBeforeAtSign() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity());
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid)
    +                .containsOnly("slot");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetTagIdForVideoImpsWithSymbolsFromAdSlotBeforeAtSign() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.banner(null).video(Video.builder().build()));
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid)
    +                .containsOnly("slot");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetAdSlotAsTagIdIfAtSignIsMissing() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity(),
    +                extImpBuilder -> extImpBuilder
    +                        .adSlot("adSlot"));
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid)
    +                .containsOnly("adSlot");
    +    }
    +
         @Test
         public void makeHttpRequestsShouldSetImpExtNullIfKeywordsAreNull() {
             // given
    @@ -247,7 +310,7 @@ public void makeHttpRequestsShouldSetImpExtNullIfKeywordsAreNull() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getExt).containsNull();
         }
    @@ -265,13 +328,13 @@ public void makeHttpRequestsShouldSetImpExtNullIfKeywordsAreEmpty() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getExt).containsNull();
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetImpExtFromKeywords() throws IOException {
    +    public void makeHttpRequestsShouldSetImpExtFromKeywords() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
    @@ -283,12 +346,71 @@ public void makeHttpRequestsShouldSetImpExtFromKeywords() throws IOException {
             final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
     
             // then
    +        final ObjectNode expectedImpExt = mapper.createObjectNode();
    +        expectedImpExt.put("key2", "value1,value2");
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsOnly(expectedImpExt);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldAddImpExtAddUnitKeyKeyWordFromDataAdSlotIfAdServerNameIsNotGam() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .id("123")
    +                        .banner(Banner.builder().build())
    +                        .ext(mapper.valueToTree(PubmaticBidderImpExt.of(
    +                                ExtImpPubmatic.builder().build(),
    +                                PubmaticExtData.of("pbaAdSlot",
    +                                        PubmaticExtDataAdServer.of("adServerName", "adServerAdSlot"))
    +                        )))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedImpExt = mapper.createObjectNode();
    +        expectedImpExt.put("dfp_ad_unit_code", "pbaAdSlot");
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsOnly(expectedImpExt);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldAddImpExtAddUnitKeyKeyWordFromAdServerAdSlotIfAdServerNameIsGam() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .id("123")
    +                        .banner(Banner.builder().build())
    +                        .ext(mapper.valueToTree(PubmaticBidderImpExt.of(
    +                                ExtImpPubmatic.builder().build(),
    +                                PubmaticExtData.of("pbaAdSlot", PubmaticExtDataAdServer.of("gam", "adServerAdSlot"))
    +                        )))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedImpExt = mapper.createObjectNode();
    +        expectedImpExt.put("dfp_ad_unit_code", "adServerAdSlot");
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getExt)
    -                .containsOnly(mapper.readValue("{\"key2\":\"value1,value2\"}", ObjectNode.class));
    +                .containsOnly(expectedImpExt);
         }
     
         @Test
    @@ -305,12 +427,14 @@ public void makeHttpRequestsShouldSetImpExtFromKeywordsSkippingKeysWithEmptyValu
             final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
     
             // then
    +        final ObjectNode expectedImpExt = mapper.createObjectNode();
    +        expectedImpExt.put("key2", "value1,value2");
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getExt)
    -                .containsOnly(mapper.readValue("{\"key2\":\"value1,value2\"}", ObjectNode.class));
    +                .containsOnly(expectedImpExt);
         }
     
         @Test
    @@ -327,10 +451,11 @@ public void makeHttpRequestsShouldSetRequestExtFromWrapExt() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .extracting(BidRequest::getExt)
                     .containsOnly(jacksonMapper.fillExtension(
    -                        ExtRequest.empty(), PubmaticRequestExt.of(mapper.valueToTree(singletonMap("key", 1)))));
    +                        ExtRequest.empty(), mapper.createObjectNode()
    +                                .set("wrapper", mapper.valueToTree(singletonMap("key", 1)))));
         }
     
         @Test
    @@ -347,7 +472,7 @@ public void makeHttpRequestsShouldNotChangeExtIfWrapExtIsMissing() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .extracting(BidRequest::getExt)
                     .containsOnly(ExtRequest.empty());
         }
    @@ -366,13 +491,57 @@ public void makeHttpRequestsShouldSetSitePublisherIdFromImpExtPublisherId() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .extracting(BidRequest::getSite)
                     .extracting(Site::getPublisher)
                     .extracting(Publisher::getId)
                     .containsOnly("pub id");
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldUpdateSitePublisherIdFromImpExtPublisherId() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.site(Site.builder()
    +                        .publisher(Publisher.builder().id("anotherId").build())
    +                        .build()),
    +                identity(),
    +                identity());
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsOnly("pub id");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetTrimmedImpExtPublisherId() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.site(Site.builder().build()),
    +                identity(),
    +                extImpPubmaticBuilder -> extImpPubmaticBuilder.publisherId("  pubId  "));
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsOnly("pubId");
    +    }
    +
         @Test
         public void makeHttpRequestsShouldNotSetAppPublisherIdIfSiteIsNotNull() {
             // given
    @@ -389,7 +558,7 @@ public void makeHttpRequestsShouldNotSetAppPublisherIdIfSiteIsNotNull() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
                     .extracting(BidRequest::getApp)
                     .extracting(App::getId)
                     .containsNull();
    @@ -409,7 +578,30 @@ public void makeHttpRequestsShouldSetAppPublisherIdIfSiteIsNull() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getApp)
    +                .extracting(App::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsOnly("pub id");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSUpdateAppPublisherIdExtPublisherIdIsPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.app(App.builder()
    +                        .publisher(Publisher.builder().id("anotherId").build())
    +                        .build()),
    +                identity(),
    +                identity());
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
                     .extracting(BidRequest::getApp)
                     .extracting(App::getPublisher)
                     .extracting(Publisher::getId)
    @@ -625,8 +817,67 @@ public void makeBidsShouldReturnBannerBidIfExtBidContainsIllegalBidType() throws
         }
     
         @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(pubmaticBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +    public void makeHttpRequestsShouldReplaceDctrIfPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                extImpPubmaticBuilder -> extImpPubmaticBuilder.dctr("dctr")
    +                        .keywords(singletonList(ExtImpPubmaticKeyVal.of("key_val", asList("value1", "value2")))));
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final Map expectedKeyWords = singletonMap("key_val", "dctr");
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(mapper.convertValue(expectedKeyWords, ObjectNode.class));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReplacePmZoneIdIfPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                extImpPubmaticBuilder -> extImpPubmaticBuilder.pmZoneId("pmzoneid")
    +                        .keywords(singletonList(ExtImpPubmaticKeyVal.of("pmZoneId", asList("value1", "value2")))));
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final Map expectedKeyWords = singletonMap("pmZoneId", "pmzoneid");
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(mapper.convertValue(expectedKeyWords, ObjectNode.class));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReplacePmZoneIDOldKeyNameWithNew() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                identity(),
    +                extImpPubmaticBuilder -> extImpPubmaticBuilder
    +                        .keywords(singletonList(ExtImpPubmaticKeyVal.of("pmZoneID", asList("value1", "value2")))));
    +
    +        // when
    +        final Result>> result = pubmaticBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +
    +        final Map expectedKeyWords = singletonMap("pmZoneId", "value1,value2");
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getExt)
    +                .containsExactly(mapper.convertValue(expectedKeyWords, ObjectNode.class));
         }
     
         private static BidRequest givenBidRequest(
    @@ -667,6 +918,7 @@ private static Imp givenImp(Function impCustomiz
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/pubnative/PubnativeBidderTest.java b/src/test/java/org/prebid/server/bidder/pubnative/PubnativeBidderTest.java
    index 104eb593b17..703b153142f 100644
    --- a/src/test/java/org/prebid/server/bidder/pubnative/PubnativeBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/pubnative/PubnativeBidderTest.java
    @@ -12,7 +12,11 @@
     import com.iab.openrtb.response.BidResponse;
     import com.iab.openrtb.response.SeatBid;
     import org.junit.Before;
    +import org.junit.Rule;
     import org.junit.Test;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
     import org.prebid.server.VertxTest;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderError;
    @@ -20,19 +24,25 @@
     import org.prebid.server.bidder.model.HttpRequest;
     import org.prebid.server.bidder.model.HttpResponse;
     import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.currency.CurrencyConversionService;
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.request.pubnative.ExtImpPubnative;
     
    +import java.math.BigDecimal;
     import java.util.List;
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
     import static org.assertj.core.api.Assertions.tuple;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.eq;
    +import static org.mockito.Mockito.verify;
     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;
    @@ -41,16 +51,23 @@ public class PubnativeBidderTest extends VertxTest {
     
         private static final String ENDPOINT_URL = "https://test.endpoint.com";
     
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
         private PubnativeBidder pubnativeBidder;
     
    +    @Mock
    +    private CurrencyConversionService currencyConversionService;
    +
         @Before
         public void setUp() {
    -        pubnativeBidder = new PubnativeBidder(ENDPOINT_URL, jacksonMapper);
    +        pubnativeBidder = new PubnativeBidder(ENDPOINT_URL, jacksonMapper, currencyConversionService);
         }
     
         @Test
         public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException().isThrownBy(() -> new PubnativeBidder("invalid_url", jacksonMapper));
    +        assertThatIllegalArgumentException().isThrownBy(() -> new PubnativeBidder("invalid_url",
    +                jacksonMapper, currencyConversionService));
         }
     
         @Test
    @@ -148,6 +165,71 @@ public void makeHttpRequestsShouldCreateRequestPerEachImpAndSkipOnesWithErrors()
                     .containsOnly("imp1", "imp2");
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldChangeImpCurrencyToUsdIfPresent() {
    +        // given
    +        given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString()))
    +                .willReturn(BigDecimal.TEN);
    +        final BidRequest bidRequest = givenBidRequest(identity(),
    +                requestBuilder -> requestBuilder.imp(
    +                        singletonList(givenImp(impBuilder ->
    +                                impBuilder
    +                                        .id("imp1")
    +                                        .bidfloorcur("EUR")
    +                                        .bidfloor(BigDecimal.ONE)))));
    +
    +        // when
    +        final Result>> result = pubnativeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor, Imp::getBidfloorcur)
    +                .containsExactly(tuple(BigDecimal.TEN, "USD"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldChangeBidRequestCurrencyToUsd() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(identity(),
    +                requestBuilder -> requestBuilder.imp(singletonList(givenImp(identity()))));
    +
    +        // when
    +        final Result>> result = pubnativeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getCur)
    +                .containsExactly("USD");
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldGetCurrencyFromBidRequestIfImpBidfloorCurIsAbsent() {
    +        // given
    +        given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString()))
    +                .willReturn(BigDecimal.TEN);
    +        final BidRequest bidRequest = givenBidRequest(identity(), requestBuilder ->
    +                requestBuilder
    +                        .imp(singletonList(givenImp(impBuilder -> impBuilder.id("imp1").bidfloor(BigDecimal.ONE))))
    +                        .cur(singletonList("EUR")));
    +
    +        // when
    +        final Result>> result = pubnativeBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        verify(currencyConversionService).convertCurrency(eq(BigDecimal.ONE), any(), eq("EUR"), eq("USD"));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBidfloor, Imp::getBidfloorcur)
    +                .containsExactly(tuple(BigDecimal.TEN, "USD"));
    +    }
    +
         @Test
         public void makeHttpRequestsShouldSetBannerHeightAndWidthFromFirstFormatWhenHeightIsNullOrZero() {
             // given
    @@ -313,8 +395,134 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessi
         }
     
         @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(pubnativeBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +    public void makeBidsShouldResolveBidSizeFromBannerIfWAndHArePresent() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder()
    +                                .banner(Banner.builder().w(100).h(100).build())
    +                                .id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = pubnativeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").w(100).h(100).build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldResolveBidSizeForBannerWhenWAndHNotNullAndFormatHasSingleElementWithSameSize()
    +            throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder()
    +                                .banner(Banner.builder().w(100).h(100)
    +                                        .format(singletonList(Format.builder().w(100).h(100).build())).build())
    +                                .id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = pubnativeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").w(100).h(100).build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldNotResolveBidSizeForBannerWhenWAndHNotNullAndFormatHasSingleElementWithDifferentSize()
    +            throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder()
    +                                .banner(Banner.builder().w(100).h(100)
    +                                        .format(singletonList(Format.builder().w(150).h(150).build())).build())
    +                                .id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = pubnativeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldNotResolveBidSizeForBannerWhenWAndHNotNullAndFormatHasMultipleElements()
    +            throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder()
    +                                .banner(Banner.builder().w(100).h(100)
    +                                        .format(singletonList(Format.builder().w(100).h(100).build())).build())
    +                                .id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = pubnativeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").w(100).h(100).build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldResolveBidSizeForBannerWhenWAndHAreNullAndFormatHasSingleElements()
    +            throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder()
    +                                .banner(Banner.builder()
    +                                        .format(singletonList(Format.builder().w(150).h(150).build())).build())
    +                                .id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = pubnativeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").w(150).h(150).build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldNotResolveBidSizeForBannerWhenWAndHAreNullAndFormatMultipleElements()
    +            throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder()
    +                                .banner(Banner.builder()
    +                                        .format(asList(Format.builder().w(100).h(100).build(),
    +                                                Format.builder().w(150).h(150).build())).build())
    +                                .id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = pubnativeBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
         private static BidRequest givenBidRequest(
    @@ -340,6 +548,7 @@ private static Imp givenImp(Function impModifier
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/pulsepoint/PulsepointAdapterTest.java b/src/test/java/org/prebid/server/bidder/pulsepoint/PulsepointAdapterTest.java
    deleted file mode 100644
    index f0b0da95f4a..00000000000
    --- a/src/test/java/org/prebid/server/bidder/pulsepoint/PulsepointAdapterTest.java
    +++ /dev/null
    @@ -1,519 +0,0 @@
    -package org.prebid.server.bidder.pulsepoint;
    -
    -import com.fasterxml.jackson.databind.node.IntNode;
    -import com.fasterxml.jackson.databind.node.ObjectNode;
    -import com.fasterxml.jackson.databind.node.TextNode;
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.Banner;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.BidRequest.BidRequestBuilder;
    -import com.iab.openrtb.request.Device;
    -import com.iab.openrtb.request.Format;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Publisher;
    -import com.iab.openrtb.request.Regs;
    -import com.iab.openrtb.request.Site;
    -import com.iab.openrtb.request.Source;
    -import com.iab.openrtb.request.User;
    -import com.iab.openrtb.response.Bid;
    -import com.iab.openrtb.response.BidResponse;
    -import com.iab.openrtb.response.BidResponse.BidResponseBuilder;
    -import com.iab.openrtb.response.SeatBid;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.auction.model.AdUnitBid.AdUnitBidBuilder;
    -import org.prebid.server.auction.model.AdapterRequest;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    -import org.prebid.server.auction.model.PreBidRequestContext.PreBidRequestContextBuilder;
    -import org.prebid.server.bidder.model.AdapterHttpRequest;
    -import org.prebid.server.bidder.model.ExchangeCall;
    -import org.prebid.server.bidder.pulsepoint.proto.PulsepointParams;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.request.PreBidRequest;
    -import org.prebid.server.proto.request.PreBidRequest.PreBidRequestBuilder;
    -import org.prebid.server.proto.request.Video;
    -import org.prebid.server.proto.response.BidderDebug;
    -import org.prebid.server.proto.response.MediaType;
    -
    -import java.math.BigDecimal;
    -import java.util.EnumSet;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.function.Function;
    -import java.util.stream.Collectors;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.emptySet;
    -import static java.util.Collections.singleton;
    -import static java.util.Collections.singletonList;
    -import static java.util.function.Function.identity;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatCode;
    -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    -import static org.assertj.core.api.Assertions.assertThatThrownBy;
    -import static org.assertj.core.api.Assertions.tuple;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -
    -public class PulsepointAdapterTest extends VertxTest {
    -
    -    private static final String BIDDER = "pulsepoint";
    -    private static final String COOKIE_FAMILY = BIDDER;
    -    private static final String ENDPOINT_URL = "http://endpoint.org/";
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private UidsCookie uidsCookie;
    -
    -    private AdapterRequest adapterRequest;
    -    private PreBidRequestContext preBidRequestContext;
    -    private ExchangeCall exchangeCall;
    -    private PulsepointAdapter adapter;
    -
    -    @Before
    -    public void setUp() {
    -        adapterRequest = givenBidder(identity());
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -        adapter = new PulsepointAdapter(COOKIE_FAMILY, ENDPOINT_URL, jacksonMapper);
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException()
    -                .isThrownBy(() -> new PulsepointAdapter(COOKIE_FAMILY, "invalid_url", jacksonMapper))
    -                .withMessage("URL supplied is not valid: invalid_url");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedHeaders() {
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).flatExtracting(r -> r.getHeaders().entries())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple("Content-Type", "application/json;charset=utf-8"),
    -                        tuple("Accept", "application/json"));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfParamsMissingInAtLeastOneAdUnitBid() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(identity()),
    -                givenAdUnitBid(builder -> builder.params(null))));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Pulsepoint params section is missing");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamsCouldNotBeParsed() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("cp", new TextNode("non-integer"));
    -        adapterRequest = givenBidder(builder -> builder.params(params));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessageStartingWith("Cannot deserialize value of type");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamPublisherIdIsMissing() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("cp", null);
    -        adapterRequest = givenBidder(builder -> builder.params(params));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Missing PublisherId param cp");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamTagIdIsMissing() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("cp", new IntNode(1));
    -        params.set("ct", null);
    -        adapterRequest = givenBidder(builder -> builder.params(params));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Missing TagId param ct");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamAdSizeIsMissing() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("cp", new IntNode(1));
    -        params.set("ct", new IntNode(1));
    -        params.set("cf", null);
    -        adapterRequest = givenBidder(builder -> builder.params(params));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Missing AdSize param cf");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamAdSizeIsInvalid() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("cp", new IntNode(1));
    -        params.set("ct", new IntNode(1));
    -        params.set("cf", new TextNode("invalid"));
    -        adapterRequest = givenBidder(builder -> builder.params(params));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid AdSize param invalid");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamAdSizeWidthIsInvalid() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("cp", new IntNode(1));
    -        params.set("ct", new IntNode(1));
    -        params.set("cf", new TextNode("invalidX500"));
    -        adapterRequest = givenBidder(builder -> builder.params(params));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid Width param invalid");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamAdSizeHeightIsInvalid() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("cp", new IntNode(1));
    -        params.set("ct", new IntNode(1));
    -        params.set("cf", new TextNode("100Xinvalid"));
    -        adapterRequest = givenBidder(builder -> builder.params(params));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid Height param invalid");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .mediaTypes(emptySet()))));
    -
    -        preBidRequestContext = givenPreBidRequestContext(identity(), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("openRTB bids need at least one Imp");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldNotFailIfMediaTypeHasVideoAndMimesListIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBid(builder -> builder
    -                        .adUnitCode("adUnitCode1")
    -                        .mediaTypes(EnumSet.of(MediaType.banner, MediaType.video))
    -                        .video(Video.builder()
    -                                .mimes(emptyList())
    -                                .build()))));
    -
    -        // when and then
    -        assertThatCode(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .doesNotThrowAnyException();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder
    -                        .bidderCode(BIDDER)
    -                        .adUnitCode("adUnitCode1")
    -                        .instl(1)
    -                        .topframe(1)
    -                        .params(mapper.valueToTree(PulsepointParams.of(101, 102, "800x600")))
    -                        .sizes(singletonList(Format.builder().w(300).h(250).build())));
    -
    -        preBidRequestContext = givenPreBidRequestContext(
    -                builder -> builder
    -                        .referer("http://www.example.com")
    -                        .domain("example.com")
    -                        .ip("192.168.144.1")
    -                        .ua("userAgent1"),
    -                builder -> builder
    -                        .timeoutMillis(1500L)
    -                        .tid("tid1")
    -                        .user(User.builder()
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null))));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUid1");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .containsOnly(BidRequest.builder()
    -                        .id("tid1")
    -                        .at(1)
    -                        .tmax(1500L)
    -                        .imp(singletonList(Imp.builder()
    -                                .id("adUnitCode1")
    -                                .instl(1)
    -                                .tagid("102")
    -                                .banner(Banner.builder()
    -                                        .w(800)
    -                                        .h(600)
    -                                        .topframe(1)
    -                                        .format(singletonList(Format.builder()
    -                                                .w(300)
    -                                                .h(250)
    -                                                .build()))
    -                                        .build())
    -                                .build()))
    -                        .site(Site.builder()
    -                                .domain("example.com")
    -                                .page("http://www.example.com")
    -                                .publisher(Publisher.builder().id("101").build())
    -                                .build())
    -                        .device(Device.builder()
    -                                .ua("userAgent1")
    -                                .ip("192.168.144.1")
    -                                .build())
    -                        .user(User.builder()
    -                                .buyeruid("buyerUid1")
    -                                .ext(ExtUser.builder().consent("consent").build())
    -                                .build())
    -                        .regs(Regs.of(0, ExtRegs.of(1, null)))
    -                        .source(Source.builder()
    -                                .fd(1)
    -                                .tid("tid1")
    -                                .build())
    -                        .build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithAppFromPreBidRequest() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder
    -                .app(App.builder().id("appId").build()).user(User.builder().build()));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(r -> r.getPayload().getApp().getId())
    -                .containsOnly("appId");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithUserFromPreBidRequestIfAppPresent() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder
    -                .app(App.builder().build())
    -                .user(User.builder().buyeruid("buyerUid").build()));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUidFromCookie");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(r -> r.getPayload().getUser())
    -                .containsOnly(User.builder().buyeruid("buyerUid").build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithOneRequestIfMultipleAdUnitsInPreBidRequest() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1")),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(2)
    -                .extracting(Imp::getId).containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldFailIfBidImpIdDoesNotMatchAdUnitCode() {
    -        // given
    -        adapterRequest = givenBidder(builder -> builder.adUnitCode("adUnitCode"));
    -
    -        exchangeCall = givenExchangeCall(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("anotherAdUnitCode").build()))
    -                        .build())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Unknown ad unit code 'anotherAdUnitCode'");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidder(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"));
    -
    -        exchangeCall = givenExchangeCall(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .price(new BigDecimal("8.43"))
    -                                        .adm("adm")
    -                                        .crid("crid")
    -                                        .w(300)
    -                                        .h(250)
    -                                        .dealid("dealId")
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids)
    -                .containsExactly(org.prebid.server.proto.response.Bid.builder()
    -                        .code("adUnitCode")
    -                        .price(new BigDecimal("8.43"))
    -                        .adm("adm")
    -                        .creativeId("crid")
    -                        .width(300)
    -                        .height(250)
    -                        .bidder(BIDDER)
    -                        .bidId("bidId")
    -                        .mediaType(MediaType.banner)
    -                        .build());
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnEmptyBidsIfEmptyOrNullBidResponse() {
    -        // given
    -        adapterRequest = givenBidder(identity());
    -
    -        exchangeCall = givenExchangeCall(identity(), br -> br.seatbid(null));
    -
    -        // when and then
    -        assertThat(adapter.extractBids(adapterRequest, exchangeCall)).isEmpty();
    -        assertThat(adapter.extractBids(adapterRequest, ExchangeCall.empty(null))).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnMultipleBidBuildersIfMultipleAdUnitsInPreBidRequestAndBidsInResponse() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1")),
    -                givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2"))));
    -
    -        exchangeCall = givenExchangeCall(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(asList(Bid.builder().impid("adUnitCode1").build(),
    -                                        Bid.builder().impid("adUnitCode2").build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(2)
    -                .extracting(org.prebid.server.proto.response.Bid::getCode)
    -                .containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    private static AdapterRequest givenBidder(Function adUnitBidBuilderCustomizer) {
    -        return AdapterRequest.of(BIDDER, singletonList(givenAdUnitBid(adUnitBidBuilderCustomizer)));
    -    }
    -
    -    private static AdUnitBid givenAdUnitBid(Function adUnitBidBuilderCustomizer) {
    -        // ad unit bid
    -        final AdUnitBidBuilder adUnitBidBuilderMinimal = AdUnitBid.builder()
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .params(mapper.valueToTree(PulsepointParams.of(42, 100500, "100X200")))
    -                .mediaTypes(singleton(MediaType.banner));
    -        final AdUnitBidBuilder adUnitBidBuilderCustomized = adUnitBidBuilderCustomizer.apply(adUnitBidBuilderMinimal);
    -
    -        return adUnitBidBuilderCustomized.build();
    -    }
    -
    -    private PreBidRequestContext givenPreBidRequestContext(
    -            Function preBidRequestContextBuilderCustomizer,
    -            Function preBidRequestBuilderCustomizer) {
    -
    -        final PreBidRequestBuilder preBidRequestBuilderMinimal = PreBidRequest.builder().accountId("accountId");
    -        final PreBidRequest preBidRequest = preBidRequestBuilderCustomizer.apply(preBidRequestBuilderMinimal).build();
    -
    -        final PreBidRequestContextBuilder preBidRequestContextBuilderMinimal =
    -                PreBidRequestContext.builder()
    -                        .preBidRequest(preBidRequest)
    -                        .uidsCookie(uidsCookie);
    -        return preBidRequestContextBuilderCustomizer.apply(preBidRequestContextBuilderMinimal).build();
    -    }
    -
    -    private static ExchangeCall givenExchangeCall(
    -            Function bidRequestBuilderCustomizer,
    -            Function bidResponseBuilderCustomizer) {
    -
    -        final BidRequestBuilder bidRequestBuilderMinimal = BidRequest.builder();
    -        final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(bidRequestBuilderMinimal).build();
    -
    -        final BidResponseBuilder bidResponseBuilderMinimal = BidResponse.builder();
    -        final BidResponse bidResponse = bidResponseBuilderCustomizer.apply(bidResponseBuilderMinimal).build();
    -
    -        return ExchangeCall.success(bidRequest, bidResponse, BidderDebug.builder().build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/pulsepoint/PulsepointBidderTest.java b/src/test/java/org/prebid/server/bidder/pulsepoint/PulsepointBidderTest.java
    index 20e2c6b443d..b37b1644d4a 100644
    --- a/src/test/java/org/prebid/server/bidder/pulsepoint/PulsepointBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/pulsepoint/PulsepointBidderTest.java
    @@ -2,11 +2,14 @@
     
     import com.fasterxml.jackson.core.JsonProcessingException;
     import com.iab.openrtb.request.App;
    +import com.iab.openrtb.request.Audio;
     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;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
     import com.iab.openrtb.response.SeatBid;
    @@ -21,17 +24,16 @@
     import org.prebid.server.bidder.model.Result;
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.request.pulsepoint.ExtImpPulsepoint;
    +import org.prebid.server.proto.openrtb.ext.response.BidType;
     
     import java.util.List;
     import java.util.function.Function;
     
     import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.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.banner;
     
     public class PulsepointBidderTest extends VertxTest {
     
    @@ -49,25 +51,6 @@ public void creationShouldFailOnInvalidEndpointUrl() {
             assertThatIllegalArgumentException().isThrownBy(() -> new PulsepointBidder("invalid_url", jacksonMapper));
         }
     
    -    @Test
    -    public void makeHttpRequestsShouldSkipInvalidImpAndAddErrorIfImpHasNoBanner() {
    -        // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(
    -                        givenImp(impBuilder -> impBuilder.banner(null)),
    -                        givenImp(identity())))
    -                .build();
    -
    -        // when
    -        final Result>> result = pulsepointBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput(
    -                        "Invalid MediaType. Pulsepoint supports only Banner type. Ignoring ImpID=123"));
    -        assertThat(result.getValue()).hasSize(1);
    -    }
    -
         @Test
         public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             // given
    @@ -85,125 +68,120 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtPublisherIdIsNullOrZero() {
    +    public void makeHttpRequestsShouldSetEmptyPublisherIdIfValidPublisherWasNotFound() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(
    -                        givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(
    -                                null, ExtImpPulsepoint.of(null, null, null))))),
    +                .site(Site.builder().build())
    +                .imp(singletonList(
                             givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(
    -                                null, ExtImpPulsepoint.of(0, null, null)))))))
    +                                null, ExtImpPulsepoint.of(null, null)))))))
                     .build();
     
             // when
             final Result>> result = pulsepointBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(2)
    -                .containsOnly(BidderError.badInput("Missing PublisherId param cp"));
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsOnly("");
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtTagIdIsNullOrZero() {
    +    public void makeHttpRequestsShouldSetImpTagIdFromImpExt() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(
    -                        givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(
    -                                null, ExtImpPulsepoint.of(1, null, null))))),
    -                        givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(
    -                                null, ExtImpPulsepoint.of(1, 0, null)))))))
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(identity());
     
             // when
             final Result>> result = pulsepointBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(2)
    -                .containsOnly(BidderError.badInput("Missing TagId param ct"));
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid)
    +                .containsOnly("23");
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtAdSizeIsNullOrEmpty() {
    +    public void makeHttpRequestsShouldSetSitePublisherIdFromFirstImpExt() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(
    -                        givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(
    -                                null, ExtImpPulsepoint.of(1, 1, null))))),
    -                        givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(
    -                                null, ExtImpPulsepoint.of(1, 1, "")))))))
    +                .site(Site.builder().build())
    +                .imp(asList(givenImp(identity()),
    +                        givenImp(impBuilder -> impBuilder
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                        ExtImpPulsepoint.of(222, 23)))))))
                     .build();
     
             // when
             final Result>> result = pulsepointBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(2)
    -                .containsOnly(BidderError.badInput("Missing AdSize param cf"));
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsOnly("111");
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtAdSizeIsNotValid() {
    +    public void makeHttpRequestsShouldSetSitePublisherIdToNewlyCreatedPublisher() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(
    -                        givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(
    -                                null, ExtImpPulsepoint.of(1, 1, "10x20x30")))))))
    +                .site(Site.builder().build())
    +                .imp(singletonList(givenImp(identity())))
                     .build();
     
             // when
             final Result>> result = pulsepointBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Invalid AdSize param 10x20x30"));
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsOnly("111");
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtAdSizeHasInvalidWidthOrHeight() {
    +    public void makeHttpRequestsShouldSetSitePublisherIdToExistingPublisher() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .imp(asList(
    -                        givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(
    -                                null, ExtImpPulsepoint.of(1, 1, "invalidx250"))))),
    -                        givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(
    -                                null, ExtImpPulsepoint.of(1, 1, "300xInvalid")))))))
    +                .site(Site.builder().publisher(Publisher.builder().build()).build())
    +                .imp(singletonList(givenImp(identity())))
                     .build();
     
             // when
             final Result>> result = pulsepointBidder.makeHttpRequests(bidRequest);
     
    -        // then
    -        assertThat(result.getErrors()).hasSize(2)
    -                .containsOnly(BidderError.badInput("Invalid Width or Height param invalid x 250"),
    -                        BidderError.badInput("Invalid Width or Height param 300 x invalid"));
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldSetImpTagIdFromImpExt() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(identity());
    -
    -        // when
    -        final Result>> result = pulsepointBidder.makeHttpRequests(bidRequest);
    -
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getTagid)
    -                .containsOnly("23");
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsOnly("111");
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetImpBannerWidthAndHeightFromImpExt() {
    +    public void makeHttpRequestsShouldSetAppPublisherIdFromFirstImpExt() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(identity());
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .imp(asList(givenImp(identity()),
    +                        givenImp(impBuilder -> impBuilder
    +                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                        ExtImpPulsepoint.of(222, 23)))))))
    +                .build();
     
             // when
             final Result>> result = pulsepointBidder.makeHttpRequests(bidRequest);
    @@ -211,21 +189,19 @@ public void makeHttpRequestsShouldSetImpBannerWidthAndHeightFromImpExt() {
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .containsOnly(Banner.builder().w(300).h(250).build());
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getApp)
    +                .extracting(App::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsOnly("111");
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetSitePublisherIdFromLastImpExt() {
    +    public void makeHttpRequestsShouldSetAppPublisherIdToNewlyCreatedPublisher() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .site(Site.builder().build())
    -                .imp(asList(givenImp(identity()),
    -                        givenImp(impBuilder -> impBuilder
    -                                .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                        ExtImpPulsepoint.of(222, 23, "100x100")))))))
    +                .app(App.builder().build())
    +                .imp(singletonList(givenImp(identity())))
                     .build();
     
             // when
    @@ -235,21 +211,21 @@ public void makeHttpRequestsShouldSetSitePublisherIdFromLastImpExt() {
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1)
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .extracting(BidRequest::getSite)
    -                .extracting(Site::getPublisher)
    +                .extracting(BidRequest::getApp)
    +                .extracting(App::getPublisher)
                     .extracting(Publisher::getId)
    -                .containsOnly("222");
    +                .containsOnly("111");
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetAppPublisherIdFromLastImpExt() {
    +    public void makeHttpRequestsShouldSetAppPublisherIdFromFirstImpExtToExistingPublisher() {
             // given
             final BidRequest bidRequest = BidRequest.builder()
    -                .app(App.builder().build())
    +                .app(App.builder().publisher(Publisher.builder().build()).build())
                     .imp(asList(givenImp(identity()),
                             givenImp(impBuilder -> impBuilder
                                     .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                        ExtImpPulsepoint.of(222, 23, "100x100")))))))
    +                                        ExtImpPulsepoint.of(222, 23)))))))
                     .build();
     
             // when
    @@ -262,7 +238,7 @@ public void makeHttpRequestsShouldSetAppPublisherIdFromLastImpExt() {
                     .extracting(BidRequest::getApp)
                     .extracting(App::getPublisher)
                     .extracting(Publisher::getId)
    -                .containsOnly("222");
    +                .containsOnly("111");
         }
     
         @Test
    @@ -309,12 +285,48 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso
         }
     
         @Test
    -    public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
    +    public void makeBidsShouldDropBidIfThereIsNoMatchWithImp() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(
                     BidRequest.builder()
                             .imp(singletonList(Imp.builder().id("123").build()))
                             .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("1234"))));
    +
    +        // when
    +        final Result> result = pulsepointBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldDropBidIfThereIsNoMediaTypeInImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = pulsepointBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
                     mapper.writeValueAsString(
                             givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
     
    @@ -323,13 +335,69 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(BidderBid::getType)
    +                .containsOnly(BidType.banner);
         }
     
         @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(pulsepointBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    +    public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = pulsepointBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(BidderBid::getType)
    +                .containsOnly(BidType.video);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnAudioBid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").audio(Audio.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = pulsepointBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(BidderBid::getType)
    +                .containsOnly(BidType.audio);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = pulsepointBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(BidderBid::getType)
    +                .containsOnly(BidType.xNative);
         }
     
         private static BidRequest givenBidRequest(
    @@ -348,12 +416,13 @@ private static Imp givenImp(Function impCustomiz
             return impCustomizer.apply(Imp.builder()
                     .id("123")
                     .banner(Banner.builder().build())
    -                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpPulsepoint.of(111, 23, "300x250")))))
    +                .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpPulsepoint.of(111, 23)))))
                     .build();
         }
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/revcontent/RevcontentBidderTest.java b/src/test/java/org/prebid/server/bidder/revcontent/RevcontentBidderTest.java
    new file mode 100644
    index 00000000000..fb6e20ff51e
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/revcontent/RevcontentBidderTest.java
    @@ -0,0 +1,238 @@
    +package org.prebid.server.bidder.revcontent;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.App;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Site;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import io.netty.handler.codec.http.HttpHeaderValues;
    +import io.vertx.core.MultiMap;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class RevcontentBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://test.endpoint.com";
    +
    +    private RevcontentBidder revcontentBidder;
    +
    +    @Before
    +    public void setUp() {
    +        revcontentBidder = new RevcontentBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException()
    +                .isThrownBy(() -> new RevcontentBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotModifyIncomingRequestAndAddHeaders() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().name("appname").build())
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createObjectNode())))
    +                        .build()))
    +                .id("request_id")
    +                .build();
    +
    +        // when
    +        final Result>> result = revcontentBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .containsOnly(bidRequest);
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getHeaders)
    +                .flatExtracting(MultiMap::entries)
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .containsOnly(
    +                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE),
    +                        tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldFailIfAppOrSiteNotDefined() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createObjectNode())))
    +                        .build()))
    +                .id("request_id")
    +                .build();
    +
    +        // when
    +        final Result>> result = revcontentBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsOnly(
    +                        BidderError.badInput("Impression is missing app name or site domain, and must contain one."));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldFailIfAppAndSiteDefinedButWithoutNameAndDomain() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .app(App.builder().build())
    +                .site(Site.builder().build())
    +                .imp(singletonList(Imp.builder()
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createObjectNode())))
    +                        .build()))
    +                .id("request_id")
    +                .build();
    +
    +        // when
    +        final Result>> result = revcontentBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsOnly(
    +                        BidderError.badInput("Impression is missing app name or site domain, and must contain one."));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = revcontentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = revcontentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = revcontentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfAdmStartsWithOpenHtmlTag() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123").adm(""))));
    +
    +        // when
    +        final Result> result = revcontentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").adm("").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfAdmDoesNotStartsWithOpenHtmlTag() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123").adm("notHtml"))));
    +
    +        // when
    +        final Result> result = revcontentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").adm("notHtml").build(), xNative, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfAdmIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = revcontentBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder()
    +                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/rhythmone/RhythmoneBidderTest.java b/src/test/java/org/prebid/server/bidder/rhythmone/RhythmoneBidderTest.java
    index d413170d5aa..b81444470a2 100644
    --- a/src/test/java/org/prebid/server/bidder/rhythmone/RhythmoneBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/rhythmone/RhythmoneBidderTest.java
    @@ -26,7 +26,6 @@
     import java.util.Map;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static java.util.function.Function.identity;
     import static org.assertj.core.api.Assertions.assertThat;
    @@ -223,11 +222,6 @@ public void makeBidsShouldReturnVideoBidIfBidRequestHasVideoAndNoBanner() throws
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(rhythmoneBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidRequest givenBidRequest(
                 Function bidRequestCustomizer,
                 Function impCustomizer,
    @@ -259,6 +253,7 @@ private static Imp givenImp(
     
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java b/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java
    index 705f00afbbf..704074a1e80 100644
    --- a/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java
    @@ -20,7 +20,6 @@
     import java.util.List;
     import java.util.function.Function;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    @@ -124,13 +123,9 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessi
                     .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(rtbhouseBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static BidResponse givenBidResponse(Function bidCustomizer) {
             return BidResponse.builder()
    +                .cur("USD")
                     .seatbid(singletonList(SeatBid.builder()
                             .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
                             .build()))
    diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconAdapterTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconAdapterTest.java
    deleted file mode 100644
    index 8461b06bc88..00000000000
    --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconAdapterTest.java
    +++ /dev/null
    @@ -1,938 +0,0 @@
    -package org.prebid.server.bidder.rubicon;
    -
    -import com.fasterxml.jackson.databind.node.IntNode;
    -import com.fasterxml.jackson.databind.node.MissingNode;
    -import com.fasterxml.jackson.databind.node.ObjectNode;
    -import com.fasterxml.jackson.databind.node.TextNode;
    -import com.iab.openrtb.request.App;
    -import com.iab.openrtb.request.Banner;
    -import com.iab.openrtb.request.BidRequest;
    -import com.iab.openrtb.request.Content;
    -import com.iab.openrtb.request.Device;
    -import com.iab.openrtb.request.Format;
    -import com.iab.openrtb.request.Imp;
    -import com.iab.openrtb.request.Publisher;
    -import com.iab.openrtb.request.Regs;
    -import com.iab.openrtb.request.Site;
    -import com.iab.openrtb.request.Source;
    -import com.iab.openrtb.request.User;
    -import com.iab.openrtb.response.Bid;
    -import com.iab.openrtb.response.BidResponse;
    -import com.iab.openrtb.response.SeatBid;
    -import org.junit.Before;
    -import org.junit.Rule;
    -import org.junit.Test;
    -import org.mockito.Mock;
    -import org.mockito.junit.MockitoJUnit;
    -import org.mockito.junit.MockitoRule;
    -import org.prebid.server.VertxTest;
    -import org.prebid.server.auction.model.AdUnitBid;
    -import org.prebid.server.auction.model.AdUnitBid.AdUnitBidBuilder;
    -import org.prebid.server.auction.model.AdapterRequest;
    -import org.prebid.server.auction.model.PreBidRequestContext;
    -import org.prebid.server.bidder.model.AdapterHttpRequest;
    -import org.prebid.server.bidder.model.ExchangeCall;
    -import org.prebid.server.bidder.rubicon.proto.RubiconBannerExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconBannerExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconDeviceExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconDeviceExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconImpExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRpTrack;
    -import org.prebid.server.bidder.rubicon.proto.RubiconParams;
    -import org.prebid.server.bidder.rubicon.proto.RubiconParams.RubiconParamsBuilder;
    -import org.prebid.server.bidder.rubicon.proto.RubiconPubExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconPubExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconSiteExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconSiteExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconTargeting;
    -import org.prebid.server.bidder.rubicon.proto.RubiconTargetingExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconTargetingExtRp;
    -import org.prebid.server.cookie.UidsCookie;
    -import org.prebid.server.exception.PreBidException;
    -import org.prebid.server.proto.openrtb.ext.request.ExtDevice;
    -import org.prebid.server.proto.openrtb.ext.request.ExtPublisher;
    -import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
    -import org.prebid.server.proto.openrtb.ext.request.ExtSite;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.request.PreBidRequest;
    -import org.prebid.server.proto.request.Sdk;
    -import org.prebid.server.proto.request.Video;
    -import org.prebid.server.proto.response.BidderDebug;
    -import org.prebid.server.proto.response.MediaType;
    -
    -import java.math.BigDecimal;
    -import java.util.Collections;
    -import java.util.EnumSet;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.function.Function;
    -import java.util.stream.Collectors;
    -
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyList;
    -import static java.util.Collections.singleton;
    -import static java.util.Collections.singletonList;
    -import static java.util.Collections.singletonMap;
    -import static java.util.function.Function.identity;
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    -import static org.assertj.core.api.Assertions.assertThatThrownBy;
    -import static org.assertj.core.api.Assertions.tuple;
    -import static org.mockito.ArgumentMatchers.eq;
    -import static org.mockito.BDDMockito.given;
    -
    -public class RubiconAdapterTest extends VertxTest {
    -
    -    private static final String BIDDER = "rubicon";
    -    private static final String COOKIE_FAMILY = BIDDER;
    -    private static final String ENDPOINT_URL = "http://exchange.org/";
    -    private static final String USER = "user";
    -    private static final String PASSWORD = "password";
    -
    -    @Rule
    -    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    -
    -    @Mock
    -    private UidsCookie uidsCookie;
    -
    -    private AdapterRequest adapterRequest;
    -    private PreBidRequestContext preBidRequestContext;
    -    private ExchangeCall exchangeCall;
    -    private RubiconAdapter adapter;
    -
    -    @Before
    -    public void setUp() {
    -        adapterRequest = givenBidderCustomizable(identity(), identity());
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), identity());
    -        adapter = new RubiconAdapter(COOKIE_FAMILY, ENDPOINT_URL, USER, PASSWORD, jacksonMapper);
    -    }
    -
    -    @Test
    -    public void creationShouldFailOnInvalidEndpoints() {
    -        assertThatIllegalArgumentException()
    -                .isThrownBy(() -> new RubiconAdapter(COOKIE_FAMILY, "invalid_url", USER, PASSWORD, jacksonMapper))
    -                .withMessage("URL supplied is not valid: invalid_url");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithExpectedHeaders() {
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).flatExtracting(r -> r.getHeaders().entries())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(tuple("Content-Type", "application/json;charset=utf-8"),
    -                        tuple("Accept", "application/json"),
    -                        tuple("Authorization", "Basic dXNlcjpwYXNzd29yZA=="),
    -                        tuple("User-Agent", "prebid-server/1.0"));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfParamsMissingInAtLeastOneAdUnitBid() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBidCustomizable(identity(), identity()),
    -                givenAdUnitBidCustomizable(builder -> builder.params(null), identity())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Rubicon params section is missing");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamsCouldNotBeParsed() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("accountId", new TextNode("non-integer"));
    -        adapterRequest = givenBidderCustomizable(builder -> builder.params(params), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessageStartingWith("Cannot deserialize value of type");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamAccountIdIsMissing() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("accountId", null);
    -        adapterRequest = givenBidderCustomizable(builder -> builder.params(params), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Missing accountId param");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamSiteIdIsMissing() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("accountId", new IntNode(1));
    -        params.set("siteId", null);
    -        adapterRequest = givenBidderCustomizable(builder -> builder.params(params), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Missing siteId param");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfAdUnitBidParamZoneIdIsMissing() {
    -        // given
    -        final ObjectNode params = mapper.createObjectNode();
    -        params.set("accountId", new IntNode(1));
    -        params.set("siteId", new IntNode(1));
    -        params.set("zoneId", null);
    -        adapterRequest = givenBidderCustomizable(builder -> builder.params(params), identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Missing zoneId param");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfMediaTypeIsVideoAndMimesListIsEmpty() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBidCustomizable(builder -> builder
    -                                .adUnitCode("adUnitCode1")
    -                                .mediaTypes(singleton(MediaType.video))
    -                                .video(Video.builder()
    -                                        .mimes(emptyList())
    -                                        .build()),
    -                        identity())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid AdUnit: VIDEO media type with no video data");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfNoValidAdUnits() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, emptyList());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid ad unit/imp");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldFailIfBannerWithoutValidSizes() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder.sizes(singletonList(Format.builder().w(302).h(252).build())),
    -                identity());
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Invalid ad unit/imp");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsForBannerWithFilteredValidSizes() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder.sizes(asList(Format.builder().w(302).h(252).build(),
    -                        Format.builder().w(300).h(250).build())),
    -                identity());
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .flatExtracting(BidRequest::getImp).hasSize(1)
    -                .extracting(Imp::getBanner).isNotNull()
    -                .extracting(Banner::getExt).isNotNull()
    -                .extracting(ext -> mapper.treeToValue(ext, RubiconBannerExt.class)).isNotNull()
    -                .extracting(RubiconBannerExt::getRp).isNotNull()
    -                .extracting(RubiconBannerExtRp::getSizeId).containsOnly(15);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder
    -                        .bidderCode(BIDDER)
    -                        .adUnitCode("adUnitCode")
    -                        .instl(1)
    -                        .topframe(1)
    -                        .sizes(singletonList(Format.builder().w(300).h(250).build())),
    -                identity());
    -
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(
    -                builder -> builder
    -                        .referer("http://www.example.com")
    -                        .domain("example.com")
    -                        .ip("192.168.144.1")
    -                        .ua("userAgent"),
    -                builder -> builder
    -                        .timeoutMillis(1500L)
    -                        .tid("tid")
    -                        .sdk(Sdk.of("version1", "source1", "platform1"))
    -                        .device(Device.builder()
    -                                .pxratio(new BigDecimal("4.2"))
    -                                .build()));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUid");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .extracting(AdapterHttpRequest::getPayload)
    -                .containsOnly(BidRequest.builder()
    -                        .id("tid")
    -                        .at(1)
    -                        .tmax(1500L)
    -                        .imp(singletonList(Imp.builder()
    -                                .id("adUnitCode")
    -                                .instl(1)
    -                                .banner(Banner.builder()
    -                                        .w(300)
    -                                        .h(250)
    -                                        .topframe(1)
    -                                        .format(singletonList(Format.builder()
    -                                                .w(300)
    -                                                .h(250)
    -                                                .build()))
    -                                        .ext(mapper.valueToTree(RubiconBannerExt.of(
    -                                                RubiconBannerExtRp.of(15, null, "text/html"))))
    -                                        .build())
    -                                .ext(mapper.valueToTree(RubiconImpExt.of(RubiconImpExtRp.of(4001, null,
    -                                        RubiconImpExtRpTrack.of("prebid", "source1_platform1_version1")), null)))
    -                                .build()))
    -                        .site(Site.builder()
    -                                .domain("example.com")
    -                                .page("http://www.example.com")
    -                                .publisher(Publisher.builder()
    -                                        .ext(jacksonMapper.fillExtension(
    -                                                ExtPublisher.empty(),
    -                                                RubiconPubExt.of(RubiconPubExtRp.of(2001))))
    -                                        .build())
    -                                .ext(jacksonMapper.fillExtension(
    -                                        ExtSite.of(null, null), RubiconSiteExt.of(RubiconSiteExtRp.of(3001))))
    -                                .build())
    -                        .device(Device.builder()
    -                                .ua("userAgent")
    -                                .ip("192.168.144.1")
    -                                .pxratio(new BigDecimal("4.2"))
    -                                .ext(jacksonMapper.fillExtension(
    -                                        ExtDevice.empty(),
    -                                        RubiconDeviceExt.of(RubiconDeviceExtRp.of(new BigDecimal("4.2")))))
    -                                .build())
    -                        .user(User.builder()
    -                                .buyeruid("buyerUid")
    -                                .build())
    -                        .source(Source.builder()
    -                                .fd(1)
    -                                .tid("tid")
    -                                .build())
    -                        .build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithAppFromPreBidRequest() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), builder -> builder
    -                .app(App.builder().id("appId").build()));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getApp().getId())
    -                .containsOnly("appId");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithUserFromPreBidRequestIfAppPresent() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), builder -> builder
    -                .app(App.builder().build())
    -                .user(User.builder().buyeruid("buyerUid").build()));
    -
    -        given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUidFromCookie");
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getUser())
    -                .containsOnly(User.builder().buyeruid("buyerUid").build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithMobileSpecificFeatures() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), builder -> builder
    -                .app(App.builder().id("appId").build())
    -                .sdk(Sdk.of("version1", "source1", "platform1"))
    -                .device(Device.builder().pxratio(new BigDecimal("4.2")).build())
    -                .user(User.builder().language("language1").build()));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(1)
    -                .extracting(Imp::getExt).isNotNull()
    -                .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class)).isNotNull()
    -                .extracting(RubiconImpExt::getRp).isNotNull()
    -                .extracting(RubiconImpExtRp::getTrack)
    -                .containsOnly(RubiconImpExtRpTrack.of("prebid", "source1_platform1_version1"));
    -
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getDevice()).isNotNull()
    -                .extracting(Device::getExt).isNotNull()
    -                .containsOnly(jacksonMapper.fillExtension(
    -                        ExtDevice.empty(),
    -                        RubiconDeviceExt.of(RubiconDeviceExtRp.of(new BigDecimal("4.2")))));
    -
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getSite()).isNotNull()
    -                .extracting(Site::getContent)
    -                .containsOnly(Content.builder().language("language1").build());
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithDefaultMobileSpecificFeatures() {
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(1)
    -                .extracting(Imp::getExt).isNotNull()
    -                .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class)).isNotNull()
    -                .extracting(RubiconImpExt::getRp).isNotNull()
    -                .extracting(RubiconImpExtRp::getTrack).containsOnly(RubiconImpExtRpTrack.of("prebid", "__"));
    -
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getDevice()).isNotNull()
    -                .extracting(Device::getExt).isNotNull()
    -                .containsOnly(jacksonMapper.fillExtension(
    -                        ExtDevice.empty(),
    -                        RubiconDeviceExt.of(RubiconDeviceExtRp.of(null))));
    -
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getSite()).isNotNull()
    -                .extracting(Site::getContent)
    -                .containsNull();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithAltSizeIdsIfMoreThanOneSize() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder
    -                        .sizes(asList(
    -                                Format.builder().w(250).h(360).build(),
    -                                Format.builder().w(300).h(250).build(),
    -                                Format.builder().w(300).h(600).build())),
    -                identity());
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(1)
    -                .extracting(Imp::getBanner).isNotNull()
    -                .extracting(Banner::getExt).isNotNull()
    -                .extracting(ext -> mapper.treeToValue(ext, RubiconBannerExt.class)).isNotNull()
    -                .extracting(RubiconBannerExt::getRp).isNotNull()
    -                .extracting(RubiconBannerExtRp::getSizeId, RubiconBannerExtRp::getAltSizeIds)
    -                .containsOnly(tuple(15, asList(10, 32)));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithoutInventoryAndVisitorDataIfAbsentInPreBidRequest() {
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(1)
    -                .extracting(imp -> imp.getExt().at("/rp/target")).containsOnly(MissingNode.getInstance());
    -
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getUser()).isNotNull()
    -                .extracting(User::getExt).containsNull();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithInventoryDataFromPreBidRequest() {
    -        // given
    -        final ObjectNode inventory = mapper.createObjectNode();
    -        inventory.set("rating", mapper.createArrayNode().add(new TextNode("5-star")));
    -        inventory.set("prodtype", mapper.createArrayNode().add(new TextNode("tech")));
    -
    -        adapterRequest = givenBidderCustomizable(identity(), builder -> builder.inventory(inventory));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(1)
    -                .extracting(imp -> imp.getExt().at("/rp/target")).containsOnly(inventory);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnBidRequestsWithVisitorDataFromPreBidRequest() {
    -        // given
    -        final ObjectNode visitor = mapper.createObjectNode();
    -        visitor.set("ucat", mapper.createArrayNode().add(new TextNode("new")));
    -        visitor.set("search", mapper.createArrayNode().add(new TextNode("iphone")));
    -
    -        adapterRequest = givenBidderCustomizable(identity(), builder -> builder.visitor(visitor));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getUser()).isNotNull()
    -                .extracting(user -> user.getExt().getProperty("rp").at("/target")).containsOnly(visitor);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestShouldReturnBidRequestWithConsentFromPreBidRequestUserExt() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(),
    -                builder -> builder.user(User.builder()
    -                        .ext(ExtUser.builder().consent("consent").build())
    -                        .build()));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getUser()).isNotNull()
    -                .extracting(User::getExt).isNotNull()
    -                .extracting(ExtUser::getConsent)
    -                .containsOnly("consent");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestShouldReturnBidRequestWithNullUserExtRpWhenVisitorIsNull() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(),
    -                builder -> builder.user(User.builder()
    -                        .ext(ExtUser.builder().consent("consent").build())
    -                        .build()));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getUser()).isNotNull()
    -                .extracting(User::getExt).isNotNull()
    -                .extracting(ext -> ext.getProperty("rp"))
    -                .containsNull();
    -    }
    -
    -    @Test
    -    public void makeHttpRequestShouldReturnBidRequestWithGdprFromPreBidRequestRegsExt() {
    -        // given
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(),
    -                builder -> builder.regs(Regs.of(null, ExtRegs.of(5, null))));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests)
    -                .extracting(r -> r.getPayload().getRegs()).isNotNull()
    -                .extracting(Regs::getExt).isNotNull()
    -                .extracting(ExtRegs::getGdpr)
    -                .containsOnly(5);
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnOneRequestWithOneImpIfAdUnitContainsBannerAndVideoMediaTypes() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBidCustomizable(builder -> builder
    -                                .mediaTypes(EnumSet.of(MediaType.video, MediaType.banner))
    -                                .video(Video.builder()
    -                                        .mimes(singletonList("Mime"))
    -                                        .playbackMethod(1)
    -                                        .build()),
    -                        identity())));
    -
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), identity());
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp())
    -                .extracting(imp -> imp.getVideo() == null, imp -> imp.getBanner() == null)
    -                .containsOnly(tuple(false, false));
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnListWithMultipleRequestsIfMultipleAdUnitsInPreBidRequest() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBidCustomizable(builder -> builder.adUnitCode("adUnitCode1"), identity()),
    -                givenAdUnitBidCustomizable(builder -> builder.adUnitCode("adUnitCode2"), identity())));
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(2)
    -                .flatExtracting(r -> r.getPayload().getImp()).hasSize(2)
    -                .extracting(Imp::getId).containsOnly("adUnitCode1", "adUnitCode2");
    -    }
    -
    -    @Test
    -    public void makeHttpRequestsShouldReturnRequestsWithoutVideoExtWhenMediaTypeIsVideoAndRubiconParamsVideoIsNull() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBidCustomizable(builder -> builder
    -                                .mediaTypes(Collections.singleton(MediaType.video))
    -                                .video(Video.builder()
    -                                        .mimes(Collections.singletonList("Mime"))
    -                                        .playbackMethod(1)
    -                                        .build()),
    -                        identity())));
    -
    -        preBidRequestContext = givenPreBidRequestContextCustomizable(identity(), identity());
    -
    -        // when
    -        final List> httpRequests = adapter.makeHttpRequests(adapterRequest,
    -                preBidRequestContext);
    -
    -        // then
    -        assertThat(httpRequests).hasSize(1)
    -                .flatExtracting(r -> r.getPayload().getImp())
    -                .extracting(Imp::getVideo)
    -                .extracting(com.iab.openrtb.request.Video::getExt).containsNull();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldFailIfBidImpIdDoesNotMatchAdUnitCode() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(builder -> builder.adUnitCode("adUnitCode"), identity());
    -
    -        exchangeCall = givenExchangeCallCustomizable(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder().impid("anotherAdUnitCode").price(new BigDecimal(10)).build()))
    -                        .build())));
    -
    -        // when and then
    -        assertThatThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall))
    -                .isExactlyInstanceOf(PreBidException.class)
    -                .hasMessage("Unknown ad unit code 'anotherAdUnitCode'");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithExpectedFields() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCallCustomizable(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .price(new BigDecimal("8.43"))
    -                                        .adm("adm")
    -                                        .crid("crid")
    -                                        .w(300)
    -                                        .h(250)
    -                                        .dealid("dealId")
    -                                        .ext(mapper.valueToTree(RubiconTargetingExt.of(RubiconTargetingExtRp.of(
    -                                                singletonList(RubiconTargeting.of("key", singletonList("value")))))))
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids)
    -                .containsExactly(org.prebid.server.proto.response.Bid.builder()
    -                        .code("adUnitCode")
    -                        .price(new BigDecimal("8.43"))
    -                        .adm("adm")
    -                        .creativeId("crid")
    -                        .width(300)
    -                        .height(250)
    -                        .dealId("dealId")
    -                        .mediaType(MediaType.banner)
    -                        .adServerTargeting(singletonMap("key", "value"))
    -                        .bidder(BIDDER)
    -                        .bidId("bidId")
    -                        .build());
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnEmptyBidsIfEmptyOrNullBidResponse() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(identity(), identity());
    -
    -        exchangeCall = givenExchangeCallCustomizable(identity(), br -> br.seatbid(null));
    -
    -        // when and then
    -        assertThat(adapter.extractBids(adapterRequest, exchangeCall)).isEmpty();
    -        assertThat(adapter.extractBids(adapterRequest, ExchangeCall.empty(null))).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnOnlyFirstBidBuilderFromMultipleBidsInResponse() {
    -        // given
    -        adapterRequest = AdapterRequest.of(BIDDER, asList(
    -                givenAdUnitBidCustomizable(builder -> builder.adUnitCode("adUnitCode1"), identity()),
    -                givenAdUnitBidCustomizable(builder -> builder.adUnitCode("adUnitCode2"), identity())));
    -
    -        exchangeCall = givenExchangeCallCustomizable(identity(),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(asList(Bid.builder().impid("adUnitCode1").price(new BigDecimal("1.1")).build(),
    -                                        Bid.builder().impid("adUnitCode2").price(new BigDecimal("2.2")).build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(1)
    -                .extracting(org.prebid.server.proto.response.Bid::getCode)
    -                .containsOnly("adUnitCode1");
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithZeroPriceBidsFilteredOut() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCallCustomizable(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .price(new BigDecimal("0"))
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).isEmpty();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithEmptyAdTargetingIfRubiconTargetingCouldNotBeParsed() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        final ObjectNode ext = mapper.createObjectNode();
    -        ext.set("rp", new TextNode("non-object"));
    -
    -        exchangeCall = givenExchangeCallCustomizable(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .price(new BigDecimal("10"))
    -                                        .ext(ext)
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(1)
    -                .extracting(org.prebid.server.proto.response.Bid::getAdServerTargeting).containsNull();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithEmptyAdTargetingIfNoRubiconTargetingInBidResponse() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCallCustomizable(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .price(new BigDecimal("10"))
    -                                        .ext(null)
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(1)
    -                .extracting(org.prebid.server.proto.response.Bid::getAdServerTargeting).containsNull();
    -    }
    -
    -    @Test
    -    public void extractBidsShouldReturnBidBuildersWithNotEmptyAdTargetingIfRubiconTargetingPresentInBidResponse() {
    -        // given
    -        adapterRequest = givenBidderCustomizable(
    -                builder -> builder.bidderCode(BIDDER).bidId("bidId").adUnitCode("adUnitCode"),
    -                identity());
    -
    -        exchangeCall = givenExchangeCallCustomizable(
    -                bidRequestBuilder -> bidRequestBuilder.imp(singletonList(Imp.builder().id("adUnitCode").build())),
    -                bidResponseBuilder -> bidResponseBuilder.id("bidResponseId")
    -                        .seatbid(singletonList(SeatBid.builder()
    -                                .seat("seatId")
    -                                .bid(singletonList(Bid.builder()
    -                                        .impid("adUnitCode")
    -                                        .price(new BigDecimal("10"))
    -                                        .ext(mapper.valueToTree(RubiconTargetingExt.of(RubiconTargetingExtRp.of(asList(
    -                                                RubiconTargeting.of("key1", singletonList("value1")),
    -                                                RubiconTargeting.of("key2", singletonList("value2")))))))
    -                                        .build()))
    -                                .build())));
    -
    -        // when
    -        final List bids =
    -                adapter.extractBids(adapterRequest, exchangeCall).stream()
    -                        .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList());
    -
    -        // then
    -        assertThat(bids).hasSize(1);
    -        assertThat(bids).flatExtracting(bid -> bid.getAdServerTargeting().entrySet())
    -                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    -                .containsOnly(
    -                        tuple("key1", "value1"),
    -                        tuple("key2", "value2"));
    -    }
    -
    -    private static AdapterRequest givenBidderCustomizable(
    -            Function adUnitBidBuilderCustomizer,
    -            Function rubiconParamsBuilderCustomizer) {
    -
    -        return AdapterRequest.of(BIDDER, singletonList(
    -                givenAdUnitBidCustomizable(adUnitBidBuilderCustomizer, rubiconParamsBuilderCustomizer)));
    -    }
    -
    -    private static AdUnitBid givenAdUnitBidCustomizable(
    -            Function adUnitBidBuilderCustomizer,
    -            Function rubiconParamsBuilderCustomizer) {
    -
    -        // params
    -        final RubiconParamsBuilder rubiconParamsBuilder = RubiconParams.builder()
    -                .accountId(2001)
    -                .siteId(3001)
    -                .zoneId(4001);
    -        final RubiconParamsBuilder rubiconParamsBuilderCustomized = rubiconParamsBuilderCustomizer
    -                .apply(rubiconParamsBuilder);
    -        final RubiconParams rubiconParams = rubiconParamsBuilderCustomized.build();
    -
    -        // ad unit bid
    -        final AdUnitBidBuilder adUnitBidBuilderMinimal = AdUnitBid.builder()
    -                .sizes(singletonList(Format.builder().w(300).h(250).build()))
    -                .params(mapper.valueToTree(rubiconParams))
    -                .mediaTypes(singleton(MediaType.banner));
    -        final AdUnitBidBuilder adUnitBidBuilderCustomized = adUnitBidBuilderCustomizer.apply(adUnitBidBuilderMinimal);
    -
    -        return adUnitBidBuilderCustomized.build();
    -    }
    -
    -    private PreBidRequestContext givenPreBidRequestContextCustomizable(
    -            Function preBidRequestContextBuilderCustomizer,
    -            Function
    -                    preBidRequestBuilderCustomizer) {
    -
    -        final PreBidRequest.PreBidRequestBuilder preBidRequestBuilderMinimal = PreBidRequest.builder()
    -                .accountId("accountId");
    -        final PreBidRequest preBidRequest = preBidRequestBuilderCustomizer.apply(preBidRequestBuilderMinimal).build();
    -
    -        final PreBidRequestContext.PreBidRequestContextBuilder preBidRequestContextBuilderMinimal =
    -                PreBidRequestContext.builder()
    -                        .preBidRequest(preBidRequest)
    -                        .uidsCookie(uidsCookie);
    -        return preBidRequestContextBuilderCustomizer.apply(preBidRequestContextBuilderMinimal).build();
    -    }
    -
    -    private static ExchangeCall givenExchangeCallCustomizable(
    -            Function bidRequestBuilderCustomizer,
    -            Function bidResponseBuilderCustomizer) {
    -
    -        final BidRequest.BidRequestBuilder bidRequestBuilderMinimal = BidRequest.builder();
    -        final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(bidRequestBuilderMinimal).build();
    -
    -        final BidResponse.BidResponseBuilder bidResponseBuilderMinimal = BidResponse.builder();
    -        final BidResponse bidResponse = bidResponseBuilderCustomizer.apply(bidResponseBuilderMinimal).build();
    -
    -        return ExchangeCall.success(bidRequest, bidResponse, BidderDebug.builder().build());
    -    }
    -}
    diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java
    index a6cb40a9157..fe13a33289d 100644
    --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java
    @@ -1,32 +1,43 @@
     package org.prebid.server.bidder.rubicon;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.node.ArrayNode;
    +import com.fasterxml.jackson.databind.node.IntNode;
     import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.fasterxml.jackson.databind.node.TextNode;
     import com.iab.openrtb.request.App;
    +import com.iab.openrtb.request.Audio;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.BidRequest.BidRequestBuilder;
     import com.iab.openrtb.request.Content;
    +import com.iab.openrtb.request.Data;
    +import com.iab.openrtb.request.Deal;
     import com.iab.openrtb.request.Device;
     import com.iab.openrtb.request.Format;
     import com.iab.openrtb.request.Geo;
     import com.iab.openrtb.request.Imp;
     import com.iab.openrtb.request.Imp.ImpBuilder;
     import com.iab.openrtb.request.Metric;
    +import com.iab.openrtb.request.Native;
    +import com.iab.openrtb.request.Pmp;
     import com.iab.openrtb.request.Publisher;
     import com.iab.openrtb.request.Regs;
    +import com.iab.openrtb.request.Segment;
     import com.iab.openrtb.request.Site;
     import com.iab.openrtb.request.Source;
     import com.iab.openrtb.request.User;
     import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
    -import com.iab.openrtb.response.BidResponse;
    -import com.iab.openrtb.response.SeatBid;
     import io.vertx.core.http.HttpMethod;
     import lombok.AllArgsConstructor;
     import lombok.Value;
     import org.junit.Before;
    +import org.junit.Rule;
     import org.junit.Test;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
     import org.prebid.server.VertxTest;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderError;
    @@ -34,58 +45,61 @@
     import org.prebid.server.bidder.model.HttpRequest;
     import org.prebid.server.bidder.model.HttpResponse;
     import org.prebid.server.bidder.model.Result;
    -import org.prebid.server.bidder.rubicon.proto.RubiconAppExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconBannerExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconBannerExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconImpExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtPrebidBidder;
    -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtPrebidRubiconDebug;
    -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconImpExtRpTrack;
    -import org.prebid.server.bidder.rubicon.proto.RubiconPubExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconPubExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconSiteExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconSiteExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconTargeting;
    -import org.prebid.server.bidder.rubicon.proto.RubiconTargetingExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconTargetingExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconUserExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconUserExtRp;
    -import org.prebid.server.bidder.rubicon.proto.RubiconVideoExt;
    -import org.prebid.server.bidder.rubicon.proto.RubiconVideoExtRp;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconAppExt;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconBannerExt;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconBannerExtRp;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExt;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExtRp;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExtRpTrack;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconPubExt;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconPubExtRp;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconSiteExt;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconSiteExtRp;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconTargeting;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconTargetingExt;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconTargetingExtRp;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconUserExt;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconUserExtRp;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconVideoExt;
    +import org.prebid.server.bidder.rubicon.proto.request.RubiconVideoExtRp;
    +import org.prebid.server.bidder.rubicon.proto.response.RubiconBidResponse;
    +import org.prebid.server.bidder.rubicon.proto.response.RubiconSeatBid;
    +import org.prebid.server.currency.CurrencyConversionService;
    +import org.prebid.server.exception.PreBidException;
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
     import org.prebid.server.proto.openrtb.ext.ExtPrebidBidders;
     import org.prebid.server.proto.openrtb.ext.request.ExtApp;
    -import org.prebid.server.proto.openrtb.ext.request.ExtImp;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDeal;
    +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine;
     import org.prebid.server.proto.openrtb.ext.request.ExtImpContext;
     import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
     import org.prebid.server.proto.openrtb.ext.request.ExtPublisher;
     import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
     import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid;
     import org.prebid.server.proto.openrtb.ext.request.ExtSite;
     import org.prebid.server.proto.openrtb.ext.request.ExtUser;
    -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEid;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUidExt;
     import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubicon;
     import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubicon.ExtImpRubiconBuilder;
    +import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubiconDebug;
     import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtUserTpIdRubicon;
     import org.prebid.server.proto.openrtb.ext.request.rubicon.RubiconVideoParams;
    +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
     import org.prebid.server.util.HttpUtil;
     
     import java.io.IOException;
     import java.math.BigDecimal;
     import java.util.Arrays;
    -import java.util.Collections;
     import java.util.List;
     import java.util.Map;
     import java.util.function.Function;
     
     import static java.math.BigDecimal.ONE;
     import static java.math.BigDecimal.TEN;
    -import static java.math.BigDecimal.ZERO;
     import static java.util.Arrays.asList;
     import static java.util.Collections.emptyList;
     import static java.util.Collections.singletonList;
    @@ -94,6 +108,12 @@
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
     import static org.assertj.core.api.Assertions.entry;
     import static org.assertj.core.api.Assertions.tuple;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.ArgumentMatchers.eq;
    +import static org.mockito.BDDMockito.given;
    +import static org.mockito.Mockito.verify;
    +import static org.mockito.Mockito.verifyZeroInteractions;
     import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
     import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
     
    @@ -105,17 +125,25 @@ public class RubiconBidderTest extends VertxTest {
         private static final List SUPPORTED_VENDORS = Arrays.asList("activeview", "adform",
                 "comscore", "doubleverify", "integralads", "moat", "sizmek", "whiteops");
     
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
         private RubiconBidder rubiconBidder;
     
    +    @Mock
    +    private CurrencyConversionService currencyConversionService;
    +
         @Before
         public void setUp() {
    -        rubiconBidder = new RubiconBidder(ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, false, jacksonMapper);
    +        rubiconBidder = new RubiconBidder(ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, false,
    +                currencyConversionService, jacksonMapper);
         }
     
         @Test
         public void creationShouldFailOnInvalidEndpointUrl() {
             assertThatIllegalArgumentException().isThrownBy(
    -                () -> new RubiconBidder("invalid_url", USERNAME, PASSWORD, SUPPORTED_VENDORS, false, jacksonMapper));
    +                () -> new RubiconBidder("invalid_url", USERNAME, PASSWORD, SUPPORTED_VENDORS, false,
    +                        currencyConversionService, jacksonMapper));
         }
     
         @Test
    @@ -140,14 +168,57 @@ public void makeHttpRequestsShouldFillMethodAndUrlAndExpectedHeaders() {
                             tuple(HttpUtil.USER_AGENT_HEADER.toString(), "prebid-server/1.0"));
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldFilterImpressionsWithInvalidTypes() {
    +        // given
    +        final Imp imp1 = givenImp(builder -> builder.video(Video.builder().build()));
    +        final Imp imp2 = givenImp(builder -> builder.id("2").xNative(Native.builder().build()));
    +        final Imp imp3 = givenImp(builder -> builder.id("3").audio(Audio.builder().build()));
    +        final BidRequest bidRequest = BidRequest.builder().imp(asList(imp1, imp2, imp3)).build();
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(2)
    +                .containsOnly(
    +                        BidderError.of("Impression with id 2 rejected with invalid type `xNative`."
    +                                + " Allowed types are banner and video.", BidderError.Type.bad_input),
    +                        BidderError.of("Impression with id 3 rejected with invalid type `audio`."
    +                                + " Allowed types are banner and video.", BidderError.Type.bad_input));
    +        assertThat(result.getValue()).hasSize(1);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldFilterAllImpressionsAndReturnErrorMeessagesWithoutRequests() {
    +        // given
    +        final Imp imp1 = givenImp(builder -> builder.id("1").xNative(Native.builder().build()));
    +        final Imp imp2 = givenImp(builder -> builder.id("2").audio(Audio.builder().build()));
    +        final BidRequest bidRequest = BidRequest.builder().imp(asList(imp1, imp2)).build();
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(3)
    +                .containsOnly(
    +                        BidderError.of("Impression with id 1 rejected with invalid type `xNative`."
    +                                + " Allowed types are banner and video.", BidderError.Type.bad_input),
    +                        BidderError.of("Impression with id 2 rejected with invalid type `audio`."
    +                                + " Allowed types are banner and video.", BidderError.Type.bad_input),
    +                        BidderError.of("There are no valid impressions to create bid request to rubicon bidder",
    +                                BidderError.Type.bad_input));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
         @Test
         public void makeHttpRequestsShouldReplaceDefaultParametersWithExtPrebidBiddersBidder() {
             // given
    -        final ExtRequest prebidExt = ExtRequest.of(ExtRequestPrebid.builder()
    +        final ExtRequest extRequest = ExtRequest.of(ExtRequestPrebid.builder()
                     .integration("test")
                     .build());
     
    -        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.ext(prebidExt),
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.ext(extRequest),
                     builder -> builder.banner(Banner.builder().format(singletonList(Format.builder().w(300).h(250).build()))
                             .build()), identity());
     
    @@ -161,6 +232,55 @@ public void makeHttpRequestsShouldReplaceDefaultParametersWithExtPrebidBiddersBi
                     .returns(expectedUrl, HttpRequest::getUri);
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldAddMaxbidsAttributeFromExtPrebidMultibid() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .multibid(singletonList(ExtRequestPrebidMultiBid.of("rubicon", null, 5, "prefix")))
    +                        .build())),
    +                impBuilder -> impBuilder.video(Video.builder().build()),
    +                identity());
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).doesNotContainNull()
    +                .extracting(Imp::getExt).doesNotContainNull()
    +                .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class))
    +                .extracting(RubiconImpExt::getMaxbids)
    +                .containsExactly(5);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldAddMaxbidsAttributeAsOneIfExtPrebidMultibidMaxBidsIsNotPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
    +                        .multibid(singletonList(
    +                                ExtRequestPrebidMultiBid.of("rubicon", null, null, "prefix")))
    +                        .build())),
    +                impBuilder -> impBuilder.video(Video.builder().build()),
    +                identity());
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).doesNotContainNull()
    +                .extracting(Imp::getExt).doesNotContainNull()
    +                .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class))
    +                .extracting(RubiconImpExt::getMaxbids)
    +                .containsExactly(1);
    +    }
    +
         @Test
         public void makeHttpRequestsShouldFillImpExt() {
             // given
    @@ -182,7 +302,7 @@ public void makeHttpRequestsShouldFillImpExt() {
                     .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class))
                     .containsOnly(RubiconImpExt.of(RubiconImpExtRp.of(4001,
                             mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech"))),
    -                        RubiconImpExtRpTrack.of("", "")), null));
    +                        RubiconImpExtRpTrack.of("", "")), null, 1, null));
         }
     
         @Test
    @@ -265,6 +385,65 @@ public void makeHttpRequestsShouldOverrideBannerFormatWithRubiconSizes() {
                     .containsOnly(Format.builder().w(300).h(250).build());
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldSetMobilePortrait67SizeIdFotInterstitialNotValidSize() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .instl(1)
    +                        .banner(Banner.builder()
    +                                .format(singletonList(
    +                                        Format.builder().w(360).h(616).build()))
    +                                .build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpRubicon.builder().build())))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).doesNotContainNull()
    +                .extracting(Imp::getBanner).doesNotContainNull()
    +                .containsOnly(Banner.builder()
    +                        .format(singletonList(
    +                                Format.builder().w(360).h(616).build()))
    +                        .ext(mapper.valueToTree(RubiconBannerExt.of(RubiconBannerExtRp.of(67, null, "text/html"))))
    +                        .build());
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetMobileLandscape101SizeIdFotInterstitialNotValidSize() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(singletonList(Imp.builder()
    +                        .instl(1)
    +                        .banner(Banner.builder()
    +                                .format(singletonList(
    +                                        Format.builder().w(616).h(360).build()))
    +                                .build())
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpRubicon.builder().build())))
    +                        .build()))
    +                .build();
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).doesNotContainNull()
    +                .extracting(Imp::getBanner).doesNotContainNull()
    +                .containsOnly(Banner.builder()
    +                        .format(singletonList(Format.builder().w(616).h(360).build()))
    +                        .ext(mapper.valueToTree(RubiconBannerExt.of(RubiconBannerExtRp.of(101, null, "text/html"))))
    +                        .build());
    +    }
    +
         @Test
         public void makeHttpRequestsShouldCreateBannerRequestIfImpHasBannerAndVideoButNoRequiredVideoFieldsPresent() {
             // given
    @@ -317,6 +496,150 @@ public void makeHttpRequestsShouldCreateVideoRequestIfImpHasBannerAndVideoButAll
                                     .maxduration(60).linearity(2).api(singletonList(3)).build()));
         }
     
    +    @Test
    +    public void makeHttpRequestsShouldResolveImpBidFloorCurrencyIfNotUSDAndCallCurrencyService() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder
    +                        .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(250).build())).build())
    +                        .bidfloor(BigDecimal.ONE).bidfloorcur("EUR"),
    +                identity());
    +
    +        given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString()))
    +                .willReturn(BigDecimal.TEN);
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        verify(currencyConversionService).convertCurrency(eq(BigDecimal.ONE), any(), eq("EUR"), eq("USD"));
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).doesNotContainNull()
    +                .extracting(Imp::getBidfloor, Imp::getBidfloorcur)
    +                .containsOnly(tuple(BigDecimal.TEN, "USD"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotSetBidFloorCurrencyToUSDIfNull() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder
    +                        .ext(ExtRequest.of(ExtRequestPrebid.builder().debug(1).build())),
    +                builder -> builder
    +                        .id("impId")
    +                        .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(250).build())).build())
    +                        .bidfloor(BigDecimal.ONE).bidfloorcur(null),
    +                identity());
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        verifyZeroInteractions(currencyConversionService);
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsOnly(BidderError.of("Imp `impId` floor provided with no currency, assuming USD",
    +                        BidderError.Type.bad_input));
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).doesNotContainNull()
    +                .extracting(Imp::getBidfloor, Imp::getBidfloorcur)
    +                .containsOnly(tuple(BigDecimal.ONE, null));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldIgnoreBidRequestIfCurrencyServiceThrowsAnException() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder
    +                        .id("impId")
    +                        .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(250).build())).build())
    +                        .bidfloor(BigDecimal.ONE).bidfloorcur("EUR"),
    +                identity());
    +
    +        given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString()))
    +                .willThrow(new PreBidException("failed"));
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).hasSize(1)
    +                .containsOnly(BidderError.of("Unable to convert provided bid floor currency from EUR to USD"
    +                        + " for imp `impId` with a reason: failed", BidderError.Type.bad_input));
    +    }
    +
    +    @Test
    +    public void shouldSetSizeIdTo201IfPlacementIs1AndSizeIdIsNotPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.instl(1).video(Video.builder().placement(1).build()),
    +                builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build()));
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getVideo).doesNotContainNull()
    +                .extracting(Video::getExt).doesNotContainNull()
    +                .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class))
    +                .extracting(RubiconVideoExt::getRp)
    +                .extracting(RubiconVideoExtRp::getSizeId)
    +                .containsOnly(201);
    +    }
    +
    +    @Test
    +    public void shouldSetSizeIdTo203IfPlacementIs3AndSizeIdIsNotPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.instl(1).video(Video.builder().placement(3).build()),
    +                builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build()));
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getVideo).doesNotContainNull()
    +                .extracting(Video::getExt).doesNotContainNull()
    +                .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class))
    +                .extracting(RubiconVideoExt::getRp)
    +                .extracting(RubiconVideoExtRp::getSizeId)
    +                .containsOnly(203);
    +    }
    +
    +    @Test
    +    public void shouldSetSizeIdTo202UsingInstlFlagIfPlacementAndSizeIdAreNotPresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.instl(1).video(Video.builder().placement(null).build()),
    +                builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build()));
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getVideo).doesNotContainNull()
    +                .extracting(Video::getExt).doesNotContainNull()
    +                .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class))
    +                .extracting(RubiconVideoExt::getRp)
    +                .extracting(RubiconVideoExtRp::getSizeId)
    +                .containsOnly(202);
    +    }
    +
         @Test
         public void makeHttpRequestsShouldFillVideoExt() {
             // given
    @@ -496,37 +819,141 @@ public void makeHttpRequestsShouldFillUserExtIfUserAndVisitorPresent() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldNotFillUserExtRpWhenVisitorAndInventoryIsEmpty() {
    +    public void makeHttpRequestsShouldFillUserExtRpWithIabAttributeIfSegtaxEqualsFour() {
             // given
             final BidRequest bidRequest = givenBidRequest(
    -                builder -> builder.user(User.builder().id("id").build()),
    +                builder -> builder.user(User.builder()
    +                        .data(singletonList(
    +                                givenDataWithSegmentEntry(4, "segmentId")
    +                        )).build()),
                     builder -> builder.video(Video.builder().build()),
    -                builder -> builder
    -                        .visitor(mapper.createObjectNode())
    -                        .inventory(mapper.createObjectNode()));
    +                identity());
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
     
             // then
    +        final ObjectNode expectedTarget = mapper.createObjectNode();
    +        final ArrayNode expectedIabAttribute = expectedTarget.putArray("iab");
    +        expectedIabAttribute.add("segmentId");
    +
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue()).hasSize(1).doesNotContainNull()
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .extracting(BidRequest::getUser).doesNotContainNull()
    -                .containsOnly(User.builder().id("id").build());
    +                .extracting(User::getExt)
    +                .containsOnly(jacksonMapper.fillExtension(
    +                        ExtUser.builder().build(),
    +                        RubiconUserExt.builder()
    +                                .rp(RubiconUserExtRp.of(expectedTarget))
    +                                .build()));
         }
     
         @Test
    -    public void makeHttpRequestsShouldFillUserExtIfUserAndDigiTrustPresent() {
    +    public void makeHttpRequestsShouldFillUserExtRpWithIabAttributeOnlyIfSegtaxIsEqualFour() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     builder -> builder.user(User.builder()
    -                        .ext(ExtUser.builder()
    -                                .digitrust(ExtUserDigiTrust.of("id", 123, 0))
    +                        .data(asList(
    +                                givenDataWithSegmentEntry(4, "segmentId"),
    +                                givenDataWithSegmentEntry(2, "secondSegmentId")))
    +                        .build()),
    +                builder -> builder.video(Video.builder().build()),
    +                identity());
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedTarget = mapper.createObjectNode();
    +        final ArrayNode expectedIabAttribute = expectedTarget.putArray("iab");
    +        expectedIabAttribute.add("segmentId");
    +
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getUser).doesNotContainNull()
    +                .extracting(User::getExt)
    +                .containsOnly(jacksonMapper.fillExtension(
    +                        ExtUser.builder().build(),
    +                        RubiconUserExt.builder()
    +                                .rp(RubiconUserExtRp.of(expectedTarget))
    +                                .build()));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldFillSiteExtRpWithIabAttributeIfSegtaxEqualsOneOrTwoOrFiveOrSix() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.site(Site.builder()
    +                        .content(Content.builder()
    +                                .data(asList(
    +                                        givenDataWithSegmentEntry(1, "firstSegmentId"),
    +                                        givenDataWithSegmentEntry(2, "secondSegmentId"),
    +                                        givenDataWithSegmentEntry(3, "thirdSegmentId"),
    +                                        givenDataWithSegmentEntry(5, "fifthSegmentId"),
    +                                        givenDataWithSegmentEntry(6, "sixthSegmentId")))
                                     .build())
                             .build()),
                     builder -> builder.video(Video.builder().build()),
    -                identity());
    +                identity());
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        final ObjectNode expectedTarget = mapper.createObjectNode();
    +        final ArrayNode expectedIabAttribute = expectedTarget.putArray("iab");
    +        expectedIabAttribute.add("firstSegmentId");
    +        expectedIabAttribute.add("secondSegmentId");
    +        expectedIabAttribute.add("fifthSegmentId");
    +        expectedIabAttribute.add("sixthSegmentId");
    +
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getSite).doesNotContainNull()
    +                .extracting(Site::getExt)
    +                .extracting(ext -> ext.getProperty("rp"))
    +                .extracting(rp -> rp.get("target"))
    +                .containsExactly(expectedTarget);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldIgnoreNotIntSegtaxProperty() {
    +        // given
    +        final ObjectNode userNode = mapper.createObjectNode();
    +        userNode.put("segtax", "3");
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.user(User.builder().data(singletonList(Data.builder()
    +                        .segment(singletonList(Segment.builder().id("segmentId")
    +                                .build()))
    +                        .ext(userNode).build())).build()),
    +                builder -> builder.video(Video.builder().build()),
    +                identity());
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1).doesNotContainNull()
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .extracting(BidRequest::getUser).doesNotContainNull()
    +                .extracting(User::getExt)
    +                .containsNull();
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldNotFillUserExtRpWhenVisitorAndInventoryIsEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                builder -> builder.user(User.builder().id("id").build()),
    +                builder -> builder.video(Video.builder().build()),
    +                builder -> builder
    +                        .visitor(mapper.createObjectNode())
    +                        .inventory(mapper.createObjectNode()));
    +
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
     
    @@ -535,11 +962,7 @@ public void makeHttpRequestsShouldFillUserExtIfUserAndDigiTrustPresent() {
             assertThat(result.getValue()).hasSize(1).doesNotContainNull()
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .extracting(BidRequest::getUser).doesNotContainNull()
    -                .containsOnly(User.builder()
    -                        .ext(ExtUser.builder()
    -                                .digitrust(ExtUserDigiTrust.of("id", 123, 0))
    -                                .build())
    -                        .build());
    +                .containsOnly(User.builder().id("id").build());
         }
     
         @Test
    @@ -691,11 +1114,12 @@ public void makeHttpRequestsShouldNormalizeAndCopyUserExtDataFieldsToUserExtRp()
                     // will be normalized to array of strings
                     .put("property2", "value1")
                     .put("property3", 123)
    +                .put("property4", false)
    +                .set("property5", mapper.createArrayNode().add(true).add(false))
                     // remnants will be discarded
    -                .set("property4", mapper.createArrayNode().add("value1").add(123))
    -                .set("property5", mapper.createObjectNode().put("sub-property1", "value1"))
    -                .put("property6", 123.456d)
    -                .put("property7", false);
    +                .set("property6", mapper.createArrayNode().add("value1").add(123))
    +                .set("property7", mapper.createObjectNode().put("sub-property1", "value1"))
    +                .put("property8", 123.456d);
     
             final BidRequest bidRequest = givenBidRequest(
                     builder -> builder
    @@ -722,12 +1146,16 @@ public void makeHttpRequestsShouldNormalizeAndCopyUserExtDataFieldsToUserExtRp()
                                                     .add("value1")
                                                     .add("value2"))
                                             .set("property2", mapper.createArrayNode().add("value1"))
    -                                        .set("property3", mapper.createArrayNode().add("123"))))
    +                                        .set("property3", mapper.createArrayNode().add("123"))
    +                                        .set("property4", mapper.createArrayNode().add("false"))
    +                                        .set("property5", mapper.createArrayNode()
    +                                                .add("true")
    +                                                .add("false"))))
                                     .build()));
         }
     
         @Test
    -    public void makeHttpRequestsShouldNotCreateUserIfVisitorAndDigiTrustAndConsentNotPresent() {
    +    public void makeHttpRequestsShouldNotCreateUserIfVisitorAndConsentNotPresent() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
    @@ -1172,7 +1600,7 @@ public void makeHttpRequestsShouldFillSiteExtIfSitePresent() {
                                             RubiconPubExt.of(RubiconPubExtRp.of(2001))))
                                     .build())
                             .ext(jacksonMapper.fillExtension(
    -                                ExtSite.of(null, null), RubiconSiteExt.of(RubiconSiteExtRp.of(3001))))
    +                                ExtSite.of(null, null), RubiconSiteExt.of(RubiconSiteExtRp.of(3001, null))))
                             .build());
         }
     
    @@ -1199,7 +1627,7 @@ public void makeHttpRequestsShouldPassSiteExtAmpIfPresent() {
                                             RubiconPubExt.of(RubiconPubExtRp.of(2001))))
                                     .build())
                             .ext(jacksonMapper.fillExtension(
    -                                ExtSite.of(1, null), RubiconSiteExt.of(RubiconSiteExtRp.of(3001))))
    +                                ExtSite.of(1, null), RubiconSiteExt.of(RubiconSiteExtRp.of(3001, null))))
                             .build());
         }
     
    @@ -1221,7 +1649,7 @@ public void makeHttpRequestsShouldRemoveSiteExtData() {
                     .extracting(Site::getExt)
                     .containsOnly(jacksonMapper.fillExtension(
                             ExtSite.of(null, null),
    -                        RubiconSiteExt.of(RubiconSiteExtRp.of(3001))));
    +                        RubiconSiteExt.of(RubiconSiteExtRp.of(3001, null))));
         }
     
         @Test
    @@ -1248,7 +1676,7 @@ public void makeHttpRequestsShouldFillAppExtIfAppPresent() {
                                     .build())
                             .ext(jacksonMapper.fillExtension(
                                     ExtApp.of(null, null),
    -                                RubiconAppExt.of(RubiconSiteExtRp.of(3001))))
    +                                RubiconAppExt.of(RubiconSiteExtRp.of(3001, null))))
                             .build());
         }
     
    @@ -1270,7 +1698,7 @@ public void makeHttpRequestsShouldRemoveAppExtData() {
                     .extracting(App::getExt)
                     .containsOnly(jacksonMapper.fillExtension(
                             ExtApp.of(null, null),
    -                        RubiconAppExt.of(RubiconSiteExtRp.of(3001))));
    +                        RubiconAppExt.of(RubiconSiteExtRp.of(3001, null))));
         }
     
         @Test
    @@ -1324,7 +1752,7 @@ public void makeHttpRequestsShouldCreateRequestPerImp() {
                     .imp(singletonList(Imp.builder()
                             .video(Video.builder().build())
                             .ext(mapper.valueToTree(RubiconImpExt.of(
    -                                RubiconImpExtRp.of(null, null, RubiconImpExtRpTrack.of("", "")), null)))
    +                                RubiconImpExtRp.of(null, null, RubiconImpExtRpTrack.of("", "")), null, 1, null)))
                             .build()))
                     .build();
             final BidRequest expectedBidRequest2 = BidRequest.builder()
    @@ -1332,7 +1760,7 @@ public void makeHttpRequestsShouldCreateRequestPerImp() {
                             .id("2")
                             .video(Video.builder().build())
                             .ext(mapper.valueToTree(RubiconImpExt.of(
    -                                RubiconImpExtRp.of(null, null, RubiconImpExtRpTrack.of("", "")), null)))
    +                                RubiconImpExtRp.of(null, null, RubiconImpExtRpTrack.of("", "")), null, 1, null)))
                             .build()))
                     .build();
     
    @@ -1343,18 +1771,19 @@ public void makeHttpRequestsShouldCreateRequestPerImp() {
         }
     
         @Test
    -    public void makeHttpRequestsShouldCopyAndModifyImpExtContextDataFieldsToRubiconImpExtRpTarget() {
    +    public void makeHttpRequestsShouldCopyAndModifyImpExtContextDataAndDataFieldsToRubiconImpExtRpTarget() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
                     impBuilder -> impBuilder.video(Video.builder().build()),
                     identity());
     
    -        final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        final ObjectNode impExtContextDataNode = mapper.createObjectNode()
    -                .set("property", mapper.createArrayNode().add("value"))
    -                .put("adslot", "/test");
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, null, impExtContextDataNode)));
    +        bidRequest.getImp().get(0).getExt()
    +                .set("context", mapper.createObjectNode()
    +                        .set("data", mapper.createObjectNode()
    +                                .set("property1", mapper.createArrayNode().add("value1"))))
    +                .set("data", mapper.createObjectNode()
    +                        .set("property2", mapper.createArrayNode().add("value2")));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1369,28 +1798,30 @@ public void makeHttpRequestsShouldCopyAndModifyImpExtContextDataFieldsToRubiconI
                     .extracting(RubiconImpExt::getRp)
                     .extracting(RubiconImpExtRp::getTarget)
                     .containsOnly(mapper.createObjectNode()
    -                        .set("property", mapper.createArrayNode().add("value"))
    -                        .set("adslot", mapper.createArrayNode().add("/test"))
    -                        .put("dfp_ad_unit_code", "test"));
    +                        .set("property1", mapper.createArrayNode().add("value1"))
    +                        .set("property2", mapper.createArrayNode().add("value2")));
         }
     
         @Test
    -    public void makeHttpRequestsShouldPreferDataAdSlotWhenAdserverIsGam() {
    +    public void makeHttpRequestsShouldPreferContextDataGamAdSlot() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
                     impBuilder -> impBuilder.video(Video.builder().build()),
                     identity());
     
    -        final ObjectNode adserverNode = mapper.createObjectNode();
    -        adserverNode.put("name", "gam");
    -        adserverNode.put("adslot", "/test-adserver");
    -        final ObjectNode impExtContextDataNode = mapper.createObjectNode()
    -                .put("adslot", "/test-data")
    -                .set("adserver", adserverNode);
    -
    -        final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, null, impExtContextDataNode)));
    +        bidRequest.getImp().get(0).getExt()
    +                .set("context", mapper.createObjectNode()
    +                        .set("data", mapper.createObjectNode()
    +                                .put("pbadslot", "/test-pbadslot-context-data")
    +                                .set("adserver", mapper.createObjectNode()
    +                                        .put("name", "gam")
    +                                        .put("adslot", "/test-adserver-context-data"))))
    +                .set("data", mapper.createObjectNode()
    +                        .put("pbadslot", "/test-pbadslot-data")
    +                        .set("adserver", mapper.createObjectNode()
    +                                .put("name", "gam")
    +                                .put("adslot", "/test-adserver-data")));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1402,29 +1833,27 @@ public void makeHttpRequestsShouldPreferDataAdSlotWhenAdserverIsGam() {
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getExt)
                     .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class))
    -                .extracting(RubiconImpExt::getRp)
    -                .extracting(RubiconImpExtRp::getTarget)
    -                .containsOnly(mapper.createObjectNode()
    -                        .set("adslot", mapper.createArrayNode().add("/test-data"))
    -                        .put("dfp_ad_unit_code", "test-data"));
    +                .extracting(RubiconImpExt::getGpid)
    +                .containsOnly("/test-adserver-context-data");
         }
     
         @Test
    -    public void makeHttpRequestsShouldTakeGamAdSlotWhenDataAdSlotIsNotDefined() {
    +    public void makeHttpRequestsShouldPreferDataGamAdSlot() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
                     impBuilder -> impBuilder.video(Video.builder().build()),
                     identity());
     
    -        final ObjectNode adserverNode = mapper.createObjectNode();
    -        adserverNode.put("name", "gam");
    -        adserverNode.put("adslot", "/test-adserver");
    -        final ObjectNode impExtContextDataNode = mapper.createObjectNode()
    -                .set("adserver", adserverNode);
    -
    -        final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, null, impExtContextDataNode)));
    +        bidRequest.getImp().get(0).getExt()
    +                .set("context", mapper.createObjectNode()
    +                        .set("data", mapper.createObjectNode()
    +                                .put("pbadslot", "/test-pbadslot-context-data")))
    +                .set("data", mapper.createObjectNode()
    +                        .put("pbadslot", "/test-pbadslot-data")
    +                        .set("adserver", mapper.createObjectNode()
    +                                .put("name", "gam")
    +                                .put("adslot", "test-adserver-data")));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1436,28 +1865,25 @@ public void makeHttpRequestsShouldTakeGamAdSlotWhenDataAdSlotIsNotDefined() {
                     .flatExtracting(BidRequest::getImp)
                     .extracting(Imp::getExt)
                     .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class))
    -                .extracting(RubiconImpExt::getRp)
    -                .extracting(RubiconImpExtRp::getTarget)
    -                .containsOnly(mapper.createObjectNode().put("dfp_ad_unit_code", "test-adserver"));
    +                .extracting(RubiconImpExt::getGpid)
    +                .containsOnly("test-adserver-data");
         }
     
         @Test
    -    public void makeHttpRequestsShouldNotCopyAdSlotFromAdServerToRubiconImpExtRpTargetIfAdServerNameIsNotGam() {
    +    public void makeHttpRequestsShouldNotCopyAdSlotFromAdServerWhenAdServerNameIsNotGam() {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
                     impBuilder -> impBuilder.video(Video.builder().build()),
                     identity());
     
    -        final ObjectNode adserverNode = mapper.createObjectNode();
    -        adserverNode.put("name", "not-gam");
    -        adserverNode.put("adslot", "/test-adserver");
    -        final ObjectNode impExtContextDataNode = mapper.createObjectNode()
    -                .put("property", "value")
    -                .set("adserver", adserverNode);
    -
    -        final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, null, impExtContextDataNode)));
    +        bidRequest.getImp().get(0).getExt()
    +                .set("context", mapper.createObjectNode()
    +                        .set("data", mapper.createObjectNode()
    +                                .put("property", "value")
    +                                .set("adserver", mapper.createObjectNode()
    +                                        .put("name", "not-gam")
    +                                        .put("adslot", "/test-adserver"))));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1475,35 +1901,6 @@ public void makeHttpRequestsShouldNotCopyAdSlotFromAdServerToRubiconImpExtRpTarg
                             .set("property", mapper.createArrayNode().add("value")));
         }
     
    -    @Test
    -    public void makeHttpRequestsShouldCopyAdSlotFromPbadslotImpExtContextDataFieldsToRubiconImpExtRpTarget() {
    -        // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                identity(),
    -                impBuilder -> impBuilder.video(Video.builder().build()),
    -                identity());
    -
    -        final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        final ObjectNode impExtContextDataNode = mapper.createObjectNode().put("pbadslot", "/test");
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, null, impExtContextDataNode)));
    -
    -        // when
    -        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue())
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getExt)
    -                .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class))
    -                .extracting(RubiconImpExt::getRp)
    -                .extracting(RubiconImpExtRp::getTarget)
    -                .containsOnly(mapper.createObjectNode()
    -                        .set("pbadslot", mapper.createArrayNode().add("/test"))
    -                        .put("dfp_ad_unit_code", "test"));
    -    }
    -
         @Test
         public void makeHttpRequestsShouldNotCopyAndModifyImpExtContextDataAdslotToRubiconImpExtRpTargetDfpAdUnitCode() {
             // given
    @@ -1515,7 +1912,7 @@ public void makeHttpRequestsShouldNotCopyAndModifyImpExtContextDataAdslotToRubic
             final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
             final ObjectNode impExtContextDataNode = mapper.createObjectNode()
                     .set("adslot", mapper.createArrayNode().add("123"));
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, null, impExtContextDataNode)));
    +        impExt.set("context", mapper.valueToTree(ExtImpContext.of(impExtContextDataNode)));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1600,7 +1997,7 @@ public void makeHttpRequestsShouldCopySiteExtDataAndImpExtContextDataFieldsToRub
                     identity());
     
             final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, null, impExtContextDataNode)));
    +        impExt.set("context", mapper.valueToTree(ExtImpContext.of(impExtContextDataNode)));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1633,7 +2030,7 @@ public void makeHttpRequestsShouldCopyAppExtDataAndImpExtContextDataFieldsToRubi
                     identity());
     
             final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, null, impExtContextDataNode)));
    +        impExt.set("context", mapper.valueToTree(ExtImpContext.of(impExtContextDataNode)));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1651,7 +2048,7 @@ public void makeHttpRequestsShouldCopyAppExtDataAndImpExtContextDataFieldsToRubi
         }
     
         @Test
    -    public void makeHttpRequestsShouldMergeImpExtRubiconAndContextKeywordsToRubiconImpExtRpTargetKeywords()
    +    public void makeHttpRequestsShouldMergeImpExtRubiconAndContextAndDataKeywordsToRubiconImpExtRpTargetKeywords()
                 throws IOException {
     
             // given
    @@ -1661,7 +2058,11 @@ public void makeHttpRequestsShouldMergeImpExtRubiconAndContextKeywordsToRubiconI
                     identity());
     
             final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of("imp,ext,context,keywords", null, null)));
    +        final ExtImpContext extImpContext = ExtImpContext.of(null);
    +        extImpContext.addProperty("keywords", new TextNode("imp,ext,context,keywords"));
    +        impExt.set("context", mapper.valueToTree(extImpContext));
    +        impExt.set("data", mapper.createObjectNode()
    +                .put("keywords", "imp,ext,data,keywords"));
             impExt.set(
                     "bidder",
                     mapper.valueToTree(ExtImpRubicon.builder()
    @@ -1680,12 +2081,19 @@ public void makeHttpRequestsShouldMergeImpExtRubiconAndContextKeywordsToRubiconI
                     .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class))
                     .extracting(RubiconImpExt::getRp)
                     .extracting(RubiconImpExtRp::getTarget)
    -                .containsOnly(mapper.readTree(
    -                        "{\"keywords\":[\"imp\", \"ext\", \"context\", \"keywords\", \"rubicon\"]}"));
    +                .containsOnly(mapper.readTree("{\"keywords\":["
    +                        + "\"imp,ext,data,keywords\","
    +                        + " \"imp\","
    +                        + " \"ext\","
    +                        + " \"context\","
    +                        + " \"keywords\","
    +                        + " \"data\","
    +                        + " \"rubicon\""
    +                        + "]}"));
         }
     
         @Test
    -    public void makeHttpRequestsShouldCopyImpExtContextSearchToRubiconImpExtRpTargetSearch() throws IOException {
    +    public void makeHttpRequestsShouldCopyImpExtContextAndDataSearchToRubiconImpExtRpTargetSearch() throws IOException {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     identity(),
    @@ -1693,7 +2101,10 @@ public void makeHttpRequestsShouldCopyImpExtContextSearchToRubiconImpExtRpTarget
                     identity());
     
             final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, "imp ext search", null)));
    +        final ExtImpContext extImpContext = ExtImpContext.of(null);
    +        extImpContext.addProperty("search", new TextNode("imp ext context search"));
    +        impExt.set("context", mapper.valueToTree(extImpContext));
    +        impExt.set("data", mapper.createObjectNode().put("search", "imp ext data search"));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1707,7 +2118,7 @@ public void makeHttpRequestsShouldCopyImpExtContextSearchToRubiconImpExtRpTarget
                     .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class))
                     .extracting(RubiconImpExt::getRp)
                     .extracting(RubiconImpExtRp::getTarget)
    -                .containsOnly(mapper.readTree("{\"search\":[\"imp ext search\"]}"));
    +                .containsOnly(mapper.readTree("{\"search\":[\"imp ext data search\", \"imp ext context search\"]}"));
         }
     
         @Test
    @@ -1732,7 +2143,7 @@ public void makeHttpRequestsShouldCopyImpExtVideoLanguageToSiteContentLanguage()
     
         @Test
         public void makeHttpRequestsShouldMergeImpExtContextSearchAndSiteSearchAndCopyToRubiconImpExtRpTarget()
    -            throws IOException {
    +            throws JsonProcessingException {
             // given
             final BidRequest bidRequest = givenBidRequest(
                     requestBuilder -> requestBuilder.site(Site.builder().search("site search").build()),
    @@ -1740,7 +2151,9 @@ public void makeHttpRequestsShouldMergeImpExtContextSearchAndSiteSearchAndCopyTo
                     identity());
     
             final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, "imp ext search", null)));
    +        final ExtImpContext extImpContext = ExtImpContext.of(null);
    +        extImpContext.addProperty("search", new TextNode("imp ext search"));
    +        impExt.set("context", mapper.valueToTree(extImpContext));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1779,7 +2192,7 @@ public void makeHttpRequestsShouldMergeImpExtContextDataAndSiteAttributesAndCopy
                     .put("search", "imp ext search");
     
             final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, null, impExtContextData)));
    +        impExt.set("context", mapper.valueToTree(ExtImpContext.of(impExtContextData)));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1827,7 +2240,7 @@ public void makeHttpRequestsShouldMergeImpExtContextDataAndAppAttributesAndCopyT
                     .set("pagecat", mapper.createArrayNode().add("imp ext pagecat"));
     
             final ObjectNode impExt = bidRequest.getImp().get(0).getExt();
    -        impExt.set("context", mapper.valueToTree(ExtImpContext.of(null, null, impExtContextData)));
    +        impExt.set("context", mapper.valueToTree(ExtImpContext.of(impExtContextData)));
     
             // when
             final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    @@ -1897,6 +2310,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
                     .imp(asList(
                             givenImp(builder -> builder.video(Video.builder().build())),
                             Imp.builder()
    +                                .banner(Banner.builder().build())
                                     .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))
                                     .build()))
                     .build();
    @@ -1995,14 +2409,14 @@ public void makeHttpRequestsShouldProcessMetricsAndFillViewabilityVendor() {
                             Metric.builder().type("unsupported").value(0.5f).vendor("comscore").build(),
                             Metric.builder().type("viewability").value(0.6f).vendor("seller-declared").build(),
                             Metric.builder().type("unsupported").value(0.7f).vendor("somebody").build());
    +
             assertThat(result.getValue()).hasSize(1).doesNotContainNull()
                     .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
                     .flatExtracting(BidRequest::getImp).doesNotContainNull()
                     .extracting(Imp::getExt).doesNotContainNull()
                     .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class))
    -                .containsOnly(RubiconImpExt.of(RubiconImpExtRp.of(null,
    -                        null,
    -                        RubiconImpExtRpTrack.of("", "")), asList("moat.com", "doubleclickbygoogle.com")));
    +                .containsOnly(RubiconImpExt.of(RubiconImpExtRp.of(null, null,
    +                        RubiconImpExtRpTrack.of("", "")), asList("moat.com", "doubleclickbygoogle.com"), 1, null));
         }
     
         @Test
    @@ -2045,16 +2459,206 @@ public void makeHttpRequestsShouldUpdateSourceWithPchainIfDefinedInImpExt() {
         }
     
         @Test
    -    public void makeBidsShouldReturnEmptyResultIfResponseStatusIsNoContent() {
    +    public void makeBidsShouldTolerateMismatchedBidImpId() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(impBuilder -> impBuilder.id("impId")),
    +                givenBidResponse(bidBuilder -> bidBuilder.price(ONE).impid("mismatched_impId")));
    +
    +        // when
    +        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnOnlyLineItemRequestsWithExpectedFieldsWhenImpPmpDealsArePresent()
    +            throws IOException {
    +        // given
    +        final List dealsList = asList(
    +                Deal.builder().ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of(null, "123",
    +                        singletonList(Format.builder().w(120).h(600).build()), null))))
    +                        .build(),
    +                Deal.builder().ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of(null, "234",
    +                        singletonList(Format.builder().w(300).h(250).build()), null))))
    +                        .build());
    +
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder
    +                        // request for banner should be ignored, should make two requests for deals instead
    +                        .banner(Banner.builder().format(singletonList(Format.builder().w(468).h(60).build())).build())
    +                        .pmp(Pmp.builder().deals(dealsList).build()));
    +
    +        // when
    +        final Result>> result = rubiconBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +
    +        assertThat(result.getValue()).hasSize(2)
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp).hasSize(2)
    +                .extracting(Imp::getBanner)
    +                .flatExtracting(Banner::getFormat)
    +                .containsOnly(Format.builder().w(120).h(600).build(), Format.builder().w(300).h(250).build());
    +
    +        assertThat(result.getValue())
    +                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(imp -> mapper.treeToValue(imp.getExt(), RubiconImpExt.class).getRp().getTarget())
    +                .containsOnly(
    +                        mapper.readTree("{\"line_item\":\"123\"}"),
    +                        mapper.readTree("{\"line_item\":\"234\"}"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldSetSeatBuyerToMetaNetworkId() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
    +                        .cur("USD")
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
    +                                .buyer("123")
    +                                .bid(singletonList(givenBid(bid -> bid.price(ONE))))
    +                                .build()))
    +                        .build()));
    +
    +        // when
    +        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    +
    +        // then
    +        final ObjectNode expectedBidExt = mapper.valueToTree(
    +                ExtPrebid.of(ExtBidPrebid.builder()
    +                        .meta(mapper.createObjectNode().set("networkId", IntNode.valueOf(123)))
    +                        .build(), null));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getExt)
    +                .containsExactly(expectedBidExt);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldNotSetNegativeSeatBuyerToMetaNetworkId() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
    +                        .cur("USD")
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
    +                                .buyer("-123")
    +                                .bid(singletonList(givenBid(bid -> bid.price(ONE))))
    +                                .build()))
    +                        .build()));
    +
    +        // when
    +        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getExt)
    +                .containsNull();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldNotSetZeroSeatBuyerToMetaNetworkId() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
    +                        .cur("USD")
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
    +                                .buyer("0")
    +                                .bid(singletonList(givenBid(bid -> bid.price(ONE))))
    +                                .build()))
    +                        .build()));
    +
    +        // when
    +        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getExt)
    +                .containsNull();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldTolerateWithNotParsibleBuyerValue() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
    +                        .cur("USD")
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
    +                                .buyer("canNotBeParsed")
    +                                .bid(singletonList(givenBid(bid -> bid.price(ONE))))
    +                                .build()))
    +                        .build()));
    +
    +        // when
    +        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getExt)
    +                .containsNull();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldTolerateWithNotIntegerBuyerValue() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
    +                        .cur("USD")
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
    +                                .buyer("123.2")
    +                                .bid(singletonList(givenBid(bid -> bid.price(ONE))))
    +                                .build()))
    +                        .build()));
    +
    +        // when
    +        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getExt)
    +                .containsNull();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldRemoveBidsWithNotParsedExtAndAddError() throws JsonProcessingException {
             // given
    -        final HttpCall httpCall = HttpCall.success(HttpRequest.builder().build(),
    -                HttpResponse.of(204, null, null), null);
    +        final ObjectNode invalidBidExt = mapper.createObjectNode();
    +        invalidBidExt.set("prebid", mapper.createArrayNode());
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
    +                        .cur("USD")
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
    +                                .buyer("123")
    +                                .bid(singletonList(givenBid(bid -> bid.id("789").price(ONE).ext(invalidBidExt))))
    +                                .build()))
    +                        .build()));
     
             // when
             final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
     
             // then
    -        assertThat(result).isEqualTo(Result.of(Collections.emptyList(), Collections.emptyList()));
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badServerResponse("Invalid ext passed in bid with id: 789"));
    +        assertThat(result.getValue()).isEmpty();
         }
     
         @Test
    @@ -2075,8 +2679,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
         @Test
         public void makeBidsShouldReturnBannerBidIfRequestImpHasNoVideo() throws JsonProcessingException {
             // given
    -        final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()),
    -                givenBidResponse(ONE));
    +        final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()), givenBidResponse(ONE));
     
             // when
             final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    @@ -2143,45 +2746,42 @@ public void makeBidsShouldReturnVideoBidIfRequestImpHasVideo() throws JsonProces
         }
     
         @Test
    -    public void makeBidsShouldNotReturnImpIfNonDealBidPriceLessThanZero() throws JsonProcessingException {
    -        // given
    -        final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()),
    -                givenBidResponse(BigDecimal.valueOf(-1)));
    -
    -        // when
    -        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    -
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    -
    -    @Test
    -    public void makeBidsShouldNotReturnImpIfNonDealBidPriceEqualToZero() throws JsonProcessingException {
    +    public void makeBidsShouldNotReduceBidderAmountForBidsWithSameImpId() throws JsonProcessingException {
             // given
    -        final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()),
    -                givenBidResponse(ZERO));
    +        final Bid firstBid = Bid.builder()
    +                .id("firstBidId")
    +                .impid("impId")
    +                .price(ONE)
    +                .build();
     
    -        // when
    -        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    +        final Bid secondBid = Bid.builder()
    +                .id("secondBidId")
    +                .impid("impId")
    +                .price(ONE)
    +                .build();
     
    -        // then
    -        assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    -    }
    +        final String bidResponse = mapper.writeValueAsString(RubiconBidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(RubiconSeatBid.builder()
    +                        .bid(Arrays.asList(firstBid, secondBid))
    +                        .build()))
    +                .build());
     
    -    @Test
    -    public void makeBidsShouldNotReturnImpIfDealBidPriceLessThanZero() throws JsonProcessingException {
    -        // given
    -        final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()),
    -                givenBidResponse(BigDecimal.valueOf(-1)));
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(builder -> builder.video(Video.builder().build())),
    +                bidResponse);
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.id("impId").video(Video.builder().build()));
     
             // when
    -        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    +        final Result> result = rubiconBidder.makeBids(httpCall, bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getId)
    +                .containsExactlyInAnyOrder("firstBidId", "secondBidId");
         }
     
         @Test
    @@ -2222,10 +2822,11 @@ public void makeBidsShouldReturnBidWithOverriddenCpmFromImp() throws JsonProcess
                                     mapper.createObjectNode().put("cpmoverride", 5.55))))) // will be ignored
                     .build());
     
    -        final ExtImp extImp = ExtImp.of(ExtImpPrebid.builder()
    -                .bidder(mapper.valueToTree(
    -                        RubiconImpExtPrebidBidder.of(RubiconImpExtPrebidRubiconDebug.of(4.44f))))
    -                .build(), null);
    +        final ExtPrebid extImp = ExtPrebid.of(
    +                null,
    +                ExtImpRubicon.builder()
    +                        .debug(ExtImpRubiconDebug.of(4.44f))
    +                        .build());
     
             // when
             final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(
    @@ -2245,9 +2846,9 @@ public void makeBidsShouldReturnBidWithOverriddenCpmFromImp() throws JsonProcess
         public void makeBidsShouldReturnBidWithBidIdFieldFromBidResponseIfZero() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()),
    -                mapper.writeValueAsString(BidResponse.builder()
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
                             .bidid("bidid1") // returned bidid from XAPI
    -                        .seatbid(singletonList(SeatBid.builder()
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
                                     .bid(singletonList(Bid.builder().id("0").price(ONE).build()))
                                     .build()))
                             .build()));
    @@ -2258,16 +2859,16 @@ public void makeBidsShouldReturnBidWithBidIdFieldFromBidResponseIfZero() throws
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().id("bidid1").price(ONE).build(), banner, "USD"));
    +                .containsOnly(BidderBid.of(Bid.builder().id("bidid1").price(ONE).build(), banner, null));
         }
     
         @Test
         public void makeBidsShouldReturnBidWithOriginalBidIdFieldFromBidResponseIfNotZero() throws JsonProcessingException {
             // given
             final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()),
    -                mapper.writeValueAsString(BidResponse.builder()
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
                             .bidid("bidid1") // returned bidid from XAPI
    -                        .seatbid(singletonList(SeatBid.builder()
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
                                     .bid(singletonList(Bid.builder().id("non-zero").price(ONE).build()))
                                     .build()))
                             .build()));
    @@ -2278,18 +2879,18 @@ public void makeBidsShouldReturnBidWithOriginalBidIdFieldFromBidResponseIfNotZer
             // then
             assertThat(result.getErrors()).isEmpty();
             assertThat(result.getValue())
    -                .containsOnly(BidderBid.of(Bid.builder().id("non-zero").price(ONE).build(), banner, "USD"));
    +                .containsOnly(BidderBid.of(Bid.builder().id("non-zero").price(ONE).build(), banner, null));
         }
     
         @Test
         public void makeBidsShouldReturnBidWithRandomlyGeneratedId() throws JsonProcessingException {
             // given
             rubiconBidder = new RubiconBidder(
    -                ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, jacksonMapper);
    +                ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, currencyConversionService, jacksonMapper);
     
             final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()),
    -                mapper.writeValueAsString(BidResponse.builder()
    -                        .seatbid(singletonList(SeatBid.builder()
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
                                     .bid(singletonList(Bid.builder().id("bidid1").price(ONE).build()))
                                     .build()))
                             .build()));
    @@ -2306,13 +2907,64 @@ public void makeBidsShouldReturnBidWithRandomlyGeneratedId() throws JsonProcessi
                     .doesNotContain("bidid1");
         }
     
    +    @Test
    +    public void makeBidsShouldReturnBidWithCurrencyFromBidResponse() throws JsonProcessingException {
    +        // given
    +        rubiconBidder = new RubiconBidder(
    +                ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, currencyConversionService, jacksonMapper);
    +
    +        final HttpCall httpCall = givenHttpCall(givenBidRequest(identity()),
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
    +                        .cur("EUR")
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
    +                                .bid(singletonList(Bid.builder().id("bidid1").price(ONE).build()))
    +                                .build()))
    +                        .build()));
    +
    +        // when
    +        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBidCurrency)
    +                .containsOnly("EUR");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBidWithDchainFromRequest() throws JsonProcessingException {
    +        // given
    +        final ObjectNode requestNode = mapper.valueToTree(ExtBidPrebid.builder()
    +                .meta(mapper.createObjectNode().set("dChain", TextNode.valueOf("dChain")))
    +                .build());
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(identity()),
    +                mapper.writeValueAsString(RubiconBidResponse.builder()
    +                        .seatbid(singletonList(RubiconSeatBid.builder()
    +                                .bid(singletonList(givenBid(bid -> bid.ext(requestNode).price(ONE))))
    +                                .build()))
    +                        .build()));
    +
    +        // when
    +        final Result> result = rubiconBidder.makeBids(httpCall, givenBidRequest(identity()));
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getExt)
    +                .containsExactly(requestNode);
    +    }
    +
         @Test
         public void extractTargetingShouldReturnEmptyMapForEmptyExtension() {
    +        // when and then
             assertThat(rubiconBidder.extractTargeting(mapper.createObjectNode())).isEmpty();
         }
     
         @Test
         public void extractTargetingShouldReturnEmptyMapForInvalidExtension() {
    +        // when and then
             assertThat(rubiconBidder.extractTargeting(mapper.createObjectNode().put("rp", 1))).isEmpty();
             assertThat(rubiconBidder.extractTargeting(mapper.createObjectNode().putObject("rp").put("targeting", 1)))
                     .isEmpty();
    @@ -2320,11 +2972,13 @@ public void extractTargetingShouldReturnEmptyMapForInvalidExtension() {
     
         @Test
         public void extractTargetingShouldReturnEmptyMapForNullRp() {
    +        // when and then
             assertThat(rubiconBidder.extractTargeting(mapper.createObjectNode().putObject("rp"))).isEmpty();
         }
     
         @Test
         public void extractTargetingShouldReturnEmptyMapForNullTargeting() {
    +        // when and then
             assertThat(rubiconBidder.extractTargeting(mapper.createObjectNode().putObject("rp").putObject("targeting")))
                     .isEmpty();
         }
    @@ -2354,7 +3008,7 @@ private static BidRequest givenBidRequest(Function impCustomizer,
                                                   Function extCustomizer) {
             return bidRequestCustomizer.apply(BidRequest.builder()
    -                .imp(singletonList(givenImp(impCustomizer, extCustomizer))))
    +                        .imp(singletonList(givenImp(impCustomizer, extCustomizer))))
                     .build();
         }
     
    @@ -2370,7 +3024,8 @@ private static BidRequest givenBidRequest(Function impCu
         private static Imp givenImp(Function impCustomizer,
                                     Function extCustomizer) {
             return impCustomizer.apply(Imp.builder()
    -                .ext(mapper.valueToTree(ExtPrebid.of(null, extCustomizer.apply(ExtImpRubicon.builder()).build()))))
    +                        .ext(mapper.valueToTree(ExtPrebid.of(
    +                                null, extCustomizer.apply(ExtImpRubicon.builder()).build()))))
                     .build();
         }
     
    @@ -2378,6 +3033,13 @@ private static Imp givenImp(Function impCustomizer) {
             return givenImp(impCustomizer, identity());
         }
     
    +    private static Data givenDataWithSegmentEntry(Integer segtax, String segmentId) {
    +        return Data.builder()
    +                .segment(singletonList(Segment.builder().id(segmentId).build()))
    +                .ext(mapper.createObjectNode().put("segtax", segtax))
    +                .build();
    +    }
    +
         private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
             return HttpCall.success(
                     HttpRequest.builder().payload(bidRequest).build(),
    @@ -2385,16 +3047,24 @@ private static HttpCall givenHttpCall(BidRequest bidRequest, String
                     null);
         }
     
    -    private static String givenBidResponse(BigDecimal price) throws JsonProcessingException {
    -        return mapper.writeValueAsString(BidResponse.builder()
    -                .seatbid(singletonList(SeatBid.builder()
    -                        .bid(singletonList(Bid.builder()
    -                                .price(price)
    -                                .build()))
    +    private static String givenBidResponse(Function bidCustomizer)
    +            throws JsonProcessingException {
    +        return mapper.writeValueAsString(RubiconBidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(RubiconSeatBid.builder()
    +                        .bid(singletonList(givenBid(bidCustomizer)))
                             .build()))
                     .build());
         }
     
    +    private static String givenBidResponse(BigDecimal price) throws JsonProcessingException {
    +        return givenBidResponse(bidBuilder -> bidBuilder.price(price));
    +    }
    +
    +    private static Bid givenBid(Function bidCustomizer) {
    +        return bidCustomizer.apply(Bid.builder()).build();
    +    }
    +
         @AllArgsConstructor(staticName = "of")
         @Value
         private static class Inventory {
    diff --git a/src/test/java/org/prebid/server/bidder/salunamedia/SaLunamediaBidderTest.java b/src/test/java/org/prebid/server/bidder/salunamedia/SaLunamediaBidderTest.java
    new file mode 100644
    index 00000000000..f8f9fdfb605
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/salunamedia/SaLunamediaBidderTest.java
    @@ -0,0 +1,255 @@
    +package org.prebid.server.bidder.salunamedia;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +
    +import java.util.List;
    +import java.util.function.Function;
    +import java.util.stream.Collectors;
    +
    +import static java.util.Arrays.asList;
    +import static java.util.Collections.singletonList;
    +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.banner;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
    +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
    +
    +public class SaLunamediaBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "https://randomurl.com";
    +
    +    private SaLunamediaBidder saLunamediaBidder;
    +
    +    @Before
    +    public void setUp() {
    +        saLunamediaBidder = new SaLunamediaBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(
    +                () -> new SaLunamediaBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnOnlyOneRequestForAllImps() {
    +        // given
    +        final BidRequest bidRequest = BidRequest.builder()
    +                .imp(asList(
    +                        Imp.builder().id("123").banner(Banner.builder().build()).build(),
    +                        Imp.builder().id("456").video(Video.builder().build()).build()))
    +                .build();
    +
    +        // when
    +        final Result>> requests = saLunamediaBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(requests.getErrors()).isEmpty();
    +        assertThat(requests.getValue()).hasSize(1);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = saLunamediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
    +                });
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = saLunamediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty SeatBid"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidResponseSeatBidIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = saLunamediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty SeatBid"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidResponseFirstSeatBidHasEmptyBid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().build()))
    +                        .build(),
    +                mapper.writeValueAsString(BidResponse.builder().seatbid(singletonList(null)).build()));
    +
    +        // when
    +        final Result> result = saLunamediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty SeatBid.Bids"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidResponseFirstBidOfFirstSeatBidHasEmptyExt()
    +            throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = saLunamediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Missing BidExt"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidResponseFirstBidHasInvalidMediaType() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponses(singletonList(bidBuilder -> bidBuilder.impid("123").ext(
    +                        jacksonMapper.mapper().createObjectNode().put("mediaType", "invalid_type "))))));
    +
    +        // when
    +        final Result> result = saLunamediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(error -> {
    +                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
    +                    assertThat(error.getMessage()).startsWith("Cannot deserialize value of type");
    +                });
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfExtMediatypeIsNative() throws JsonProcessingException {
    +        // given
    +        final ObjectNode mediaTypeObjectNode = jacksonMapper.mapper()
    +                .createObjectNode().put("mediaType", "native");
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponses(asList(
    +                        bidBuilder -> bidBuilder.impid("123").ext(mediaTypeObjectNode),
    +                        bidBuilder -> bidBuilder.impid("345").ext(mediaTypeObjectNode)))));
    +
    +        // when
    +        final Result> result = saLunamediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).containsExactly(BidderBid.of(
    +                Bid.builder().impid("123").ext(mediaTypeObjectNode).build(), xNative, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfExtMediatypeIsBanner() throws JsonProcessingException {
    +        // given
    +        final ObjectNode mediaTypeObjectNode = jacksonMapper.mapper()
    +                .createObjectNode().put("mediaType", "banner");
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponses(asList(
    +                        bidBuilder -> bidBuilder.impid("123").ext(mediaTypeObjectNode),
    +                        bidBuilder -> bidBuilder.impid("345").ext(mediaTypeObjectNode)))));
    +
    +        // when
    +        final Result> result = saLunamediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).containsExactly(BidderBid.of(
    +                Bid.builder().impid("123").ext(mediaTypeObjectNode).build(), banner, null));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfExtMediatypeIsVideo() throws JsonProcessingException {
    +        // given
    +        final ObjectNode mediaTypeObjectNode = jacksonMapper.mapper()
    +                .createObjectNode().put("mediaType", "video");
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().build()))
    +                        .build(),
    +                mapper.writeValueAsString(givenBidResponses(asList(
    +                        bidBuilder -> bidBuilder.impid("123").ext(mediaTypeObjectNode),
    +                        bidBuilder -> bidBuilder.impid("345").ext(mediaTypeObjectNode)))));
    +
    +        // when
    +        final Result> result = saLunamediaBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).containsExactly(BidderBid.of(
    +                Bid.builder().impid("123").ext(mediaTypeObjectNode).build(), video, null));
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponses(List> bidCustomizers) {
    +        return BidResponse.builder()
    +                .seatbid(bidCustomizers.stream().map(customizer -> SeatBid.builder()
    +                        .bid(singletonList(customizer.apply(Bid.builder()).build()))
    +                        .build())
    +                        .collect(Collectors.toList()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughBidderTest.java b/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughBidderTest.java
    index bd905f46a5f..9ba342050f3 100644
    --- a/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughBidderTest.java
    @@ -28,6 +28,7 @@
     import org.prebid.server.proto.openrtb.ext.request.ExtUser;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEid;
     import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid;
    +import org.prebid.server.proto.openrtb.ext.request.sharethrough.ExtData;
     import org.prebid.server.proto.openrtb.ext.request.sharethrough.ExtImpSharethrough;
     import org.prebid.server.proto.openrtb.ext.response.BidType;
     import org.prebid.server.util.HttpUtil;
    @@ -43,7 +44,6 @@
     import java.util.Map;
     import java.util.concurrent.TimeUnit;
     
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    @@ -128,7 +128,8 @@ public void makeHttpRequestsShouldReturnRequestWithCorrectUriAndHeaders() throws
                     .imp(singletonList(Imp.builder()
                             .id("abc")
                             .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                ExtImpSharethrough.of("pkey", false, Arrays.asList(10, 20), BigDecimal.ONE))))
    +                                ExtImpSharethrough.of("pkey", false, Arrays.asList(10, 20),
    +                                        BigDecimal.ONE, ExtData.of("pbAdSlot")))))
                             .banner(Banner.builder().w(40).h(30).build())
                             .build()))
                     .app(App.builder().ext(ExtApp.of(null, null)).build())
    @@ -146,7 +147,8 @@ public void makeHttpRequestsShouldReturnRequestWithCorrectUriAndHeaders() throws
             // then
             final String expectedParameters = "?placement_key=pkey&bidId=abc&consent_required=false&consent_string="
                     + "&us_privacy=&instant_play_capable=true&stayInIframe=false&height=10&width=20"
    -                + "&adRequestAt=" + URLENCODED_TEST_FORMATTED_TIME + "&supplyId=FGMrCMMc&strVersion=8";
    +                + "&adRequestAt=" + URLENCODED_TEST_FORMATTED_TIME + "&supplyId=FGMrCMMc&strVersion=8"
    +                + "&gpid=pbAdSlot";
             final SharethroughRequestBody expectedPayload = SharethroughRequestBody.of(singletonList("testBlocked"), 2000L,
                     DEADLINE_FORMATTED_TIME, true, BigDecimal.ONE);
     
    @@ -182,8 +184,8 @@ public void makeHttpRequestsShouldReturnRequestWithCorrectUriAndHeadersDefaultPa
                     .build();
             final BidRequest bidRequest = BidRequest.builder()
                     .imp(singletonList(Imp.builder()
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                ExtImpSharethrough.of("pkey", false, null, null))))
    +                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSharethrough.of(
    +                                "pkey", false, null, null, null))))
                             .build()))
                     .site(Site.builder().page("http://page.com").build())
                     .device(Device.builder().build())
    @@ -300,11 +302,6 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             assertThat(result.getValue()).isEmpty();
         }
     
    -    @Test
    -    public void extractTargetingShouldReturnEmptyMap() {
    -        assertThat(sharethroughBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap());
    -    }
    -
         private static HttpCall givenHttpCall(SharethroughRequestBody bidRequest, String body) {
             return HttpCall.success(
                     HttpRequest.builder().payload(bidRequest).build(),
    diff --git a/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtilTest.java b/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtilTest.java
    index 8c4d80347ff..7bc99c65344 100644
    --- a/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtilTest.java
    +++ b/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughRequestUtilTest.java
    @@ -286,7 +286,8 @@ public void getSizeShouldReturnDefaultSize1x1WhenExtImpSizeIsEmptyAndImpIsNotBan
             final Imp notBannerImp = Imp.builder().build();
             final Banner banner = Banner.builder().format(Collections.emptyList()).build();
             final Imp bannerImpEmptyFormat = Imp.builder().banner(banner).build();
    -        final ExtImpSharethrough extImpSharethrough = ExtImpSharethrough.of(null, null, Collections.emptyList(), null);
    +        final ExtImpSharethrough extImpSharethrough = ExtImpSharethrough.of(
    +                null, null, Collections.emptyList(), null, null);
     
             // when and then
             final Size expected = Size.of(1, 1);
    @@ -301,7 +302,8 @@ public void getSizeShouldReturnExtImpSizeWhenExtImpSizeIsNotEmptyAndNotContainsZ
             final List formats = Collections.singletonList(Format.builder().w(200).h(200).build());
             final Banner banner = Banner.builder().format(formats).build();
             final Imp imp = Imp.builder().banner(banner).build();
    -        final ExtImpSharethrough extImpSharethrough = ExtImpSharethrough.of(null, null, Arrays.asList(100, 100), null);
    +        final ExtImpSharethrough extImpSharethrough = ExtImpSharethrough.of(
    +                null, null, Arrays.asList(100, 100), null, null);
     
             // when and then
             final Size expected = Size.of(100, 100);
    @@ -318,7 +320,8 @@ public void getSizeShouldReturnBiggestSizeFromFormatsWhenExtImpSizeIsEmptyAndImp
             final List formats = Arrays.asList(firstFormat, secondFormat, thirdFormat);
             final Banner banner = Banner.builder().format(formats).build();
             final Imp imp = Imp.builder().banner(banner).build();
    -        final ExtImpSharethrough extImpSharethrough = ExtImpSharethrough.of(null, null, Collections.emptyList(), null);
    +        final ExtImpSharethrough extImpSharethrough = ExtImpSharethrough.of(
    +                null, null, Collections.emptyList(), null, null);
     
             // when and then
             final Size expected = Size.of(320, 300);
    diff --git a/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughUriBuilderUtilTest.java b/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughUriBuilderUtilTest.java
    index 39167dfb17e..f77d7811ac8 100644
    --- a/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughUriBuilderUtilTest.java
    +++ b/src/test/java/org/prebid/server/bidder/sharethrough/SharethroughUriBuilderUtilTest.java
    @@ -8,6 +8,16 @@
     
     public class SharethroughUriBuilderUtilTest {
     
    +    @Test
    +    public void buildSharethroughUrlParametersShouldThrowIllegalArgumentExceptionWhenEndpointUrlComposingFails() {
    +        // given
    +        final String uri = "http://invalid domain.com";
    +
    +        // when and then
    +        assertThatExceptionOfType(IllegalArgumentException.class)
    +                .isThrownBy(() -> SharethroughUriBuilderUtil.buildSharethroughUrlParameters(uri));
    +    }
    +
         @Test
         public void buildSharethroughUrlParametersShouldThrowIllegalArgumentExceptionWhenHeightIsNotNumberOrNotPresent() {
             // given
    @@ -25,7 +35,7 @@ public void buildSharethroughUrlParametersShouldThrowIllegalArgumentExceptionWhe
         }
     
         @Test
    -    public void buildSharethroughUrlParametersShouldThrowIllegalArgumentExceptionWhenWidthtIsNotNumberOrNotPresent() {
    +    public void buildSharethroughUrlParametersShouldThrowIllegalArgumentExceptionWhenWidthIsNotNumberOrNotPresent() {
             // given
             final String uriNull = "http://uri.com?placement_key=pkey&bidId=bidid&height=30&width=null";
             final String uriNotNumber = "http://uri.com?placement_key=pkey&bidId=bidid&height=30&width=notNumber";
    diff --git a/src/test/java/org/prebid/server/bidder/silvermob/SilvermobBidderTest.java b/src/test/java/org/prebid/server/bidder/silvermob/SilvermobBidderTest.java
    new file mode 100644
    index 00000000000..df4d82cf394
    --- /dev/null
    +++ b/src/test/java/org/prebid/server/bidder/silvermob/SilvermobBidderTest.java
    @@ -0,0 +1,303 @@
    +package org.prebid.server.bidder.silvermob;
    +
    +import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.iab.openrtb.request.Banner;
    +import com.iab.openrtb.request.BidRequest;
    +import com.iab.openrtb.request.Device;
    +import com.iab.openrtb.request.Imp;
    +import com.iab.openrtb.request.Native;
    +import com.iab.openrtb.request.Video;
    +import com.iab.openrtb.response.Bid;
    +import com.iab.openrtb.response.BidResponse;
    +import com.iab.openrtb.response.SeatBid;
    +import io.vertx.core.MultiMap;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.prebid.server.VertxTest;
    +import org.prebid.server.bidder.model.BidderBid;
    +import org.prebid.server.bidder.model.BidderError;
    +import org.prebid.server.bidder.model.HttpCall;
    +import org.prebid.server.bidder.model.HttpRequest;
    +import org.prebid.server.bidder.model.HttpResponse;
    +import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.silvermob.ExtImpSilvermob;
    +import org.prebid.server.util.HttpUtil;
    +
    +import java.util.Arrays;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import static java.util.Collections.singletonList;
    +import static java.util.function.Function.identity;
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.assertj.core.api.Assertions.tuple;
    +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;
    +
    +public class SilvermobBidderTest extends VertxTest {
    +
    +    private static final String ENDPOINT_URL = "http://{{Host}}.test/some/path/{{ZoneID}}";
    +
    +    private SilvermobBidder silvermobBidder;
    +
    +    @Before
    +    public void setUp() {
    +        silvermobBidder = new SilvermobBidder(ENDPOINT_URL, jacksonMapper);
    +    }
    +
    +    @Test
    +    public void creationShouldFailOnInvalidEndpointUrl() {
    +        assertThatIllegalArgumentException().isThrownBy(() -> new SilvermobBidder("invalid_url", jacksonMapper));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        // when
    +        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allMatch(error ->
    +                        error.getMessage().startsWith("error unmarshalling imp.ext.bidder: Cannot deserialize instance")
    +                                && error.getType() == BidderError.Type.bad_input);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfHostIsEmptyOrNull() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpSilvermob.of("testZoneId", "")))));
    +        // when
    +        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("host is a required silvermob ext.imp param"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnErrorIfZoneIdIsEmptyOrNull() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpSilvermob.of(null, "testHost")))));
    +        // when
    +        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("zoneId is a required silvermob ext.imp param"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateSingleRequestForEveryImpression() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder ->
    +                bidRequestBuilder.imp(Arrays.asList(givenImp(identity()), givenImp(identity()))), identity());
    +        // when
    +        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(2);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldReturnRequestForEveryValidImpressionAndErrorForNotValidImp() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder ->
    +                bidRequestBuilder.imp(Arrays.asList(givenImp(impBuilder ->
    +                                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                                        ExtImpSilvermob.of(null, "testHost"))))),
    +                        givenImp(identity()))), identity());
    +        // when
    +        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).hasSize(1);
    +        assertThat(result.getErrors()).hasSize(1);
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldCreateCorrectURL() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build()));
    +
    +        // when
    +        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getUri)
    +                .containsOnly("http://testHostId.test/some/path/testZoneId");
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null, "invalid");
    +
    +        // when
    +        final Result> result = silvermobBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allMatch(error -> error.getMessage().startsWith("Error unmarshalling server "
    +                        + "Response: Failed to decode: Unrecognized token")
    +                        && error.getType() == BidderError.Type.bad_server_response);
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(null));
    +
    +        // when
    +        final Result> result = silvermobBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Response in not present"));
    +        assertThat(result.getValue()).isEmpty();
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfBidResponseSeatBidIsNullOrEmpty() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(null,
    +                mapper.writeValueAsString(BidResponse.builder().build()));
    +
    +        // when
    +        final Result> result = silvermobBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty SeatBid array"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = silvermobBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = silvermobBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetAdditionalHeadersIfDeviceFieldsAreNotEmpty() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                requestBuilder -> requestBuilder
    +                        .device(Device.builder().ua("user_agent").ip("test_ip").build()),
    +                identity());
    +
    +        // when
    +        final Result>> result = silvermobBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue()).hasSize(1)
    +                .extracting(HttpRequest::getHeaders)
    +                .flatExtracting(MultiMap::entries)
    +                .extracting(Map.Entry::getKey, Map.Entry::getValue)
    +                .containsExactlyInAnyOrder(
    +                        tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), "application/json;charset=utf-8"),
    +                        tuple(HttpUtil.ACCEPT_HEADER.toString(), "application/json"),
    +                        tuple(HttpUtil.USER_AGENT_HEADER.toString(), "user_agent"),
    +                        tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"),
    +                        tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "test_ip"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                BidRequest.builder()
    +                        .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build()))
    +                        .build(),
    +                mapper.writeValueAsString(
    +                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
    +
    +        // when
    +        final Result> result = silvermobBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD"));
    +    }
    +
    +    private static BidRequest givenBidRequest(
    +            Function bidRequestCustomizer,
    +            Function impCustomizer) {
    +
    +        return bidRequestCustomizer.apply(BidRequest.builder()
    +                .imp(singletonList(givenImp(impCustomizer))))
    +                .build();
    +    }
    +
    +    private static BidRequest givenBidRequest(Function impCustomizer) {
    +        return givenBidRequest(identity(), impCustomizer);
    +    }
    +
    +    private static Imp givenImp(Function impCustomizer) {
    +        return impCustomizer.apply(Imp.builder()
    +                .id("123")
    +                .banner(Banner.builder().id("banner_id").build())
    +                .ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpSilvermob.of("testZoneId", "testHostId")))))
    +                .build();
    +    }
    +
    +    private static BidResponse givenBidResponse(Function bidCustomizer) {
    +        return BidResponse.builder()
    +                .cur("USD")
    +                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
    +                        .build()))
    +                .build();
    +    }
    +
    +    private static HttpCall givenHttpCall(BidRequest bidRequest, String body) {
    +        return HttpCall.success(
    +                HttpRequest.builder().payload(bidRequest).build(),
    +                HttpResponse.of(200, null, body),
    +                null);
    +    }
    +}
    diff --git a/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java b/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java
    index c9fd8783b5c..09efb5afd8b 100644
    --- a/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java
    +++ b/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java
    @@ -1,6 +1,8 @@
     package org.prebid.server.bidder.smaato;
     
     import com.fasterxml.jackson.core.JsonProcessingException;
    +import com.fasterxml.jackson.databind.node.TextNode;
    +import com.iab.openrtb.request.App;
     import com.iab.openrtb.request.Banner;
     import com.iab.openrtb.request.BidRequest;
     import com.iab.openrtb.request.Format;
    @@ -8,217 +10,518 @@
     import com.iab.openrtb.request.Publisher;
     import com.iab.openrtb.request.Site;
     import com.iab.openrtb.request.User;
    +import com.iab.openrtb.request.Video;
     import com.iab.openrtb.response.Bid;
     import com.iab.openrtb.response.BidResponse;
     import com.iab.openrtb.response.SeatBid;
     import io.vertx.core.MultiMap;
     import org.junit.Before;
    +import org.junit.Rule;
     import org.junit.Test;
    +import org.mockito.Mock;
    +import org.mockito.junit.MockitoJUnit;
    +import org.mockito.junit.MockitoRule;
     import org.prebid.server.VertxTest;
    +import org.prebid.server.auction.model.Endpoint;
     import org.prebid.server.bidder.model.BidderBid;
     import org.prebid.server.bidder.model.BidderError;
     import org.prebid.server.bidder.model.HttpCall;
     import org.prebid.server.bidder.model.HttpRequest;
     import org.prebid.server.bidder.model.HttpResponse;
     import org.prebid.server.bidder.model.Result;
    +import org.prebid.server.bidder.smaato.proto.SmaatoBidExt;
    +import org.prebid.server.bidder.smaato.proto.SmaatoBidRequestExt;
     import org.prebid.server.bidder.smaato.proto.SmaatoSiteExtData;
     import org.prebid.server.bidder.smaato.proto.SmaatoUserExtData;
     import org.prebid.server.proto.openrtb.ext.ExtPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
    +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidPbs;
     import org.prebid.server.proto.openrtb.ext.request.ExtSite;
     import org.prebid.server.proto.openrtb.ext.request.ExtUser;
     import org.prebid.server.proto.openrtb.ext.request.smaato.ExtImpSmaato;
    +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
    +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo;
    +import org.prebid.server.util.HttpUtil;
     
    +import java.time.Clock;
    +import java.util.Arrays;
     import java.util.List;
    +import java.util.function.BiFunction;
     import java.util.function.Function;
    +import java.util.function.UnaryOperator;
    +import java.util.stream.Collectors;
     
    -import static java.util.Arrays.asList;
    -import static java.util.Collections.emptyMap;
     import static java.util.Collections.singletonList;
    -import static java.util.function.Function.identity;
    +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.assertj.core.api.Assertions.tuple;
    +import static org.mockito.Mockito.when;
     import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
     import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
     
     public class SmaatoBidderTest extends VertxTest {
     
    +    @Rule
    +    public final MockitoRule mockitoRule = MockitoJUnit.rule();
    +
         private static final String ENDPOINT_URL = "https://test.endpoint.com";
     
         private SmaatoBidder smaatoBidder;
     
    +    @Mock
    +    private Clock clock;
    +
         @Before
         public void setUp() {
    -        smaatoBidder = new SmaatoBidder(ENDPOINT_URL, jacksonMapper);
    +        smaatoBidder = new SmaatoBidder(ENDPOINT_URL, jacksonMapper, clock);
         }
     
         @Test
         public void creationShouldFailOnInvalidEndpointUrl() {
    -        assertThatIllegalArgumentException().isThrownBy(() -> new SmaatoBidder("invalid_url", jacksonMapper));
    +        assertThatIllegalArgumentException().isThrownBy(() -> new SmaatoBidder("invalid_url", jacksonMapper, clock));
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +    public void makeHttpRequestsShouldModifyUserIfUserExtDataIsPresent() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder
    -                        .id("123")
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder ->
    +                bidRequestBuilder.user(User.builder()
    +                        .ext(ExtUser.builder()
    +                                .data(mapper.valueToTree(SmaatoUserExtData.of("keywords", "gender", 1)))
    +                                .build())
    +                        .build()), identity());
    +
             // when
             final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance");
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getUser)
    +                .extracting(User::getKeywords, User::getGender, User::getYob)
    +                .containsExactly(tuple("keywords", "gender", 1));
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfBannerOrVideoImpIsEmpty() {
    +    public void makeHttpRequestsShouldModifySiteIfSiteExtDataIsPresent() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder.id("impid").banner(null).video(null));
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder ->
    +                bidRequestBuilder.site(Site.builder()
    +                        .ext(ExtSite.of(1, mapper.valueToTree(SmaatoSiteExtData.of("keywords"))))
    +                        .build()), identity());
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getKeywords, Site::getExt)
    +                .containsExactly(tuple("keywords", null));
    +    }
    +
    +    @Test
    +    public void makeHttpRequestsShouldSetExt() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest();
     
             // when
             final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError
    -                        .badInput("invalid MediaType. SMAATO only supports Banner and Video. Ignoring ImpID=impid"));
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getExt)
    +                .containsExactly(jacksonMapper.fillExtension(ExtRequest.empty(),
    +                        SmaatoBidRequestExt.of("prebid_server_0.4")));
         }
     
         @Test
    -    public void makeHttpRequestsShouldReturnErrorIfBannerSizeIsEmpty() {
    +    public void makePodHttpRequestsShouldReturnErrorsForImpsOfInvalidMediaType() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder.id("impid").banner(Banner.builder().build()));
    +        final BidRequest bidRequest = givenVideoBidRequest(identity(), impBuilder -> impBuilder.video(null));
     
             // when
             final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(
    +                BidderError.badInput("Invalid MediaType. Smaato only supports Video for AdPod."));
    +    }
    +
    +    @Test
    +    public void makePodHttpRequestsShouldCorrectlyConstructImpPodsAndRequests() {
    +        // given
    +        final BidRequest bidRequest = givenVideoBidRequest(
    +                identity(),
    +                impBuilder -> impBuilder.id("1_0"),
    +                impBuilder -> impBuilder.id("1_1"),
    +                impBuilder -> impBuilder.id("2_0"));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        final List> requests = result.getValue();
    +        assertThat(requests).hasSize(2);
    +        assertThat(requests.get(0).getPayload().getImp())
    +                .extracting(Imp::getId)
    +                .containsExactlyInAnyOrder("1_0", "1_1");
    +        assertThat(requests.get(1).getPayload().getImp())
    +                .extracting(Imp::getId)
    +                .containsExactly("2_0");
    +    }
    +
    +    @Test
    +    public void makePodHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
    +        // given
    +        final BidRequest bidRequest = givenVideoBidRequest(
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
             assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError
    -                        .badInput("No sizes provided for Banner null"));
    +                .allSatisfy(bidderError -> {
    +                    assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(bidderError.getMessage()).startsWith("Cannot deserialize instance");
    +                });
         }
     
         @Test
    -    public void makeHttpRequestsShouldNotChangeBannerWidthAndHeightIfPresent() {
    +    public void makePodHttpRequestsShouldReturnErrorIfImpExtPublisherIdIsAbsent() {
             // given
    -        final BidRequest bidRequest = givenBidRequest(
    -                impBuilder -> impBuilder
    -                        .banner(Banner.builder()
    -                                .format(singletonList(Format.builder().w(300).h(500).build()))
    -                                .w(200)
    -                                .h(150)
    -                                .build()));
    +        final BidRequest bidRequest = givenVideoBidRequest(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpSmaato.of(null, null, "adbreakId")))));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Missing publisherId property."));
    +    }
    +
    +    @Test
    +    public void makePodHttpRequestsShouldReturnErrorIfImpExtAdBreakIdIsAbsent() {
    +        // given
    +        final BidRequest bidRequest = givenVideoBidRequest(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSmaato.of(
    +                        "publisherId", null, null)))));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Missing adbreakId property."));
    +    }
    +
    +    @Test
    +    public void makePodHttpRequestsShouldEnrichSiteWithPublisherIdIfSiteIsPresentInRequest() {
    +        // given
    +        final BidRequest bidRequest = givenVideoBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.site(Site.builder().build()).app(null),
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpSmaato.of("publisherId", null, "adBreakId")))));
     
             // when
             final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getSite)
    +                .extracting(Site::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsExactly("publisherId");
    +    }
    +
    +    @Test
    +    public void makePodHttpRequestsShouldEnrichAppWithPublisherIdIfSiteIsAbsentAndAppIsPresentInRequest() {
    +        // given
    +        final BidRequest bidRequest = givenVideoBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.site(null).app(App.builder().build()),
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpSmaato.of("publisherId", null, "adBreakId")))));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getApp)
    +                .extracting(App::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsExactly("publisherId");
    +    }
    +
    +    @Test
    +    public void makePodHttpRequestsShouldReturnErrorIfSiteAndAppAreAbsentInRequest() {
    +        // given
    +        final BidRequest bidRequest = givenVideoBidRequest(bidRequestBuilder ->
    +                bidRequestBuilder.site(null).app(null), identity());
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Missing Site/App."));
    +    }
    +
    +    @Test
    +    public void makePodHttpRequestsShouldCorrectlyModifyImps() {
    +        // given
    +        final BidRequest bidRequest = givenVideoBidRequest(
    +                identity(),
    +                impBuilder -> impBuilder.id("1_0"),
    +                impBuilder -> impBuilder.id("1_1"));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        BiFunction resultCustomizer =
    +                (builder, idx) -> builder
    +                        .id(String.format("1_%d", idx))
    +                        .tagid("adbreakId")
    +                        .ext(null)
    +                        .video(Video.builder()
    +                                .ext(mapper.createObjectNode().set("context", TextNode.valueOf("adpod")))
    +                                .sequence(idx + 1)
    +                                .build());
    +
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .extracting(Banner::getW, Banner::getH)
    -                .containsOnly(tuple(200, 150));
    +                .containsExactlyInAnyOrder(
    +                        givenVideoImp(impBuilder -> resultCustomizer.apply(impBuilder, 0)),
    +                        givenVideoImp(impBuilder -> resultCustomizer.apply(impBuilder, 1)));
    +    }
    +
    +    @Test
    +    public void makeIndividualHttpRequestsShouldReturnErrorsOfImpsWithInvalidMediaTypes() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.video(null).banner(null));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Invalid MediaType. Smaato only supports Banner and Video."));
         }
     
         @Test
    -    public void makeHttpRequestsShouldSetBannerWidthAndHeightFromFirstFormatIfEmpty() {
    +    public void makeIndividualHttpRequestsShouldCorrectlySplitImps() {
             // given
             final BidRequest bidRequest = givenBidRequest(
    +                identity(),
                     impBuilder -> impBuilder
    -                        .banner(Banner.builder()
    -                                .format(asList(Format.builder().w(300).h(500).build(),
    -                                        Format.builder().w(450).h(150).build()))
    -                                .build()));
    +                        .id("123")
    +                        .banner(Banner.builder().w(1).h(1).build())
    +                        .video(Video.builder().w(1).h(1).build()),
    +                impBuilder -> impBuilder.id("456").banner(Banner.builder().w(1).h(1).build()));
     
             // when
             final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +        assertThat(result.getValue()).hasSize(3)
    +                .extracting(HttpRequest::getPayload)
                     .flatExtracting(BidRequest::getImp)
    -                .extracting(Imp::getBanner)
    -                .extracting(Banner::getW, Banner::getH)
    -                .containsOnly(tuple(300, 500));
    +                .allMatch(imp -> Boolean.logicalXor(imp.getVideo() != null, imp.getBanner() != null));
    +
         }
     
         @Test
    -    public void makeHttpRequestsShouldModifyRequestSite() {
    +    public void makeIndividualHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder()
    -                        .id("123")
    -                        .banner(Banner.builder()
    -                                .id("banner_id").format(asList(Format.builder().w(300).h(500).build(),
    -                                        Format.builder().w(450).h(150).build())).build())
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                ExtImpSmaato.of("publisherId", "adspaceId")))).build()))
    -                .site(Site.builder()
    -                        .ext(ExtSite.of(null, mapper.valueToTree(SmaatoSiteExtData.of("keywords"))))
    -                        .build())
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(bidderError -> {
    +                    assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(bidderError.getMessage()).startsWith("Cannot deserialize instance");
    +                });
    +    }
    +
    +    @Test
    +    public void makeIndividualHttpRequestsShouldReturnErrorIfImpExtPublisherIdIsAbsent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpSmaato.of(null, "adspaceId", null)))));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Missing publisherId property."));
    +    }
    +
    +    @Test
    +    public void makeIndividualHttpRequestsShouldReturnErrorIfImpExtAdSpaceIdIsAbsent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSmaato.of(
    +                        "publisherId", null, null)))));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Missing adspaceId property."));
    +    }
    +
    +    @Test
    +    public void makeIndividualHttpRequestsShouldEnrichSiteWithPublisherIdIfSiteIsPresentInRequest() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.site(Site.builder().build()).app(null),
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpSmaato.of("publisherId", "adspaceId", null)))));
     
             // when
             final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
                     .extracting(BidRequest::getSite)
                     .extracting(Site::getPublisher)
                     .extracting(Publisher::getId)
    -                .containsOnly("publisherId");
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .extracting(BidRequest::getSite)
    -                .extracting(Site::getKeywords)
    -                .containsOnly("keywords");
    +                .containsExactly("publisherId");
         }
     
         @Test
    -    public void makeHttpRequestsShouldModifyRequestUser() {
    +    public void makeIndividualHttpRequestsShouldEnrichAppWithPublisherIdIfSiteIsAbsentAndAppIsPresentInRequest() {
             // given
    -        final BidRequest bidRequest = BidRequest.builder()
    -                .imp(singletonList(Imp.builder()
    -                        .id("123")
    -                        .banner(Banner.builder()
    -                                .id("banner_id").format(asList(Format.builder().w(300).h(500)
    -                                                .build(),
    -                                        Format.builder().w(450).h(150).build())).build())
    -                        .ext(mapper.valueToTree(ExtPrebid.of(null,
    -                                ExtImpSmaato.of("publisherId", "adspaceId")))).build()))
    -                .user(User.builder()
    -                        .ext(ExtUser.builder()
    -                                .data(mapper.valueToTree(SmaatoUserExtData.of("keywords", "gender", 1)))
    -                                .build())
    -                        .build())
    -                .build();
    +        final BidRequest bidRequest = givenBidRequest(
    +                bidRequestBuilder -> bidRequestBuilder.site(null).app(App.builder().build()),
    +                impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpSmaato.of("publisherId", "adspaceId", null)))));
     
             // when
             final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
     
             // then
             assertThat(result.getErrors()).isEmpty();
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .extracting(BidRequest::getUser)
    -                .extracting(User::getExt)
    -                .containsOnly(ExtUser.builder().build());
    -        assertThat(result.getValue()).hasSize(1)
    -                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
    -                .extracting(BidRequest::getUser)
    -                .extracting(User::getGender)
    -                .containsOnly("gender");
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .extracting(BidRequest::getApp)
    +                .extracting(App::getPublisher)
    +                .extracting(Publisher::getId)
    +                .containsExactly("publisherId");
    +    }
    +
    +    @Test
    +    public void makeIndividualHttpRequestsShouldReturnErrorIfSiteAndAppAreAbsentInRequest() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder ->
    +                bidRequestBuilder.site(null).app(null), identity());
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors())
    +                .containsExactly(BidderError.badInput("Missing Site/App."));
    +    }
    +
    +    @Test
    +    public void makeIndividualHttpRequestsShouldReturnErrorIfBannerSizesAndFormatsAreAbsent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build()));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("No sizes provided for Banner."));
    +    }
    +
    +    @Test
    +    public void makeIndividualHttpRequestsShouldNotModifyBannerIfBannerSizesArePresent() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder ->
    +                impBuilder.banner(Banner.builder().w(1).h(1).build()));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .containsExactly(Banner.builder().w(1).h(1).build());
    +    }
    +
    +    @Test
    +    public void makeIndividualHttpRequestsShouldReplaceBannerSizesWithFirstFormatIfFormatsArePresent() {
    +        // given
    +        final Banner banner = Banner.builder().format(singletonList(Format.builder().w(2).h(2).build())).build();
    +        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(banner));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getBanner)
    +                .containsExactly(banner.toBuilder().w(2).h(2).build());
    +    }
    +
    +    @Test
    +    public void makeIndividualHttpRequestsShouldSetImpTagIdAndRemoveImpExt() {
    +        // given
    +        final BidRequest bidRequest = givenBidRequest(impBuilder ->
    +                impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null,
    +                        ExtImpSmaato.of("publisherId", "adspaceId", null)))));
    +
    +        // when
    +        final Result>> result = smaatoBidder.makeHttpRequests(bidRequest);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(HttpRequest::getPayload)
    +                .flatExtracting(BidRequest::getImp)
    +                .extracting(Imp::getTagid, Imp::getExt)
    +                .containsExactly(tuple("adspaceId", null));
         }
     
         @Test
    @@ -230,17 +533,18 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
             final Result> result = smaatoBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1);
    -        assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token");
    -        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response);
             assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).hasSize(1)
    +                .allSatisfy(bidderError -> {
    +                    assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
    +                    assertThat(bidderError.getMessage()).startsWith("Failed to decode:");
    +                });
         }
     
         @Test
         public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
             // given
    -        final HttpCall httpCall = givenHttpCall(null,
    -                mapper.writeValueAsString(null), null);
    +        final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null), null);
     
             // when
             final Result> result = smaatoBidder.makeBids(httpCall, null);
    @@ -250,89 +554,234 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces
             assertThat(result.getValue()).isEmpty();
         }
     
    +    @Test
    +    public void makeBidsShouldReturnErrorOnEmptyBidAdm() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.id("test"))),
    +                HttpUtil.headers());
    +
    +        // when
    +        final Result> result = smaatoBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Empty ad markup in bid with id: test"));
    +    }
    +
         @Test
         public void makeBidsShouldReturnErrorIfNotSupportedMarkupType() throws JsonProcessingException {
             // given
    -        final MultiMap headers = MultiMap.caseInsensitiveMultiMap().set("X-SMT-ADTYPE", "anyType");
    -        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().id("123").build()))
    -                        .build(),
    -                mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))), headers);
    +        final MultiMap headers = MultiMap.caseInsensitiveMultiMap().set("X-Smt-Adtype", "anyType");
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm("> result = smaatoBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Unknown markup type anyType"));
             assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Invalid markupType anyType"));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldCalculateTtlIfExpirationHeaderIsPresentInResponse() throws JsonProcessingException {
    +        // given
    +        when(clock.millis()).thenReturn(100L);
    +
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm("> result = smaatoBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getExp)
    +                .containsExactly(9);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldSetTtlToZeroIfExpirationHeaderIsPresentInResponseButLessThanCurrentTime()
    +            throws JsonProcessingException {
    +        // given
    +        when(clock.millis()).thenReturn(999999L);
    +
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm("> result = smaatoBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getExp)
    +                .containsExactly(0);
    +    }
    +
    +    @Test
    +    public void makeBidsShouldSetDefaultTtlIfExpirationHeaderIsAbsentInResponse() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm("> result = smaatoBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getErrors()).isEmpty();
    +        assertThat(result.getValue())
    +                .extracting(BidderBid::getBid)
    +                .extracting(Bid::getExp)
    +                .containsExactly(300);
         }
     
         @Test
         public void makeBidsShouldReturnErrorIfMarkupTypeIsBlank() throws JsonProcessingException {
             // given
    -        final MultiMap headers = MultiMap.caseInsensitiveMultiMap().set("X-SMT-ADTYPE", "");
    -        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().id("123").build()))
    -                        .build(),
    -                mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123").adm("adm"))), headers);
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm("adm"))),
    +                MultiMap.caseInsensitiveMultiMap().set("X-Smt-Adtype", ""));
    +
    +        // when
    +        final Result> result = smaatoBidder.makeBids(httpCall, null);
    +
    +        // then
    +        assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Invalid ad markup adm."));
    +    }
    +
    +    @Test
    +    public void makeBidsShouldReturnErrorIfAdmIsInvalid() throws JsonProcessingException {
    +        // given
    +        final HttpCall httpCall = givenHttpCall(
    +                givenBidRequest(),
    +                mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm("{\"image\": invalid"))),
    +                MultiMap.caseInsensitiveMultiMap().set("X-Smt-Adtype", ""));
     
             // when
             final Result> result = smaatoBidder.makeBids(httpCall, null);
     
             // then
    -        assertThat(result.getErrors()).hasSize(1)
    -                .containsOnly(BidderError.badInput("Invalid ad markup adm"));
             assertThat(result.getValue()).isEmpty();
    +        assertThat(result.getErrors()).hasSize(1);
    +        assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot decode bid.adm:");
    +        assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input);
         }
     
         @Test
         public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsReachmedia() throws JsonProcessingException {
             // given
    -        final MultiMap headers = MultiMap.caseInsensitiveMultiMap().set("X-SMT-ADTYPE", "Richmedia");
    -        final HttpCall httpCall = givenHttpCall(BidRequest.builder()
    -                        .imp(singletonList(Imp.builder().id("123").build()))
    -                        .build(),
    -                mapper.writeValueAsString(
    -                        givenBidResponse(bidBuilder -> bidBuilder.impid("123").adm("{\"richmedia\":{\"mediadata\":"
    -                                + "{\"content\":\"
    hello
    \", \"w\":350,\"h\":50},\"impressiontrackers\":" - + "[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track" - + "/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\"," - + "\"//prebid-test.smaatolabs.net/track/click/2\"]}}"))), headers); + final String adm = "{\"richmedia\":{\"mediadata\":" + + "{\"content\":\"
    hello
    \", \"w\":350,\"h\":50},\"impressiontrackers\":" + + "[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track" + + "/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\"," + + "\"//prebid-test.smaatolabs.net/track/click/2\"]}}"; + + final HttpCall httpCall = givenHttpCall( + givenBidRequest(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm(adm))), + MultiMap.caseInsensitiveMultiMap().set("X-Smt-Adtype", "Richmedia")); // when final Result> result = smaatoBidder.makeBids(httpCall, null); // then + final String expectedAdm = + "
    hello
    \"\"\"\"
    "; + final Bid expectedBid = Bid.builder() .impid("123") - .adm("
    hello
    \", \"w\":350,\"h\":50},\"impressiontrackers\":" + + "[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track" + + "/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\"," + + "\"//prebid-test.smaatolabs.net/track/click/2\"]}}"; + + final HttpCall httpCall = givenHttpCall( + givenBidRequest(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm(adm))), + MultiMap.caseInsensitiveMultiMap()); + + // when + final Result> result = smaatoBidder.makeBids(httpCall, null); + + // then + final String expectedAdm = + "
    hello
    \"\"\"\"
    ") + + "net/track/imp/2\" alt=\"\" width=\"0\" height=\"0\"/>"; + + final Bid expectedBid = Bid.builder() + .impid("123") + .adm(expectedAdm) + .ext(mapper.valueToTree(ExtPrebid.of(ExtBidPrebid.builder().build(), null))) + .exp(300) .build(); + assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .containsOnly(BidderBid.of(expectedBid, banner, "USD")); + assertThat(result.getValue()).containsExactly(BidderBid.of(expectedBid, banner, "USD")); } @Test - public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsImg() throws JsonProcessingException { + public void makeBidsShouldReturnErrorIfAdMarkTypeIsReachmediaAndAdmIsEmpty() + throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + givenBidRequest(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm("{}"))), + MultiMap.caseInsensitiveMultiMap().set("X-Smt-Adtype", "Richmedia")); + + // when + final Result> result = smaatoBidder.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsExactly(BidderError.badInput("bid.adm.richmedia is empty")); + } + + @Test + public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsVideoAndAdTypeHeaderIsAbsent() + throws JsonProcessingException { // given - final MultiMap headers = MultiMap.caseInsensitiveMultiMap().set("X-SMT-ADTYPE", "Img"); - final HttpCall httpCall = givenHttpCall(BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").build())) - .build(), + final HttpCall httpCall = givenHttpCall( + givenBidRequest(), mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123").adm("{\"image\":{\"img\":{\"url\":\"" - + "//prebid-test.smaatolabs.net/img/320x50.jpg\",\"w\":350,\"h\":50,\"ctaurl\":\"" - + "//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"" - + "//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/" - + "imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"" - + "//prebid-test.smaatolabs.net/track/click/2\"]}}"))), headers); + givenBidResponse(bidBuilder -> bidBuilder.adm("> result = smaatoBidder.makeBids(httpCall, null); @@ -340,63 +789,130 @@ public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsImg() throws JsonProcess // then final Bid expectedBid = Bid.builder() .impid("123") - .adm("
    httpCall = givenHttpCall( + givenBidRequest(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm(adm))), + MultiMap.caseInsensitiveMultiMap().set("X-Smt-Adtype", "Img")); + + // when + final Result> result = smaatoBidder.makeBids(httpCall, null); + + // then + final String expectedAdm = + "
    \"\"\"\"
    ") + + "track/imp/2\" alt=\"\" width=\"0\" height=\"0\"/>
    "; + + final Bid expectedBid = Bid.builder() + .impid("123") + .adm(expectedAdm) + .ext(mapper.valueToTree(ExtPrebid.of(ExtBidPrebid.builder().build(), null))) + .exp(300) .build(); + assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .containsOnly(BidderBid.of(expectedBid, banner, "USD")); + assertThat(result.getValue()).containsExactly(BidderBid.of(expectedBid, banner, "USD")); } @Test - public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsVideo() throws JsonProcessingException { + public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsImgAndParametersAreEmpty() throws JsonProcessingException { // given - final MultiMap headers = MultiMap.caseInsensitiveMultiMap().set("X-SMT-ADTYPE", "Video"); - final HttpCall httpCall = givenHttpCall(BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").build())) - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123").adm(""))), headers); + final String adm = "{\"image\":{\"img\":{\"url\":\"" + + "//prebid-test.smaatolabs.net/img/320x50.jpg\",\"ctaurl\":\"" + + "//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"" + + "//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/" + + "imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"" + + "//prebid-test.smaatolabs.net/track/click/2\"]}}"; + + final HttpCall httpCall = givenHttpCall( + givenBidRequest(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.adm(adm))), + MultiMap.caseInsensitiveMultiMap().set("X-Smt-Adtype", "Img")); // when final Result> result = smaatoBidder.makeBids(httpCall, null); // then + final String expectedAdm = + "
    \"\"\"\"
    "; + final Bid expectedBid = Bid.builder() .impid("123") - .adm("Video") + .adm(expectedAdm) + .ext(mapper.valueToTree(ExtPrebid.of(ExtBidPrebid.builder().build(), null))) + .exp(300) .build(); + assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .containsOnly(BidderBid.of(expectedBid, video, "USD")); + assertThat(result.getValue()).containsExactly(BidderBid.of(expectedBid, banner, "USD")); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsVideo() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall(null, - mapper.writeValueAsString(BidResponse.builder().build()), null); + final HttpCall httpCall = givenHttpCall( + givenBidRequest(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> + bidBuilder + .adm("") + .cat(singletonList("Category1")) + .ext(mapper.valueToTree(SmaatoBidExt.of(100))))), + MultiMap.caseInsensitiveMultiMap().set("X-SMT-ADTYPE", "Video")); // when final Result> result = smaatoBidder.makeBids(httpCall, null); // then + final Bid expectedBid = Bid.builder() + .impid("123") + .adm("Video") + .cat(singletonList("Category1")) + .ext(mapper.valueToTree(ExtPrebid.of(ExtBidPrebid.builder() + .video(ExtBidPrebidVideo.of(100, "Category1")).build(), null))) + .exp(300) + .build(); + assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(expectedBid, video, "USD")); } @Test - public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() { + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { // given - final HttpCall httpCall = HttpCall - .success(null, HttpResponse.of(204, null, null), null); + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build()), null); // when final Result> result = smaatoBidder.makeBids(httpCall, null); @@ -406,17 +922,38 @@ public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() { assertThat(result.getValue()).isEmpty(); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(smaatoBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + private static BidRequest givenVideoBidRequest(Function impCustomizer) { + return givenVideoBidRequest(identity(), impCustomizer); } - private static BidRequest givenBidRequest( + private static BidRequest givenVideoBidRequest( Function bidRequestCustomizer, - Function impCustomizer) { + Function... impCustomizers) { + return bidRequestCustomizer.apply(BidRequest.builder() + .site(Site.builder().build()) + .app(App.builder().build()) + .imp(Arrays.stream(impCustomizers) + .map(SmaatoBidderTest::givenVideoImp) + .collect(Collectors.toList()))) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .pbs(ExtRequestPrebidPbs.of(Endpoint.openrtb2_video.value())) + .build())) + .build(); + } + + private static BidRequest givenBidRequest() { + return givenBidRequest(identity()); + } + private static BidRequest givenBidRequest( + Function bidRequestCustomizer, + Function... impCustomizers) { return bidRequestCustomizer.apply(BidRequest.builder() - .imp(singletonList(givenImp(impCustomizer)))) + .site(Site.builder().build()) + .app(App.builder().build()) + .imp(Arrays.stream(impCustomizers) + .map(SmaatoBidderTest::givenImp) + .collect(Collectors.toList()))) .build(); } @@ -424,18 +961,30 @@ private static BidRequest givenBidRequest(Function impCustomizer) { + return impCustomizer.apply(givenImp(identity()).toBuilder() + .video(Video.builder().build())) + .build(); + } + private static Imp givenImp(Function impCustomizer) { return impCustomizer.apply(Imp.builder() - .id("123") - .banner(Banner.builder().id("banner_id").build()).ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpSmaato.of("publisherId", "adspaceId"))))) + .id("123") + .banner(Banner.builder() + .id("banner_id") + .w(300) + .h(500) + .build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpSmaato.of("publisherId", "adspaceId", "adbreakId"))))) .build(); } - private static BidResponse givenBidResponse(Function bidCustomizer) { + private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { return BidResponse.builder() .cur("USD") - .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder().impid("123")).build())) .build())) .build(); } 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 babf01300f0..2f280cc67f6 100644 --- a/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/smartadserver/SmartadserverBidderTest.java @@ -4,6 +4,8 @@ import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; @@ -20,10 +22,10 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.smartadserver.ExtImpSmartadserver; +import java.util.Arrays; import java.util.List; import java.util.function.Function; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -32,7 +34,7 @@ public class SmartadserverBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + private static final String ENDPOINT_URL = "https://test.endpoint.com/path?testParam=testVal"; private SmartadserverBidder smartadserverBidder; @@ -50,25 +52,25 @@ public void creationShouldFailOnInvalidEndpointUrl() { public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) - .build())) + .imp(singletonList( + Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build())) .build(); + // when final Result>> result = smartadserverBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance"); + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("Error parsing smartadserverExt parameters")); } @Test public void makeHttpRequestsShouldCreateCorrectURL() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSmartadserver.of(1, 2, 3, 4)))) - .build())) + .imp(singletonList(givenImp(Function.identity()))) .build(); // when @@ -77,7 +79,81 @@ public void makeHttpRequestsShouldCreateCorrectURL() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue().get(0).getUri()).isEqualTo("https://test.endpoint.com/?callerId=5"); + assertThat(result.getValue().get(0).getUri()) + .isEqualTo("https://test.endpoint.com/path/api/bid?testParam=testVal&callerId=5"); + } + + @Test + public void makeHttpRequestsShouldUpdateSiteObjectIfPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(Function.identity()))) + .site(Site.builder() + .domain("www.foo.com") + .publisher(Publisher.builder().domain("foo.com").build()) + .build()) + .build(); + + // when + final Result>> result = smartadserverBidder.makeHttpRequests(bidRequest); + + // then + final Site expectedSite = Site.builder() + .domain("www.foo.com") + .publisher(Publisher.builder().domain("foo.com").id("4").build()) + .build(); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .containsExactly(expectedSite); + } + + @Test + public void makeHttpRequestsShouldCreateRequestForEveryValidImp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(Arrays.asList(givenImp(Function.identity()), + givenImp(impBuilder -> impBuilder.id("456")) + )) + .build(); + + // when + final Result>> result = smartadserverBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .flatExtracting(Imp::getId) + .containsExactly("123", "456"); + } + + @Test + public void makeHttpRequestsShouldCreateRequestForValidImpAndSaveErrorForInvalid() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(Arrays.asList(givenImp(impBuilder -> impBuilder.id("456")), + Imp.builder() + .id("invalidImp") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build() + )) + .build(); + + // when + final Result>> result = smartadserverBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("Error parsing smartadserverExt parameters")); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .flatExtracting(Imp::getId) + .containsExactly("456"); } @Test @@ -89,9 +165,9 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { final Result> result = smartadserverBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(result.getErrors()) + .allMatch(error -> error.getMessage().startsWith("Failed to decode: Unrecognized token") + && error.getType() == BidderError.Type.bad_server_response); assertThat(result.getValue()).isEmpty(); } @@ -142,6 +218,25 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessi .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR")); } + @Test + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(Function.identity()))); + + // when + final Result> result = smartadserverBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().build(), banner, "EUR")); + } + @Test public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessingException { // given @@ -161,9 +256,13 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessing .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "EUR")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(smartadserverBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + private static Imp givenImp(Function impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123")) + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSmartadserver.of(1, 2, 3, 4)))) + .build(); } private static BidResponse givenBidResponse(Function bidCustomizer) { diff --git a/src/test/java/org/prebid/server/bidder/smartrtb/SmartrtbBidderTest.java b/src/test/java/org/prebid/server/bidder/smartrtb/SmartrtbBidderTest.java index 8d34e38e070..e4ebf8d6810 100644 --- a/src/test/java/org/prebid/server/bidder/smartrtb/SmartrtbBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/smartrtb/SmartrtbBidderTest.java @@ -30,7 +30,6 @@ import java.util.function.Function; import static java.util.Arrays.asList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -120,7 +119,7 @@ public void makeHttpRequestsShouldSetExpectedRequestUrlAndDefaultHeaders() { .containsOnly("https://test.endpoint.com/publisherID"); assertThat(result.getValue().get(0).getHeaders()).isNotNull() .extracting(Map.Entry::getKey, Map.Entry::getValue) - .containsOnly(tuple("x-openrtb-version", "2.5"), + .containsOnly(tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"), tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString())); } @@ -185,11 +184,6 @@ public void makeBidsShouldReturnTypeBannerWhenResponseExtCreativeTypeEmpty() thr BidderError.badServerResponse("Unsupported creative type wrong type.")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(smartrtbBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static BidRequest givenBidRequest( Function bidRequestCustomizer, Function impCustomizer) { @@ -213,6 +207,7 @@ private static Imp givenImp(Function impCustomiz private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) .build(); diff --git a/src/test/java/org/prebid/server/bidder/smartyads/SmartyAdsBidderTest.java b/src/test/java/org/prebid/server/bidder/smartyads/SmartyAdsBidderTest.java new file mode 100644 index 00000000000..6dcffbdab37 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/smartyads/SmartyAdsBidderTest.java @@ -0,0 +1,372 @@ +package org.prebid.server.bidder.smartyads; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.junit.Before; +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.smartyads.ExtImpSmartyAds; +import org.prebid.server.util.HttpUtil; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static java.util.Collections.singletonList; +import static java.util.function.Function.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +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; + +public class SmartyAdsBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = + "http://{{Host}}.test.com/bid?param1={{SourceId}}¶m2={{AccountID}}"; + + private SmartyAdsBidder smartyAdsBidder; + + @Before + public void setUp() { + smartyAdsBidder = new SmartyAdsBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new SmartyAdsBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0).getMessage()).startsWith("ext.bidder not provided"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfExtBidderAccountIdParamIsMissed() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpSmartyAds.of("", "testSourceId", "testHost"))))); + + // when + final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("accountId is a required ext.bidder param")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfExtBidderSourceIdParamIsMissed() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpSmartyAds.of("testAccountId", "", "testHost"))))); + + // when + final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badInput("sourceId is a required ext.bidder param")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfExtBidderHostParamIsMissed() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpSmartyAds.of("testAccountId", "testSourceId", ""))))); + + // when + final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badInput("host is a required ext.bidder param")); + } + + @Test + public void makeHttpRequestsShouldCreateCorrectURL() { + // given + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder); + + // when + final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().get(0).getUri()) + .isEqualTo("http://testHost.test.com/bid?param1=testSourceId¶m2=testAccountId"); + } + + @Test + public void shouldRemoveAllImpsExts() { + // given + final Imp firstImp = givenImp(impBuilder -> impBuilder.id("346")); + final Imp secondImp = givenImp(impBuilder -> impBuilder.id("789")); + + final BidRequest bidRequest = BidRequest.builder().imp(Arrays.asList(firstImp, secondImp)).build(); + + // when + final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .containsOnly(updateImpExtWithNull(firstImp), updateImpExtWithNull(secondImp)); + } + + private Imp updateImpExtWithNull(Imp imp) { + return imp.toBuilder().ext(null).build(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final HttpCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = smartyAdsBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Bad Server Response")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(null)); + + // when + final Result> result = smartyAdsBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Bad Server Response")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = smartyAdsBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Empty SeatBid array")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = smartyAdsBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnEmptyValueIfBidsAreNotPresent() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) + .build(), + mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(null) + .build())) + .build())); + + // when + final Result> result = smartyAdsBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldWorkWithOnlyFirstSeatBid() throws JsonProcessingException { + // given + final SeatBid firstSeat = SeatBid.builder() + .bid(Collections.singletonList(Bid.builder().impid("456").build())) + .build(); + + final SeatBid secondSeat = SeatBid.builder() + .bid(Collections.singletonList(Bid.builder().impid("789").build())) + .build(); + + final BidResponse bidResponse = BidResponse.builder() + .cur("USD") + .seatbid(Arrays.asList(firstSeat, secondSeat)) + .build(); + + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) + .build(), + mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = smartyAdsBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("456").build(), banner, "USD")); + } + + @Test + public void makeHttpRequestsShouldSetAdditionalHeadersIfDeviceFieldsAreNotEmpty() { + // given + final BidRequest bidRequest = givenBidRequest( + requestBuilder -> requestBuilder + .device(Device.builder() + .ua("user_agent") + .ip("test_ip") + .dnt(23) + .language("testLang") + .build()), + identity()); + + // when + final Result>> result = smartyAdsBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getHeaders) + .flatExtracting(MultiMap::entries) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder( + tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), "application/json;charset=utf-8"), + tuple(HttpUtil.ACCEPT_HEADER.toString(), "application/json"), + tuple(HttpUtil.ACCEPT_LANGUAGE_HEADER.toString(), "testLang"), + tuple(HttpUtil.DNT_HEADER.toString(), "23"), + tuple(HttpUtil.USER_AGENT_HEADER.toString(), "user_agent"), + tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"), + tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "test_ip")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = smartyAdsBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = smartyAdsBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); + } + + private static BidRequest givenBidRequest( + Function bidRequestCustomizer, + Function impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static BidRequest givenBidRequest(Function impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static Imp givenImp(Function impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .banner(Banner.builder().id("banner_id").build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpSmartyAds.of("testAccountId", "testSourceId", "testHost"))))) + .build(); + } + + private static BidResponse givenBidResponse(Function bidCustomizer) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static HttpCall givenHttpCall(BidRequest bidRequest, String body) { + return HttpCall.success( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/smilewanted/SmileWantedBidderTest.java b/src/test/java/org/prebid/server/bidder/smilewanted/SmileWantedBidderTest.java new file mode 100644 index 00000000000..b1905060bf0 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/smilewanted/SmileWantedBidderTest.java @@ -0,0 +1,207 @@ +package org.prebid.server.bidder.smilewanted; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.vertx.core.MultiMap; +import org.junit.Before; +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.between.ExtImpBetween; +import org.prebid.server.util.HttpUtil; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static java.util.Collections.singletonList; +import static java.util.function.Function.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; + +public class SmileWantedBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://{{Host}}/test?param={{PublisherId}}"; + + private SmileWantedBidder smileWantedBidder; + + @Before + public void setUp() { + smileWantedBidder = new SmileWantedBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new SmileWantedBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldCorrectlyAddHeaders() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final Result>> result = smileWantedBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getHeaders) + .flatExtracting(MultiMap::entries) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder( + tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), + tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()), + tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"), + tuple("sw-integration-type", "prebid_server")); + } + + @Test + public void makeHttpRequestsShouldSetAtToOne() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final Result>> result = smileWantedBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getAt) + .containsExactly(1); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final HttpCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = smileWantedBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = smileWantedBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = smileWantedBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImpAndCorrespondingBid() + throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = smileWantedBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, null)); + } + + @Test + public void makeBidsShouldReturnBannerBidIfVideoIsAbsentInRequestImp() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = smileWantedBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null)); + } + + private static BidRequest givenBidRequest( + Function bidRequestCustomizer, + Function impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static BidRequest givenBidRequest(Function impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static Imp givenImp(Function impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .banner(Banner.builder().w(23).h(25).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of("127.0.0.1", "pubId"))))) + .build(); + } + + private static BidResponse givenBidResponse(Function bidCustomizer) { + return BidResponse.builder() + .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static HttpCall givenHttpCall(BidRequest bidRequest, String body) { + return HttpCall.success( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/somoaudience/SomoaudienceBidderTest.java b/src/test/java/org/prebid/server/bidder/somoaudience/SomoaudienceBidderTest.java index 84f950b3fe4..85c0f5d0420 100644 --- a/src/test/java/org/prebid/server/bidder/somoaudience/SomoaudienceBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/somoaudience/SomoaudienceBidderTest.java @@ -33,14 +33,14 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; -import java.io.IOException; import java.math.BigDecimal; import java.util.List; import java.util.Map; +import java.util.function.Function; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; +import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; @@ -56,26 +56,16 @@ public void setUp() { } @Test - public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMethod() - throws JsonProcessingException { + public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMethod() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .banner(Banner.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of( - null, ExtImpSomoaudience.of("placementId", BigDecimal.valueOf(1.39))))) - .build())) - .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()) - .device(Device.builder().ua("User Agent").ip("ip").dnt(1).language("en").build()) - .regs(Regs.of(0, ExtRegs.of(1, null))) - .build(); + final BidRequest bidRequest = givenBidRequest(identity()); // when final Result>> result = somoaudienceBidder.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).extracting(HttpRequest::getMethod).containsExactly(HttpMethod.POST); + assertThat(result.getValue()).extracting(HttpRequest::getMethod).containsExactly(HttpMethod.POST); assertThat(result.getValue()).extracting(HttpRequest::getUri) .containsExactly("http://somoaudience.com?s=placementId"); assertThat(result.getValue()).flatExtracting(httpRequest -> httpRequest.getHeaders().entries()) @@ -83,37 +73,28 @@ public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMeth .containsOnly( tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()), - tuple("x-openrtb-version", "2.5"), - tuple("User-Agent", "User Agent"), - tuple("X-Forwarded-For", "ip"), - tuple("Accept-Language", "en"), - tuple("DNT", "1")); - assertThat(result.getValue()).extracting(HttpRequest::getBody).containsExactly(mapper.writeValueAsString( - BidRequest.builder() - .imp(singletonList(Imp.builder().banner(Banner.builder().build()) - .bidfloor(BigDecimal.valueOf(1.39)) - .build())) - .user(User.builder() - .ext(ExtUser.builder().consent("consent").build()) - .build()) - .regs(Regs.of(0, ExtRegs.of(1, null))) - .ext(jacksonMapper.fillExtension(ExtRequest.empty(), SomoaudienceReqExt.of("hb_pbs_1.0.0"))) - .device(Device.builder().ua("User Agent").ip("ip").dnt(1).language("en").build()) - .build())); + tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5"), + tuple(HttpUtil.USER_AGENT_HEADER.toString(), "User Agent"), + tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "ip"), + tuple(HttpUtil.ACCEPT_LANGUAGE_HEADER.toString(), "en"), + tuple(HttpUtil.DNT_HEADER.toString(), "1")); + assertThat(result.getValue()).extracting(HttpRequest::getPayload).containsExactly( + givenBidRequest(bidRequestBuilder -> bidRequestBuilder + .ext(jacksonMapper.fillExtension(ExtRequest.empty(), + SomoaudienceReqExt.of("hb_pbs_1.0.0"))), + impBuilder -> impBuilder.ext(null).bidfloor(BigDecimal.valueOf(1.39)))); } @Test - public void makeHttpRequestsShouldTolerateMissingDeviceLanguage() { + public void makeHttpRequestsShouldAddOnlyDeviceNotEmptyValuesToHeaders() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .banner(Banner.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of( - null, ExtImpSomoaudience.of("placementId", BigDecimal.valueOf(1.39))))) - .build())) - .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()) - .device(Device.builder().ua("User Agent").ip("ip").dnt(1).language(null).build()) - .build(); + final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> + bidRequestBuilder.device(Device.builder() + .ua("User Agent") + .ip("") + .dnt(1) + .language(null) + .build()), identity()); // when final Result>> result = somoaudienceBidder.makeHttpRequests(bidRequest); @@ -121,64 +102,54 @@ public void makeHttpRequestsShouldTolerateMissingDeviceLanguage() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).flatExtracting(httpRequest -> httpRequest.getHeaders().entries()) - .extracting(Map.Entry::getKey, Map.Entry::getValue) - .doesNotContain(tuple("Accept-Language", null)); + .extracting(Map.Entry::getKey) + .doesNotContain(HttpUtil.ACCEPT_LANGUAGE_HEADER.toString(), + HttpUtil.X_FORWARDED_FOR_HEADER.toString()); } @Test - public void makeHttpRequestsShouldReturnCorrectRequestBodyAndUri() throws IOException { + public void makeHttpRequestsShouldReturnCorrectRequestBodyAndUri() { // given final BidRequest bidRequest = BidRequest.builder().imp(asList( - Imp.builder() - .banner(Banner.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of( - null, ExtImpSomoaudience.of("placement1", BigDecimal.valueOf(1.54))))) - .build(), - Imp.builder() - .banner(Banner.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of( - null, ExtImpSomoaudience.of("placement2", BigDecimal.valueOf(1.33))))) - .build(), - Imp.builder() + givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of( + null, ExtImpSomoaudience.of("placement1", BigDecimal.valueOf(1.54)))))), + givenImp(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of( + null, ExtImpSomoaudience.of("placement2", BigDecimal.valueOf(1.33)))))), + givenImp(impBuilder -> impBuilder .video(Video.builder().build()) + .banner(null) .ext(mapper.valueToTree(ExtPrebid.of( - null, ExtImpSomoaudience.of("placement3", BigDecimal.valueOf(1.97))))) - .build(), - Imp.builder() + null, ExtImpSomoaudience.of("placement3", BigDecimal.valueOf(1.97)))))), + + givenImp(impBuilder -> impBuilder .xNative(Native.builder().build()) + .banner(null) .ext(mapper.valueToTree(ExtPrebid.of( - null, ExtImpSomoaudience.of("placement4", BigDecimal.valueOf(2.52))))) - .build())) + null, ExtImpSomoaudience.of("placement4", BigDecimal.valueOf(2.52)))))))) .build(); // when final Result>> result = somoaudienceBidder.makeHttpRequests(bidRequest); // then - final String expectedBannersString = mapper.writeValueAsString(BidRequest.builder() + final BidRequest expectedBannersString = BidRequest.builder() .imp(asList( Imp.builder().banner(Banner.builder().build()).bidfloor(BigDecimal.valueOf(1.54)).build(), Imp.builder().banner(Banner.builder().build()).bidfloor(BigDecimal.valueOf(1.33)).build())) .ext(jacksonMapper.fillExtension(ExtRequest.empty(), SomoaudienceReqExt.of("hb_pbs_1.0.0"))) - .build()); - final String expectedVideoSting = mapper.writeValueAsString(BidRequest.builder() - .imp(singletonList( - Imp.builder().video(Video.builder().build()).bidfloor(BigDecimal.valueOf(1.97)).build())) - .ext(jacksonMapper.fillExtension(ExtRequest.empty(), SomoaudienceReqExt.of("hb_pbs_1.0.0"))) - .build()); - final String expectedNativeString = mapper.writeValueAsString(BidRequest.builder() - .imp(singletonList( - Imp.builder().xNative(Native.builder().build()).bidfloor(BigDecimal.valueOf(2.52)).build())) - .ext(jacksonMapper.fillExtension(ExtRequest.empty(), SomoaudienceReqExt.of("hb_pbs_1.0.0"))) - .build()); + .build(); + final BidRequest videoRequest = + expectedRequest(impBuilder -> impBuilder.video(Video.builder().build()), 1.97); + final BidRequest nativeRequest = + expectedRequest(impBuilder -> impBuilder.xNative(Native.builder().build()), 2.52); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(3) - .extracting(HttpRequest::getUri, HttpRequest::getBody) - .containsOnly( + assertThat(result.getValue()) + .extracting(HttpRequest::getUri, HttpRequest::getPayload) + .containsExactlyInAnyOrder( tuple("http://somoaudience.com?s=placement2", expectedBannersString), - tuple("http://somoaudience.com?s=placement3", expectedVideoSting), - tuple("http://somoaudience.com?s=placement4", expectedNativeString)); + tuple("http://somoaudience.com?s=placement3", videoRequest), + tuple("http://somoaudience.com?s=placement4", nativeRequest)); } @Test @@ -196,27 +167,9 @@ public void makeHttpRequestShouldReturnErrorMessageWhenMediaTypeIsAudio() { final Result>> result = somoaudienceBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1) - .containsExactlyInAnyOrder(BidderError.badInput( - "SomoAudience only supports banner and video imps. Ignoring imp id=impId")); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void makeHttpRequestShouldReturnErrorMessageWhenImpExtIsEmpty() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .id("impId") - .banner(Banner.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, null))).build())) - .build(); - // when - final Result>> result = somoaudienceBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("ignoring imp id=impId, extImpBidder is empty")); + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput( + "SomoAudience only supports [banner, video, native] imps. Ignoring imp id : impId")); assertThat(result.getValue()).isEmpty(); } @@ -224,16 +177,13 @@ public void makeHttpRequestShouldReturnErrorMessageWhenImpExtIsEmpty() { public void makeHttpRequestShouldReturnHttpRequestWithErrorMessage() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(asList(Imp.builder() + .imp(asList(givenImp(impBuilder -> impBuilder .id("impId") - .banner(Banner.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, null))).build(), - Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))), + givenImp(impBuilder -> impBuilder .id("impId2") - .banner(Banner.builder().build()) .ext(mapper.valueToTree(ExtPrebid.of( - null, ExtImpSomoaudience.of("placementId", null)))) - .build())) + null, ExtImpSomoaudience.of("placementId", null))))))) .build(); // when @@ -241,25 +191,22 @@ public void makeHttpRequestShouldReturnHttpRequestWithErrorMessage() { // then assertThat(result.getErrors()).hasSize(1) - .containsExactly(BidderError.badInput("ignoring imp id=impId, extImpBidder is empty")); + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("ignoring imp id=impId, error while decoding"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + }); assertThat(result.getValue()).extracting(HttpRequest::getUri) .containsExactly("http://somoaudience.com?s=placementId"); - assertThat(result.getValue()).hasSize(1) - .extracting(HttpRequest::getBody) - .containsOnly(jacksonMapper.encode(BidRequest.builder() - .imp(singletonList(Imp.builder().id("impId2").banner(Banner.builder().build()).build())) - .ext(jacksonMapper.fillExtension(ExtRequest.empty(), singletonMap("prebid", "hb_pbs_1.0.0"))) - .build())); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .containsExactly( + expectedRequest(impBuilder -> impBuilder.id("impId2").banner(Banner.builder().build()), null)); } @Test public void makeBidsShouldReturnBidWithoutErrors() throws JsonProcessingException { // given - final String response = mapper.writeValueAsString(BidResponse.builder() - .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(Bid.builder().impid("impId").build())) - .build())) - .build()); + final String response = mapper.writeValueAsString(givenBidResponse(identity())); final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("impId") .banner(Banner.builder().build()).build())) .build(); @@ -271,18 +218,14 @@ public void makeBidsShouldReturnBidWithoutErrors() throws JsonProcessingExceptio // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.banner, null)); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.banner, "EUR")); } @Test public void makeBidsShouldTakeBannerPrecedencyOverAllOtherMediaTypes() throws JsonProcessingException { // given - final String response = mapper.writeValueAsString(BidResponse.builder() - .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(Bid.builder().impid("impId").build())) - .build())) - .build()); + final String response = mapper.writeValueAsString(givenBidResponse(identity())); final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("impId") .banner(Banner.builder().build()) .video(Video.builder().build()) @@ -296,18 +239,14 @@ public void makeBidsShouldTakeBannerPrecedencyOverAllOtherMediaTypes() throws Js // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.banner, null)); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.banner, "EUR")); } @Test public void makeBidsShouldTakeVideoPrecedencyOverNative() throws JsonProcessingException { // given - final String response = mapper.writeValueAsString(BidResponse.builder() - .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(Bid.builder().impid("impId").build())) - .build())) - .build()); + final String response = mapper.writeValueAsString(givenBidResponse(identity())); final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("impId") .video(Video.builder().build()) .xNative(Native.builder().build()).build())) @@ -320,18 +259,14 @@ public void makeBidsShouldTakeVideoPrecedencyOverNative() throws JsonProcessingE // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.video, null)); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.video, "EUR")); } @Test public void makeBidsShouldReturnBidsWithNativeTypeOnlyWhenAllOtherAreAbsent() throws JsonProcessingException { // given - final String response = mapper.writeValueAsString(BidResponse.builder() - .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(Bid.builder().impid("impId").build())) - .build())) - .build()); + final String response = mapper.writeValueAsString(givenBidResponse(identity())); final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("impId") .xNative(Native.builder().build()).build())) .build(); @@ -343,18 +278,14 @@ public void makeBidsShouldReturnBidsWithNativeTypeOnlyWhenAllOtherAreAbsent() th // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.xNative, null)); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.xNative, "EUR")); } @Test public void makeBidsShouldReturnBidsWithMediaBidTypeIfMediaTypeWasNotDefinedInImp() throws JsonProcessingException { // given - final String response = mapper.writeValueAsString(BidResponse.builder() - .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(Bid.builder().impid("impId").build())) - .build())) - .build()); + final String response = mapper.writeValueAsString(givenBidResponse(identity())); final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("impId").build())).build(); @@ -365,30 +296,25 @@ public void makeBidsShouldReturnBidsWithMediaBidTypeIfMediaTypeWasNotDefinedInIm // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.banner, null)); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.banner, "EUR")); } @Test public void makeBidsShouldReturnBidWithMediaBidTypeIfCorrespondentImpWasNotFound() throws JsonProcessingException { // given - final String response = mapper.writeValueAsString(BidResponse.builder() - .cur("EUR") - .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(Bid.builder().impid("impId").build())) - .build())) - .build()); + final BidResponse response = givenBidResponse(identity()); final BidRequest bidRequest = BidRequest.builder().imp(singletonList(Imp.builder().id("impId2").build())).build(); - final HttpCall httpCall = givenHttpCall(response); + final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(response)); // when final Result> result = somoaudienceBidder.makeBids(httpCall, bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) + assertThat(result.getValue()) .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.banner, "EUR")); } @@ -445,10 +371,51 @@ public void makeBidsShouldReturnEmptyBidderWithErrorWhenResponseCantBeParsed() { // then assertThat(result.getErrors()).hasSize(1) - .containsExactly(BidderError.badServerResponse( - "Failed to decode: Unexpected end-of-input: expected close marker for Object (start marker at" - + " [Source: (String)\"{\"; line: 1, column: 1])\n at [Source: (String)\"{\"; line: 1, " - + "column: 3]")); + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: "); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest( + Function bidRequestCustomizer, + Function impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()) + .device(Device.builder().ua("User Agent").ip("ip").dnt(1).language("en").build()) + .regs(Regs.of(0, ExtRegs.of(1, null))) + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static BidRequest givenBidRequest(Function impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static Imp givenImp(Function impCustomizer) { + return impCustomizer.apply(Imp.builder() + .banner(Banner.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of( + null, ExtImpSomoaudience.of("placementId", BigDecimal.valueOf(1.39)))))) + .build(); + } + + private static BidResponse givenBidResponse(Function bidCustomizer) { + return BidResponse.builder() + .cur("EUR") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).impid("impId").build())) + .build())) + .build(); + } + + private static BidRequest expectedRequest(Function impCustomizer, Double bidFloor) { + return BidRequest.builder() + .ext(jacksonMapper.fillExtension(ExtRequest.empty(), SomoaudienceReqExt.of("hb_pbs_1.0.0"))) + .imp(singletonList(impCustomizer.apply(Imp.builder() + .bidfloor(bidFloor != null ? BigDecimal.valueOf(bidFloor) : null)) + .build())).build(); } private static HttpCall givenHttpCall(String body) { diff --git a/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java b/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java index 5a8c2c98ef8..6985f3ec5df 100644 --- a/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.function.Function; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -199,11 +198,6 @@ public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonPr assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(sonobiBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static BidRequest givenBidRequest( Function impCustomizer, Function requestCustomizer) { @@ -229,6 +223,7 @@ private static Imp givenImp(Function impCustomiz private static BidResponse givenBidResponse( Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) diff --git a/src/test/java/org/prebid/server/bidder/sovrn/SovrnAdapterTest.java b/src/test/java/org/prebid/server/bidder/sovrn/SovrnAdapterTest.java deleted file mode 100644 index b9edcb4a5f1..00000000000 --- a/src/test/java/org/prebid/server/bidder/sovrn/SovrnAdapterTest.java +++ /dev/null @@ -1,480 +0,0 @@ -package org.prebid.server.bidder.sovrn; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Banner; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Regs; -import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.Source; -import com.iab.openrtb.request.User; -import com.iab.openrtb.response.Bid; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.prebid.server.VertxTest; -import org.prebid.server.auction.model.AdUnitBid; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.bidder.model.AdapterHttpRequest; -import org.prebid.server.bidder.model.ExchangeCall; -import org.prebid.server.bidder.sovrn.proto.SovrnParams; -import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.proto.openrtb.ext.request.ExtRegs; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.request.PreBidRequest; -import org.prebid.server.proto.response.BidderDebug; -import org.prebid.server.proto.response.MediaType; - -import java.math.BigDecimal; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -import static java.util.Arrays.asList; -import static java.util.Collections.emptySet; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; -import static java.util.function.Function.identity; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.tuple; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; - -public class SovrnAdapterTest extends VertxTest { - - private static final String BIDDER = "sovrn"; - private static final String COOKIE_FAMILY = BIDDER; - private static final String ENDPOINT_URL = "http://endpoint.org/"; - - @Rule - public final MockitoRule mockitoRule = MockitoJUnit.rule(); - - @Mock - private UidsCookie uidsCookie; - - private AdapterRequest adapterRequest; - private PreBidRequestContext preBidRequestContext; - private ExchangeCall exchangeCall; - private SovrnAdapter adapter; - - @Before - public void setUp() { - adapterRequest = givenBidder(identity()); - preBidRequestContext = givenPreBidRequestContext(identity(), identity()); - adapter = new SovrnAdapter(COOKIE_FAMILY, ENDPOINT_URL, jacksonMapper); - } - - @Test - public void creationShouldFailOnNullArguments() { - assertThatNullPointerException().isThrownBy(() -> new SovrnAdapter(null, null, jacksonMapper)); - assertThatNullPointerException().isThrownBy(() -> new SovrnAdapter(COOKIE_FAMILY, null, jacksonMapper)); - } - - @Test - public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new SovrnAdapter(COOKIE_FAMILY, "invalid_url", jacksonMapper)) - .withMessage("URL supplied is not valid: invalid_url"); - } - - @Test - public void makeHttpRequestsShouldReturnRequestsWithExpectedHeaders() { - // given - given(uidsCookie.uidFrom(COOKIE_FAMILY)).willReturn("990011"); - preBidRequestContext = givenPreBidRequestContext( - preBidRequestContextBuilder -> preBidRequestContextBuilder.ua("userAgent").ip("192.168.244.1"), - preBidRequestBuilder -> preBidRequestBuilder - .device(Device.builder().dnt(11).language("fr").build())); - - // when - final List> httpRequests = adapter.makeHttpRequests(adapterRequest, - preBidRequestContext); - - // then - assertThat(httpRequests).flatExtracting(r -> r.getHeaders().entries()) - .extracting(Map.Entry::getKey, Map.Entry::getValue) - .containsOnly( - tuple("Content-Type", "application/json;charset=utf-8"), - tuple("Accept", "application/json"), - tuple("User-Agent", "userAgent"), - tuple("X-Forwarded-For", "192.168.244.1"), - tuple("DNT", "11"), - tuple("Accept-Language", "fr"), - tuple("Cookie", "ljt_reader=990011")); - } - - @Test - public void makeHttpRequestsShouldFailIfParamsMissingInAtLeastOneAdUnitBid() { - // given - adapterRequest = AdapterRequest.of(BIDDER, singletonList(givenAdUnitBid( - adUnitBidBuilder -> adUnitBidBuilder - .params(null)))); - - // when and then - assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext)) - .isExactlyInstanceOf(PreBidException.class) - .hasMessage("Sovrn params section is missing"); - } - - @Test - public void makeHttpRequestsShouldFailIfAdUnitBidParamsCouldNotBeParsed() { - // given - final ObjectNode params = mapper.createObjectNode(); - params.set("bidfloor", new TextNode("non-float")); - adapterRequest = givenBidder(adUnitBidBuilder -> adUnitBidBuilder.params(params)); - - // when and then - assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext)) - .isExactlyInstanceOf(PreBidException.class) - .hasMessageStartingWith("Cannot deserialize value of type"); - } - - @Test - public void makeHttpRequestsShouldFailIfMediaTypeIsEmpty() { - // given - adapterRequest = AdapterRequest.of(BIDDER, singletonList( - givenAdUnitBid(builder -> builder - .adUnitCode("adUnitCode1") - .mediaTypes(emptySet())))); - - preBidRequestContext = givenPreBidRequestContext(identity(), identity()); - - // when and then - assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext)) - .isExactlyInstanceOf(PreBidException.class) - .hasMessage("openRTB bids need at least one Imp"); - } - - @Test - public void makeHttpRequestsShouldFailIfMediaTypeIsVideo() { - // given - adapterRequest = AdapterRequest.of(BIDDER, singletonList( - givenAdUnitBid(builder -> builder - .adUnitCode("adUnitCode1") - .mediaTypes(EnumSet.of(MediaType.video))))); - - preBidRequestContext = givenPreBidRequestContext(identity(), identity()); - - // when and then - assertThatThrownBy(() -> adapter.makeHttpRequests(adapterRequest, preBidRequestContext)) - .isExactlyInstanceOf(PreBidException.class) - .hasMessage("openRTB bids need at least one Imp"); - } - - @Test - public void makeHttpRequestsShouldReturnRequestsWithUserFromPreBidRequestIfAppPresent() { - // given - preBidRequestContext = givenPreBidRequestContext(identity(), builder -> builder - .app(App.builder().build()) - .user(User.builder().buyeruid("110099").build())); - - given(uidsCookie.uidFrom(eq(BIDDER))).willReturn("buyerUidFromCookie"); - - // when - final List> httpRequests = adapter.makeHttpRequests(adapterRequest, - preBidRequestContext); - - // then - assertThat(httpRequests).hasSize(1) - .extracting(r -> r.getPayload().getUser()) - .containsOnly(User.builder().buyeruid("110099").build()); - } - - @Test - public void makeHttpRequestsShouldReturnListWithOneRequestIfMultipleAdUnitsInPreBidRequest() { - // given - adapterRequest = AdapterRequest.of(BIDDER, asList( - givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1")), - givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2")))); - - // when - final List> httpRequests = adapter.makeHttpRequests(adapterRequest, - preBidRequestContext); - - // then - assertThat(httpRequests).hasSize(1) - .flatExtracting(r -> r.getPayload().getImp()).hasSize(2) - .extracting(Imp::getId).containsOnly("adUnitCode1", "adUnitCode2"); - } - - @Test - public void makeHttpRequestsShouldReturnRequestsWithExpectedFields() { - // given - adapterRequest = givenBidder( - builder -> builder - .bidderCode(BIDDER) - .adUnitCode("adUnitCode1") - .params(mapper.valueToTree(SovrnParams.of("tagid", null))) - .instl(1) - .topframe(1) - .sizes(singletonList(Format.builder().w(300).h(250).build()))); - - preBidRequestContext = givenPreBidRequestContext( - builder -> builder - .referer("http://www.example.com") - .domain("example.com") - .ip("192.168.144.1") - .ua("userAgent"), - builder -> builder - .tid("tid1") - .timeoutMillis(1500L) - .user(User.builder() - .ext(ExtUser.builder().consent("consent").build()) - .build()) - .regs(Regs.of(0, ExtRegs.of(1, null))) - .device(Device.builder() - .pxratio(new BigDecimal("4.2")) - .build())); - - given(uidsCookie.uidFrom(eq(COOKIE_FAMILY))).willReturn("110099"); - - // when - final List> httpRequests = adapter.makeHttpRequests(adapterRequest, - preBidRequestContext); - - // then - assertThat(httpRequests).hasSize(1) - .extracting(AdapterHttpRequest::getPayload) - .containsOnly(BidRequest.builder() - .id("tid1") - .at(1) - .tmax(1500L) - .imp(singletonList(Imp.builder() - .id("adUnitCode1") - .instl(1) - .tagid("tagid") - .banner(Banner.builder() - .w(300) - .h(250) - .topframe(1) - .format(singletonList(Format.builder() - .w(300) - .h(250) - .build())) - .build()) - .build())) - .site(Site.builder() - .domain("example.com") - .page("http://www.example.com") - .build()) - .device(Device.builder() - .ua("userAgent") - .ip("192.168.144.1") - .pxratio(new BigDecimal("4.2")) - .build()) - .user(User.builder() - .buyeruid("110099") - .ext(ExtUser.builder().consent("consent").build()) - .build()) - .regs(Regs.of(0, ExtRegs.of(1, null))) - .source(Source.builder() - .fd(1) - .tid("tid1") - .build()) - .build()); - } - - @Test - public void extractBidsShouldFailIfBidImpIdDoesNotMatchAdUnitCode() { - // given - adapterRequest = givenBidder(builder -> builder.adUnitCode("adUnitCode")); - - exchangeCall = givenExchangeCall(identity(), - bidResponseBuilder -> bidResponseBuilder.seatbid(singletonList(SeatBid.builder() - .bid(singletonList(Bid.builder().impid("anotherAdUnitCode").build())) - .build()))); - - // when and then - assertThatThrownBy(() -> adapter.extractBids(adapterRequest, exchangeCall)) - .isExactlyInstanceOf(PreBidException.class) - .hasMessage("Unknown ad unit code 'anotherAdUnitCode'"); - } - - @Test - public void extractBidsShouldReturnEmptyBidsIfEmptyOrNullBidResponse() { - // given - adapterRequest = givenBidder(identity()); - - exchangeCall = givenExchangeCall(identity(), br -> br.seatbid(null)); - - // when and then - assertThat(adapter.extractBids(adapterRequest, exchangeCall)).isEmpty(); - assertThat(adapter.extractBids(adapterRequest, ExchangeCall.empty(null))).isEmpty(); - } - - @Test - public void extractBidsShouldReturnMultipleBidBuildersIfMultipleAdUnitsInPreBidRequestAndBidsInResponse() { - // given - adapterRequest = AdapterRequest.of(BIDDER, asList( - givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1")), - givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2")))); - - exchangeCall = givenExchangeCall(identity(), - bidResponseBuilder -> bidResponseBuilder.id("bidResponseId") - .seatbid(singletonList(SeatBid.builder() - .seat("seatId") - .bid(asList( - Bid.builder().impid("adUnitCode1").build(), - Bid.builder().impid("adUnitCode2").build())) - .build()))); - - // when - final List bids = - adapter.extractBids(adapterRequest, exchangeCall).stream() - .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList()); - - // then - assertThat(bids).hasSize(2) - .extracting(org.prebid.server.proto.response.Bid::getCode) - .containsOnly("adUnitCode1", "adUnitCode2"); - } - - @Test - public void extractBidsShouldReturnBidsBuilderWithExpectedFields() { - // given - adapterRequest = givenBidder( - adUnitBidBuilder -> adUnitBidBuilder - .bidId("bidId") - .bidderCode("bidderCode") - .adUnitCode("adUnitCode")); - - exchangeCall = givenExchangeCall( - identity(), - bidResponseBuilder -> bidResponseBuilder - .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(Bid.builder() - .price(BigDecimal.ONE) - .impid("adUnitCode") - .adm("%3Cdiv%3EThis%20is%20an%20Ad%3C%2Fdiv%3E") - .crid("crid") - .w(100) - .h(200) - .dealid("dealid") - .nurl("nurl") - .build())) - .build()))); - - // when - final List bids = - adapter.extractBids(adapterRequest, exchangeCall).stream() - .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList()); - - // then - assertThat(bids).hasSize(1) - .element(0) - .isEqualTo(org.prebid.server.proto.response.Bid.builder() - .bidId("bidId") - .code("adUnitCode") - .bidder("bidderCode") - .price(BigDecimal.ONE) - .adm("
    This is an Ad
    ") - .creativeId("crid") - .width(100) - .height(200) - .dealId("dealid") - .nurl("nurl") - .build()); - } - - @Test - public void extractBidsShouldReturnBidBuildersOrderedByBidPrice() { - // given - adapterRequest = AdapterRequest.of(BIDDER, asList( - givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode1")), - givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode2")), - givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode3")), - givenAdUnitBid(builder -> builder.adUnitCode("adUnitCode4")))); - - exchangeCall = givenExchangeCall(identity(), - bidResponseBuilder -> bidResponseBuilder.id("bidResponseId") - .seatbid(singletonList(SeatBid.builder() - .seat("seatId") - .bid(asList( - Bid.builder().impid("adUnitCode1").price(BigDecimal.TEN).build(), - Bid.builder().impid("adUnitCode2").price(BigDecimal.ONE).build(), - Bid.builder().impid("adUnitCode3").price(BigDecimal.valueOf(999)).build(), - Bid.builder().impid("adUnitCode4").price(BigDecimal.ZERO).build())) - .build()))); - - // when - final List bids = - adapter.extractBids(adapterRequest, exchangeCall).stream() - .map(org.prebid.server.proto.response.Bid.BidBuilder::build).collect(Collectors.toList()); - - // then - assertThat(bids).hasSize(4) - .extracting( - org.prebid.server.proto.response.Bid::getCode, - org.prebid.server.proto.response.Bid::getPrice) - .containsOnly( - tuple("adUnitCode4", BigDecimal.ZERO), - tuple("adUnitCode2", BigDecimal.ONE), - tuple("adUnitCode1", BigDecimal.TEN), - tuple("adUnitCode3", BigDecimal.valueOf(999))); - } - - private static AdapterRequest givenBidder( - Function adUnitBidBuilderCustomizer) { - return AdapterRequest.of(BIDDER, singletonList(givenAdUnitBid(adUnitBidBuilderCustomizer))); - } - - private static AdUnitBid givenAdUnitBid( - Function adUnitBidBuilderCustomizer) { - - // ad unit bid - final AdUnitBid.AdUnitBidBuilder adUnitBidBuilderMinimal = AdUnitBid.builder() - .sizes(singletonList(Format.builder().w(300).h(250).build())) - .mediaTypes(singleton(MediaType.banner)) - .params(mapper.valueToTree(SovrnParams.of("tagid", 14f))); - final AdUnitBid.AdUnitBidBuilder adUnitBidBuilderCustomized = adUnitBidBuilderCustomizer.apply( - adUnitBidBuilderMinimal); - - return adUnitBidBuilderCustomized.build(); - } - - private PreBidRequestContext givenPreBidRequestContext( - Function preBidRequestContextBuilderCustomizer, - Function - preBidRequestBuilderCustomizer) { - - final PreBidRequest.PreBidRequestBuilder preBidRequestBuilderMinimal = PreBidRequest.builder() - .accountId("accountId"); - final PreBidRequest preBidRequest = preBidRequestBuilderCustomizer.apply(preBidRequestBuilderMinimal).build(); - - final PreBidRequestContext.PreBidRequestContextBuilder preBidRequestContextBuilderMinimal = - PreBidRequestContext.builder() - .preBidRequest(preBidRequest) - .uidsCookie(uidsCookie); - return preBidRequestContextBuilderCustomizer.apply(preBidRequestContextBuilderMinimal).build(); - } - - private static ExchangeCall givenExchangeCall( - Function bidRequestBuilderCustomizer, - Function bidResponseBuilderCustomizer) { - - final BidRequest.BidRequestBuilder bidRequestBuilderMinimal = BidRequest.builder(); - final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(bidRequestBuilderMinimal).build(); - - final BidResponse.BidResponseBuilder bidResponseBuilderMinimal = BidResponse.builder(); - final BidResponse bidResponse = bidResponseBuilderCustomizer.apply(bidResponseBuilderMinimal).build(); - - return ExchangeCall.success(bidRequest, bidResponse, BidderDebug.builder().build()); - } -} diff --git a/src/test/java/org/prebid/server/bidder/sovrn/SovrnBidderTest.java b/src/test/java/org/prebid/server/bidder/sovrn/SovrnBidderTest.java index d8e16048d4b..2372ab92972 100644 --- a/src/test/java/org/prebid/server/bidder/sovrn/SovrnBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/sovrn/SovrnBidderTest.java @@ -175,6 +175,75 @@ public void makeHttpRequestsShouldReturnResultWithHttpRequestContainingExpectedF .build()); } + @Test + public void makeHttpRequestsShouldSetBidFloorFromExtIfImpBidFloorIsZero() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList( + Imp.builder().id("impId") + .bidfloor(BigDecimal.ZERO) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSovrn.of(null, null, BigDecimal.TEN)))) + .build())) + .build(); + + // when + final Result>> result = sovrnBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getBody) + .extracting(body -> mapper.readValue(body, BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor) + .containsExactly(BigDecimal.TEN); + } + + @Test + public void makeHttpRequestsShouldSetBidFloorFromExtIfImpBidFloorIsMissed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList( + Imp.builder().id("impId") + .bidfloor(null) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSovrn.of(null, null, BigDecimal.TEN)))) + .build())) + .build(); + + // when + final Result>> result = sovrnBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getBody) + .extracting(body -> mapper.readValue(body, BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor) + .containsExactly(BigDecimal.TEN); + } + + @Test + public void makeHttpRequestsShouldNotSetBidFloorFromExtIfImpBidFloorIsValid() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList( + Imp.builder().id("impId") + .bidfloor(BigDecimal.ONE) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSovrn.of(null, null, BigDecimal.TEN)))) + .build())) + .build(); + + // when + final Result>> result = sovrnBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getBody) + .extracting(body -> mapper.readValue(body, BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor) + .containsExactly(BigDecimal.ONE); + } + @Test public void makeHttpRequestsShouldReturnResultWithHttpRequestsContainingExpectedHeaders() { // given diff --git a/src/test/java/org/prebid/server/bidder/synacormedia/SynacormediaBidderTest.java b/src/test/java/org/prebid/server/bidder/synacormedia/SynacormediaBidderTest.java index 56c7a0c1da0..f4bce9c514a 100644 --- a/src/test/java/org/prebid/server/bidder/synacormedia/SynacormediaBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/synacormedia/SynacormediaBidderTest.java @@ -22,20 +22,18 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.synacormedia.ExtImpSynacormedia; +import org.prebid.server.proto.openrtb.ext.request.synacormedia.ExtRequestSynacormedia; import java.util.List; import java.util.function.Function; import static java.util.Arrays.asList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.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; public class SynacormediaBidderTest extends VertxTest { @@ -64,8 +62,9 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { final Result>> result = synacormediaBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance"); + assertThat(result.getErrors()).hasSize(1) + .allMatch(error -> error.getType() == BidderError.Type.bad_input + && error.getMessage().startsWith("Invalid Impression")); assertThat(result.getValue()).isEmpty(); } @@ -88,7 +87,7 @@ public void makeHttpRequestsShouldExcludeInvalidImpAndReturnExpectedResult() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .containsOnly(BidRequest.builder() .imp(singletonList(givenImp(identity()).toBuilder().tagid("tagId").build())) - .ext(jacksonMapper.fillExtension(ExtRequest.empty(), ExtImpSynacormedia.of("seatId", "tagId"))) + .ext(jacksonMapper.fillExtension(ExtRequest.empty(), ExtRequestSynacormedia.of("seatId"))) .build()); } @@ -105,7 +104,8 @@ public void makeHttpRequestsShouldReturnErrorIfFirstValidImpHasEmptySeatId() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("Invalid Impression")); + .allMatch(error -> error.getType() == BidderError.Type.bad_input + && error.getMessage().startsWith("Invalid Impression")); assertThat(result.getValue()).isEmpty(); } @@ -122,7 +122,8 @@ public void makeHttpRequestsShouldReturnErrorIfFirstValidImpHasEmptyTagId() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("Invalid Impression")); + .allMatch(error -> error.getType() == BidderError.Type.bad_input + && error.getMessage().startsWith("Invalid Impression")); assertThat(result.getValue()).isEmpty(); } @@ -150,9 +151,9 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { final Result> result = synacormediaBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(result.getErrors()).hasSize(1) + .allMatch(error -> error.getType() == BidderError.Type.bad_server_response + && error.getMessage().startsWith("Failed to decode: Unrecognized token")); assertThat(result.getValue()).isEmpty(); } @@ -229,29 +230,7 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresentAndNoBanner() throws Jso } @Test - public void makeBidsShouldReturnNativeBidIfNativeIsPresentAndNoBannerOrVideo() throws JsonProcessingException { - // given - final HttpCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder() - .audio(Audio.builder().build()) - .xNative(Native.builder().build()) - .id("123").build())) - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); - - // when - final Result> result = synacormediaBidder.makeBids(httpCall, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); - } - - @Test - public void makeBidsShouldReturnAudioBidIfNativeIsPresentAndNoBannerOrVideoOrNative() + public void makeBidsShouldReturnEmptyListIfFoundNotVideoOrBannerObject() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( @@ -268,13 +247,7 @@ public void makeBidsShouldReturnAudioBidIfNativeIsPresentAndNoBannerOrVideoOrNat // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD")); - } - - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(synacormediaBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + assertThat(result.getValue()).isEmpty(); } private static BidRequest givenBidRequest( @@ -293,6 +266,7 @@ private static Imp givenImp(Function impCustomiz private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) 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 7881407bd22..f87db16bace 100644 --- a/src/test/java/org/prebid/server/bidder/tappx/TappxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/tappx/TappxBidderTest.java @@ -17,15 +17,17 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.tappx.model.TappxBidderExt; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.tappx.ExtImpTappx; import java.math.BigDecimal; import java.util.List; import java.util.function.Function; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; +import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; @@ -60,12 +62,13 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldNotModifyIncomingRequest() { + public void makeHttpRequestsShouldReturnErrorIfEndpointUrlComposingFails() { // given final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("host", "tappxkey", "endpoint", null)))) + ExtImpTappx.of("invalid host", "tappxkey", "endpoint", null, + null, null, null)))) .build())) .build(); @@ -73,19 +76,65 @@ public void makeHttpRequestsShouldNotModifyIncomingRequest() { final Result>> result = tappxBidder.makeHttpRequests(bidRequest); // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()) + .startsWith("Failed to build endpoint URL: Illegal character in authority at index 8"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + }); + } + + @Test + public void makeHttpRequestsShouldNotModifyIncomingRequestImp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(impBuilder -> impBuilder + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTappx.of( + "host", "tappxkey", "endpoint", null, + "mktag", singletonList("bcid"), singletonList("bcrid")))))))) + .build(); + + // when + final Result>> result = tappxBidder.makeHttpRequests(bidRequest); + + // then + final ExtRequest extRequest = ExtRequest.empty(); + final TappxBidderExt tappxBidderExt = TappxBidderExt.of("tappxkey", "mktag", singletonList("bcid"), + singletonList("bcrid")); + extRequest.addProperty("bidder", mapper.valueToTree(tappxBidderExt)); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .containsExactly(bidRequest.toBuilder().ext(extRequest).build()); + } + + @Test + public void makeHttpRequestsShouldModifyBidRequestExt() { + // given + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(givenImp(identity()))).build(); + + // when + final Result>> result = tappxBidder.makeHttpRequests(bidRequest); + + // then + final ExtRequest extRequest = ExtRequest.empty(); + final TappxBidderExt tappxBidderExt = TappxBidderExt.of("tappxkey", "mktag", singletonList("bcid"), + singletonList("bcrid")); + extRequest.addProperty("bidder", mapper.valueToTree(tappxBidderExt)); + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .containsOnly(bidRequest); + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .containsExactly(extRequest); } @Test public void makeHttpRequestsShouldSetFirstImpBidFloorFromItsExt() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTappx.of( - "host", "tappxkey", "endpoint", BigDecimal.ONE)))).build())) + .imp(singletonList(givenImp(identity()))) .build(); // when @@ -94,19 +143,88 @@ public void makeHttpRequestsShouldSetFirstImpBidFloorFromItsExt() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getBidfloor) - .containsOnly(BigDecimal.ONE); + .containsExactly(BigDecimal.ONE); } @Test public void makeHttpRequestsShouldMakeRequestWithUrl() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .build(); + + // when + final Result>> result = tappxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String expectedUri = "https://host/endpoint?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; + assertThat(result.getValue()).hasSize(1) + .allSatisfy(httpRequest -> { + assertThat(httpRequest.getUri()).isEqualTo(expectedUri); + assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.POST); + }); + } + + @Test + public void makeHttpRequestShouldBuildCorrectUriWithPathInHostParameter() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpTappx.of("host/rtb/v2/", "tappxkey", "endpoint", BigDecimal.ONE, + null, null, null)))) + .build())) + .build(); + + // when + final Result>> result = tappxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String expectedUri = "https://host/rtb/v2/endpoint?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; + assertThat(result.getValue()).hasSize(1) + .allSatisfy(httpRequest -> { + assertThat(httpRequest.getUri()).isEqualTo(expectedUri); + assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.POST); + }); + } + + @Test + public void makeHttpRequestShouldBuildCorrectUriWithPathInHostParameterButWithoutTrailingForwardSlash() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpTappx.of("host/rtb/v2", "tappxkey", "endpoint", BigDecimal.ONE, + null, null, null)))) + .build())) + .build(); + + // when + final Result>> result = tappxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String expectedUri = "https://host/rtb/v2/endpoint?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; + assertThat(result.getValue()).hasSize(1) + .allSatisfy(httpRequest -> { + assertThat(httpRequest.getUri()).isEqualTo(expectedUri); + assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.POST); + }); + } + + @Test + public void makeHttpRequestShouldBuildCorrectUriWithPathInHostParameterAndSlashBeforeEndpoint() { // given final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("host", "tappxkey", "endpoint", BigDecimal.ONE)))) + ExtImpTappx.of("host/rtb/v2", "tappxkey", "/endpoint", BigDecimal.ONE, + null, null, null)))) .build())) .build(); @@ -115,7 +233,55 @@ public void makeHttpRequestsShouldMakeRequestWithUrl() { // then assertThat(result.getErrors()).isEmpty(); - final String expectedUri = "https://host/endpoint?tappxkey=tappxkey&v=1.1&type_cnn=prebid"; + final String expectedUri = "https://host/rtb/v2/endpoint?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; + assertThat(result.getValue()).hasSize(1) + .allSatisfy(httpRequest -> { + assertThat(httpRequest.getUri()).isEqualTo(expectedUri); + assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.POST); + }); + } + + @Test + public void makeHttpRequestShouldBuildCorrectUriWithWeirdCaseHttpsSchemeInHostParam() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpTappx.of("htTpS://host-host.com/rtb/v2", "tappxkey", "/endpoint", + BigDecimal.ONE, null, null, null)))) + .build())) + .build(); + + // when + final Result>> result = tappxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String expectedUri = "htTpS://host-host.com/rtb/v2/endpoint?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; + assertThat(result.getValue()).hasSize(1) + .allSatisfy(httpRequest -> { + assertThat(httpRequest.getUri()).isEqualTo(expectedUri); + assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.POST); + }); + } + + @Test + public void makeHttpRequestsShouldModifyUrl() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpTappx.of("endpoint.host", "tappxkey", "endpoint", BigDecimal.ONE, + null, null, null)))) + .build())) + .build(); + + // when + final Result>> result = tappxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String expectedUri = "https://endpoint.host?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; assertThat(result.getValue()).hasSize(1) .allSatisfy(httpRequest -> { assertThat(httpRequest.getUri()).isEqualTo(expectedUri); @@ -129,19 +295,22 @@ public void makeHttpRequestsShouldReturnErrorWhenEitherOfExtParametersIsEmpty() final BidRequest bidRequestEmptyHost = BidRequest.builder() .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("", "tappxkey", "endpoint", BigDecimal.ONE)))).build())) + ExtImpTappx.of("", "tappxkey", "endpoint", BigDecimal.ONE, + null, null, null)))).build())) .build(); final BidRequest bidRequestEmptyTappxKey = BidRequest.builder() .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("host", "", "endpoint", BigDecimal.ONE)))).build())) + ExtImpTappx.of("host", "", "endpoint", BigDecimal.ONE, + null, null, null)))).build())) .build(); final BidRequest bidRequestEmptyEndpoint = BidRequest.builder() .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("host", "tappxkey", "", BigDecimal.ONE)))).build())) + ExtImpTappx.of("host", "tappxkey", "", BigDecimal.ONE, + null, null, null)))).build())) .build(); // when @@ -219,7 +388,7 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessing // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); } @Test @@ -238,16 +407,21 @@ public void makeBidsShouldReturnDefaultBannerBid() throws JsonProcessingExceptio // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(tappxBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + private static Imp givenImp(Function impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTappx.of( + "host", "tappxkey", "endpoint", BigDecimal.ONE, + "mktag", singletonList("bcid"), singletonList("bcrid"))))) + ).build(); } private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) diff --git a/src/test/java/org/prebid/server/bidder/telaria/TelariaBidderTest.java b/src/test/java/org/prebid/server/bidder/telaria/TelariaBidderTest.java index ce6dde06de4..34d11cda0cf 100644 --- a/src/test/java/org/prebid/server/bidder/telaria/TelariaBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/telaria/TelariaBidderTest.java @@ -1,5 +1,6 @@ package org.prebid.server.bidder.telaria; +import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; @@ -8,14 +9,12 @@ import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Video; -import io.vertx.core.MultiMap; -import org.prebid.server.VertxTest; -import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.junit.Before; import org.junit.Test; +import org.prebid.server.VertxTest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; @@ -28,12 +27,11 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.telaria.ExtImpTelaria; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Function; -import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -57,21 +55,6 @@ public void creationShouldFailOnInvalidEndpointUrl() { assertThatIllegalArgumentException().isThrownBy(() -> new SmartrtbBidder("invalid_url", jacksonMapper)); } - @Test - public void makeHttpRequestsShouldReturnErrorIfImpressionListSizeIsZero() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(emptyList()) - .build(); - - // when - final Result>> result = telariaBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("Telaria: Missing Imp Object")); - } - @Test public void makeHttpRequestsShouldReturnErrorIfImpHasNoVideo() { // given @@ -84,7 +67,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpHasNoVideo() { final Result>> result = telariaBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1).containsOnly(BidderError.badInput("Telaria: Only Supports Video")); + assertThat(result.getErrors()).containsExactly(BidderError.badInput("Telaria: Only Supports Video")); } @Test @@ -99,7 +82,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpHasBanner() { final Result>> result = telariaBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1).containsOnly(BidderError.badInput("Telaria: Banner not supported")); + assertThat(result.getErrors()).containsExactly(BidderError.badInput("Telaria: Banner not supported")); } @Test @@ -129,7 +112,7 @@ public void makeHttpReturnErrorIfSeatCodeIsEmpty() { final Result>> result = telariaBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1).containsOnly(BidderError.badInput("Telaria: Seat Code required")); + assertThat(result.getErrors()).containsExactly(BidderError.badInput("Telaria: Seat Code required")); } @Test @@ -146,7 +129,7 @@ public void makeHttpRequestsShouldNotChangeExtIfExtExtraIsMissing() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .extracting(BidRequest::getExt) .extracting(ExtRequest::getPrebid) .containsNull(); @@ -173,9 +156,9 @@ public void makeHttpRequestsShouldChangeRequestExtIfExtImpExtraIsNotEmpty() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .extracting(BidRequest::getExt) - .containsOnly(jacksonMapper.fillExtension( + .containsExactly(jacksonMapper.fillExtension( ExtRequest.empty(), TelariaRequestExt.of(mapper.createObjectNode().put("custom", "1234")))); } @@ -194,7 +177,7 @@ public void makeHttpRequestsShouldNotSetAppPublisherIdIfSiteIsNotNull() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(HttpRequest::getPayload) .extracting(BidRequest::getApp) .extracting(App::getId) .containsNull(); @@ -213,12 +196,65 @@ public void makeHttpRequestsShouldSetAppPublisherIdIfSiteIsNull() { // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) .extracting(BidRequest::getApp) .extracting(App::getPublisher) .extracting(Publisher::getId) - .containsOnly("seatCode"); + .containsExactly("seatCode"); + } + + @Test + public void makeHttpRequestsShouldSetSitePublisherIdIfSiteIsPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .site(Site.builder().publisher(Publisher.builder().build()).build()) + .build(); + + // when + final Result>> result = telariaBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .extracting(Publisher::getId) + .containsExactly("seatCode"); + } + + @Test + public void makeBidsShouldReturnEmptyBidderBidsFromSecondSeatBid() throws JsonProcessingException { + // given + final SeatBid firstSeatBId = SeatBid.builder() + .bid(singletonList(Bid.builder() + .impid("123") + .build())) + .build(); + + final SeatBid secondSeatBid = SeatBid.builder() + .bid(singletonList(Bid.builder() + .impid("456") + .build())) + .build(); + + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) + .build(), + mapper.writeValueAsString(BidResponse.builder() + .seatbid(Arrays.asList(firstSeatBId, secondSeatBid)) + .build())); + + // when + final Result> result = telariaBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, null)); } @Test @@ -238,14 +274,13 @@ public void makeHttpRequestsShouldReturnResultWithHttpRequestsContainingExpected final Result>> result = telariaBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getValue()).hasSize(1) + assertThat(result.getValue()) .flatExtracting(r -> r.getHeaders().entries()) .extracting(Map.Entry::getKey, Map.Entry::getValue) - .containsOnly( + .containsExactlyInAnyOrder( tuple("Content-Type", "application/json;charset=utf-8"), tuple("Accept", "application/json"), tuple("x-openrtb-version", "2.5"), - tuple("Accept-Encoding", "gzip"), tuple("User-Agent", "userAgent"), tuple("X-Forwarded-For", "123.123.123.12"), tuple("Accept-Language", "en"), @@ -261,9 +296,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { final Result> result = telariaBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); assertThat(result.getValue()).isEmpty(); } @@ -282,41 +319,37 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso } @Test - public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnErrorIfNoBidsFromSeatArePresent() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build())) - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder().build())).build())); // when final Result> result = telariaBidder.makeBids(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + assertThat(result.getValue()).isEmpty(); } @Test - public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() { + public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException { // given - final HttpCall httpCall = HttpCall - .success(null, HttpResponse.of(204, null, null), null); + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when final Result> result = telariaBidder.makeBids(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(telariaBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); } private static BidRequest givenBidRequest( @@ -343,17 +376,16 @@ private static Imp givenImp(Function impCustomiz private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) .build(); } private static HttpCall givenHttpCall(BidRequest bidRequest, String body) { - final MultiMap headers = MultiMap.caseInsensitiveMultiMap() - .add("x-fb-an-errors", "who are you?"); return HttpCall.success( HttpRequest.builder().payload(bidRequest).build(), - HttpResponse.of(200, headers, body), + HttpResponse.of(200, null, body), null); } } diff --git a/src/test/java/org/prebid/server/bidder/triplelift/TripleliftBidderTest.java b/src/test/java/org/prebid/server/bidder/triplelift/TripleliftBidderTest.java index 30fbeaf2744..9acc63382f8 100644 --- a/src/test/java/org/prebid/server/bidder/triplelift/TripleliftBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/triplelift/TripleliftBidderTest.java @@ -26,7 +26,6 @@ import java.util.List; import java.util.function.Function; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -116,7 +115,7 @@ public void makeHttpRequestsShouldModifyTagIdFromImpExt() { @Test public void makeHttpRequestsShouldModifyBidFloorFromImpExtWhenFloorIsPresent() { // given - final BigDecimal floor = new BigDecimal(12f); + final BigDecimal floor = BigDecimal.valueOf(12.32); final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .bidfloor(new BigDecimal(1)) @@ -156,8 +155,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { @Test public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall(null, - mapper.writeValueAsString(null)); + final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); // when final Result> result = tripleliftBidder.makeBids(httpCall, null); @@ -170,7 +168,8 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces @Test public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall(null, + final HttpCall httpCall = givenHttpCall( + null, mapper.writeValueAsString(BidResponse.builder().build())); // when @@ -215,6 +214,23 @@ public void makeBidsShouldReturnTypeBannerWhenTripleliftResponseExtIsEmpty() thr .containsOnly(BidderBid.of(Bid.builder().ext(ext).build(), banner, "USD")); } + @Test + public void makeBidsShouldReturnTypeBannerWhenTripleliftInnerExtIsNull() throws JsonProcessingException { + // given + final ObjectNode ext = mapper.valueToTree(TripleliftResponseExt.of(TripleliftInnerExt.of(null))); + final HttpCall httpCall = givenHttpCall( + null, + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.ext(ext)))); + + // when + final Result> result = tripleliftBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().ext(ext).build(), banner, "USD")); + } + @Test public void makeBidsShouldReturnTypeBannerWhenTripleliftInnerExtIsNotEleven() throws JsonProcessingException { // given @@ -249,13 +265,9 @@ public void makeBidsShouldReturnTypeVideoWhenTripleliftInnerExtIsEleven() throws .containsOnly(BidderBid.of(Bid.builder().ext(ext).build(), video, "USD")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(tripleliftBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) diff --git a/src/test/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidderTest.java b/src/test/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidderTest.java index b99d60276d1..5d30fe68dca 100644 --- a/src/test/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/tripleliftnative/TripleliftNativeBidderTest.java @@ -265,11 +265,6 @@ public void makeBidsShouldReturnXnativeBidTypeType() throws JsonProcessingExcept .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(tripleliftNativeBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static BidRequest givenBidRequest( Function bidRequestCustomizer, Function impCustomizer, @@ -295,6 +290,7 @@ private static Imp givenImp( private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) diff --git a/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java b/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java index 85431dbc164..feceb706a09 100644 --- a/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ttx/TtxBidderTest.java @@ -4,7 +4,7 @@ import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -19,6 +19,8 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.ttx.proto.TtxImpExt; import org.prebid.server.bidder.ttx.proto.TtxImpExtTtx; +import org.prebid.server.bidder.ttx.response.TtxBidExt; +import org.prebid.server.bidder.ttx.response.TtxBidExtTtx; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ttx.ExtImpTtx; @@ -26,12 +28,13 @@ import java.util.function.Function; import static java.util.Arrays.asList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; public class TtxBidderTest extends VertxTest { @@ -50,7 +53,7 @@ public void creationShouldFailOnInvalidEndpointUrl() { } @Test - public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + public void makeHttpRequestsShouldAppendErrorIfImpExtCouldNotBeParsed() { // given final BidRequest bidRequest = givenBidRequest( impBuilder -> impBuilder @@ -61,14 +64,18 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // then assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Cannot deserialize instance"); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Cannot deserialize instance of"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + }); } @Test - public void makeHttpRequestsShouldSetSiteIdFromImpExt() { + public void makeHttpRequestsShouldNotUpdateSiteIfSiteNotPresent() { // given - final BidRequest bidRequest = givenBidRequest(identity()); + final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> + bidRequestBuilder.site(null), identity()); // when final Result>> result = ttxBidder.makeHttpRequests(bidRequest); @@ -78,20 +85,13 @@ public void makeHttpRequestsShouldSetSiteIdFromImpExt() { assertThat(result.getValue()).hasSize(1) .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(BidRequest::getSite) - .extracting(Site::getId) - .containsOnly("siteId"); + .containsNull(); } @Test - public void makeHttpRequestsShouldGetDetailsOnlyFromFirstImpExt() { + public void makeHttpRequestsShouldNotCreateNewSiteIfSiteNotPresentInBidRequest() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(asList( - givenImp(identity()), - givenImp(impBuilder -> impBuilder - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", "2", "3"))))) - )) - .build(); + final BidRequest bidRequest = givenBidRequest(identity()); // when final Result>> result = ttxBidder.makeHttpRequests(bidRequest); @@ -100,9 +100,8 @@ public void makeHttpRequestsShouldGetDetailsOnlyFromFirstImpExt() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .extracting(BidRequest::getSite) - .extracting(Site::getId) - .containsOnly("siteId"); + .flatExtracting(BidRequest::getSite) + .containsNull(); } @Test @@ -110,10 +109,7 @@ public void makeHttpRequestsShouldChangeOnlyFirstImpExt() { // given final BidRequest bidRequest = BidRequest.builder() .imp(asList( - givenImp(identity()), - givenImp(impBuilder -> impBuilder - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3"))))) - )) + givenImp(identity()))) .build(); // when @@ -126,8 +122,111 @@ public void makeHttpRequestsShouldChangeOnlyFirstImpExt() { .flatExtracting(BidRequest::getImp) .extracting(Imp::getExt) .containsExactly( - mapper.valueToTree(TtxImpExt.of(TtxImpExtTtx.of("productId", "zoneId"))), - mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3")))); + mapper.valueToTree(TtxImpExt.of(TtxImpExtTtx.of("productId", "zoneId")))); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfVideoParamsNotPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList( + givenImp(impBuilder -> impBuilder + .video(Video.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3"))))))) + .build(); + + // when + final Result>> result = ttxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("One or more invalid or missing video field(s) w, h, " + + "protocols, mimes, playbackmethod")); + } + + @Test + public void makeHttpRequestsShouldUpdateNotPresentPlacement() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList( + givenImp(impBuilder -> impBuilder + .video(validVideo()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3"))))))) + .build(); + + // when + final Result>> result = ttxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getVideo) + .extracting(Video::getPlacement) + .containsExactly(2); + } + + @Test + public void makeHttpRequestsShouldNotUpdatePlacementWhenProductIdIsNotInstreamAndPlacementIsNotZero() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList( + givenImp(impBuilder -> impBuilder + .video(validVideo().toBuilder().placement(23).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "3"))))))) + .build(); + + // when + final Result>> result = ttxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getVideo) + .extracting(Video::getPlacement) + .containsExactly(23); + } + + @Test + public void makeHttpRequestsShouldUpdatePlacementAndStartDelayIfProdIsInstream() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList( + givenImp(impBuilder -> impBuilder + .video(validVideo()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTtx.of("11", null, "instream"))))))) + .build(); + + // when + final Result>> result = ttxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getVideo) + .extracting(Video::getPlacement, Video::getStartdelay) + .containsExactly(tuple(1, 0)); + } + + @Test + public void makeBidsShouldReturnErrorIfNoBannerOrVideoPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(impBuilder -> impBuilder.banner(null)))) + .build(); + + // when + final Result>> result = ttxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("Imp ID 123 must have at least one of [Banner, Video] defined")); + assertThat(result.getValue()).isEmpty(); } @Test @@ -139,9 +238,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { final Result> result = ttxBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(result.getErrors()) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); assertThat(result.getValue()).isEmpty(); } @@ -174,7 +275,7 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso } @Test - public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( BidRequest.builder() @@ -193,8 +294,61 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { } @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(ttxBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + public void makeBidsShouldReturnVideoBidIfVideoInBidExt() throws JsonProcessingException { + // given + final TtxBidExt ttxBidExt = TtxBidExt.of(TtxBidExtTtx.of("video")); + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder + .ext(mapper.valueToTree(ttxBidExt))))); + + // when + final Result> result = ttxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + final Bid expectedBid = Bid.builder() + .ext(mapper.valueToTree(ttxBidExt)) + .build(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(expectedBid, video, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBidIfExtNotContainVideoString() throws JsonProcessingException { + // given + final TtxBidExt ttxBidExt = TtxBidExt.of(TtxBidExtTtx.of("notVideo")); + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder + .ext(mapper.valueToTree(ttxBidExt))))); + + // when + final Result> result = ttxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + final Bid expectedBid = Bid.builder() + .ext(mapper.valueToTree(ttxBidExt)) + .build(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(expectedBid, banner, "USD")); + } + + private static Video validVideo() { + return Video.builder() + .w(23) + .h(23) + .mimes(singletonList("mime")) + .protocols(singletonList(23)) + .playbackmethod(singletonList(27)) + .build(); } private static BidRequest givenBidRequest( @@ -220,6 +374,7 @@ private static Imp givenImp(Function impCustomiz private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) diff --git a/src/test/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidderTest.java b/src/test/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidderTest.java index 674573430df..ba60089a021 100644 --- a/src/test/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ucfunnel/UcfunnelBidderTest.java @@ -25,7 +25,6 @@ import java.util.function.Function; import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -167,13 +166,9 @@ public void makeBidsShouldReturnBannerBidIfVideoIsPresentInRequestImp() throws J .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), BidType.video, "USD")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(ucfunnelBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) .build(); diff --git a/src/test/java/org/prebid/server/bidder/unicorn/UnicornBidderTest.java b/src/test/java/org/prebid/server/bidder/unicorn/UnicornBidderTest.java new file mode 100644 index 00000000000..0483d32c8de --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/unicorn/UnicornBidderTest.java @@ -0,0 +1,341 @@ +package org.prebid.server.bidder.unicorn; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.netty.handler.codec.http.HttpHeaderValues; +import org.junit.Before; +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; +import org.prebid.server.proto.openrtb.ext.request.unicorn.ExtImpUnicorn; +import org.prebid.server.util.HttpUtil; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static java.util.Collections.singletonList; +import static java.util.function.Function.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; + +public class UnicornBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://127.0.0.1/test"; + + private UnicornBidder unicornBidder; + + @Before + public void setUp() { + unicornBidder = new UnicornBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new UnicornBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldCorrectlyAddHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> + bidRequestBuilder.device(Device.builder().ua("someUa").ip("someIp").build()), + identity()); + + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .flatExtracting(res -> res.getHeaders().entries()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder( + tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), + tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString()), + tuple(HttpUtil.USER_AGENT_HEADER.toString(), "someUa"), + tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "someIp"), + tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorForNotValidImpExt() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors()) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()) + .startsWith("Error while decoding ext of imp with id: 123, error: Cannot deserialize"); + }); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfCoppaIsOne() { + // given + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.regs(Regs.of(1, null)), identity()); + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badInput("COPPA is not supported")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfGdprIsOne() { + // given + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.regs(Regs.of(0, ExtRegs.of(1, null))), identity()); + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badInput("GDPR is not supported")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfUsPrivacyIsPresent() { + // given + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.regs(Regs.of(0, ExtRegs.of(0, "privacy"))), identity()); + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badInput("CCPA is not supported")); + } + + @Test + public void makeHttpRequestsShouldNotModifyEndpointURL() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://127.0.0.1/test"); + } + + @Test + public void makeHttpRequestsShouldEnrichEveryImpWithSecureAndTagIdParams() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getSecure, Imp::getTagid) + .containsExactly(tuple(1, "placementId")); + } + + @Test + public void makeHttpRequestsShouldSetTagIdAndUpdateBidderPlacementIdPropertyWithStoredRequestProperty() { + // given + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.ext(mapper.valueToTree(ExtPrebid.of(ExtImpPrebid.builder() + .storedrequest(ExtStoredRequest.of("storedRequestId")) + .build(), ExtImpUnicorn.of("", 123, "mediaId", 456))))); + + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getTagid) + .containsExactly("storedRequestId"); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .extracting(node -> node.get("bidder")) + .extracting(bidder -> mapper.convertValue(bidder, ExtImpUnicorn.class)) + .extracting(ExtImpUnicorn::getPlacementId) + .containsExactly("storedRequestId"); + } + + @Test + public void makeHttpRequestsShouldSetAddAccountIdPropertyToRequestExt() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getExt) + .extracting(requestExt -> requestExt.getProperty("accountId").intValue()) + .containsExactly(456); + } + + @Test + public void makeHttpRequestsShouldUpdateSourceValue() { + // given + final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> + bidRequestBuilder.source(Source.builder().tid("someTid").build()), + identity()); + + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + final ExtSource extSource = ExtSource.of(null); + extSource.addProperty("stype", new TextNode("prebid_server_uncn")); + extSource.addProperty("bidder", new TextNode("unicorn")); + final Source expectedSource = Source.builder().tid("someTid").ext(extSource).build(); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSource) + .containsExactly(expectedSource); + } + + @Test + public void makeHttpRequestsShouldReturnErrorForNotFoundStoredRequestId() { + // given + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpUnicorn.of("", 123, "mediaId", 456))))); + + // when + final Result>> result = unicornBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badInput("stored request id not found in imp: 123")); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final HttpCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = unicornBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = unicornBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = unicornBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + givenBidRequest(identity()), + mapper.writeValueAsString( + givenBidResponse(identity()))); + + // when + final Result> result = unicornBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().build(), banner, null)); + } + + private static BidRequest givenBidRequest( + Function bidRequestCustomizer, + Function impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static BidRequest givenBidRequest(Function impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static Imp givenImp(Function impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpUnicorn.of("placementId", 123, "mediaId", 456))))) + .build(); + } + + private static BidResponse givenBidResponse(Function bidCustomizer) { + return BidResponse.builder() + .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static HttpCall givenHttpCall(BidRequest bidRequest, String body) { + return HttpCall.success( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/unruly/UnrulyBidderTest.java b/src/test/java/org/prebid/server/bidder/unruly/UnrulyBidderTest.java index 6d592158fb4..77f73ea8359 100644 --- a/src/test/java/org/prebid/server/bidder/unruly/UnrulyBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/unruly/UnrulyBidderTest.java @@ -24,7 +24,6 @@ import java.util.function.Function; import static java.util.Arrays.asList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -165,11 +164,6 @@ public void makeBidsShouldReturnErrorIfImpressionWasNotFound() throws JsonProces .containsOnly(BidderError.badServerResponse("Failed to find impression 123")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(unrulyBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static BidRequest givenBidRequest( Function bidRequestCustomizer, Function impCustomizer) { @@ -193,6 +187,7 @@ private static Imp givenImp(Function impCustomiz private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) diff --git a/src/test/java/org/prebid/server/bidder/valueimpression/ValueImpressionBidderTest.java b/src/test/java/org/prebid/server/bidder/valueimpression/ValueImpressionBidderTest.java index 9e2e204987c..3c9e926a0f3 100644 --- a/src/test/java/org/prebid/server/bidder/valueimpression/ValueImpressionBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/valueimpression/ValueImpressionBidderTest.java @@ -26,7 +26,6 @@ import java.util.function.Function; import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -198,25 +197,6 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); } - @Test - public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() { - // given - final HttpCall httpCall = HttpCall - .success(null, HttpResponse.of(204, null, null), null); - - // when - final Result> result = valueImpressionBidder.makeBids(httpCall, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(valueImpressionBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static BidRequest givenBidRequest( Function bidRequestCustomizer, Function impCustomizer) { @@ -241,6 +221,7 @@ private static Imp givenImp(Function impCustomiz private static BidResponse givenBidResponse(Function bidCustomizer) { final Bid bid = bidCustomizer.apply(Bid.builder()).build(); return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder().bid(singletonList(bid)) .build())) .build(); diff --git a/src/test/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidderTest.java b/src/test/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidderTest.java index b9588bd5484..0342a6f11ef 100644 --- a/src/test/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/verizonmedia/VerizonmediaBidderTest.java @@ -1,6 +1,7 @@ package org.prebid.server.bidder.verizonmedia; import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -28,7 +29,6 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -128,7 +128,24 @@ public void makeHttpRequestsShouldCreateARequestForEachImpAndSkipImpsWithErrors( } @Test - public void makeHttpRequestsShouldAlwaysSetSiteIdAndImpTagIdFromImpExt() { + public void makeHttpRequestsShouldAlwaysSetImpTagIdFromImpExt() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result = verizonmediaBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp).hasSize(1) + .extracting(Imp::getTagid) + .containsExactly("pos"); + } + + @Test + public void makeHttpRequestsShouldSetSiteIdIfSiteIsPresentInTheRequest() { // given final BidRequest bidRequest = givenBidRequest(identity(), identity()); @@ -141,12 +158,25 @@ public void makeHttpRequestsShouldAlwaysSetSiteIdAndImpTagIdFromImpExt() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(BidRequest::getSite) .extracting(Site::getId) - .containsOnly("dcn"); - assertThat(result.getValue()) + .containsExactly("dcn"); + } + + @Test + public void makeHttpRequestsShouldSetAppIdIfAppIsPresentInTheRequest() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), + bidRequestBuilder -> bidRequestBuilder.site(null).app(App.builder().build())); + + // when + final Result>> result = verizonmediaBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp).hasSize(1) - .extracting(Imp::getTagid) - .containsOnly("pos"); + .extracting(BidRequest::getApp) + .extracting(App::getId) + .containsExactly("dcn"); } @Test @@ -324,6 +354,7 @@ public void makeBidsShouldSkipNotBannerImpAndReturnBannerBidWhenBannerPresent() Imp.builder().banner(Banner.builder().build()).id("321").build())) .build(), mapper.writeValueAsString(BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(asList(Bid.builder().impid("123").build(), Bid.builder().impid("321").build())) @@ -339,11 +370,6 @@ public void makeBidsShouldSkipNotBannerImpAndReturnBannerBidWhenBannerPresent() .containsOnly(BidderBid.of(Bid.builder().impid("321").build(), banner, "USD")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(verizonmediaBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static BidRequest givenBidRequest( Function impCustomizer, Function requestCustomizer) { @@ -363,6 +389,7 @@ private static Imp givenImp(Function impCustomiz private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) diff --git a/src/test/java/org/prebid/server/bidder/visx/VisxBidderTest.java b/src/test/java/org/prebid/server/bidder/visx/VisxBidderTest.java index 3d66b9a34cd..2ed4eb8ac59 100644 --- a/src/test/java/org/prebid/server/bidder/visx/VisxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/visx/VisxBidderTest.java @@ -27,7 +27,6 @@ import java.util.Arrays; import java.util.List; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -181,11 +180,6 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio .hasSize(1).element(0).isEqualTo(expected); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(visxBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static HttpCall givenHttpCall(BidRequest bidRequest, String body) { return HttpCall.success( HttpRequest.builder().payload(bidRequest).build(), diff --git a/src/test/java/org/prebid/server/bidder/vrtcal/VrtcalBidderTest.java b/src/test/java/org/prebid/server/bidder/vrtcal/VrtcalBidderTest.java index a6e9579f3bd..1e87c87458c 100644 --- a/src/test/java/org/prebid/server/bidder/vrtcal/VrtcalBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/vrtcal/VrtcalBidderTest.java @@ -6,6 +6,7 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +import io.netty.handler.codec.http.HttpHeaderValues; import org.junit.Before; import org.junit.Test; import org.prebid.server.VertxTest; @@ -17,15 +18,17 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.vrtcal.ExtImpVrtcal; +import org.prebid.server.util.HttpUtil; import java.util.List; +import java.util.Map; import java.util.function.Function; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; public class VrtcalBidderTest extends VertxTest { @@ -47,10 +50,8 @@ public void creationShouldFailOnInvalidEndpointUrl() { @Test public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))).build())) - .build(); + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); // when final Result>> result = vrtcalBidder.makeHttpRequests(bidRequest); @@ -64,13 +65,8 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { @Test public void makeHttpRequestsShouldNotModifyIncomingRequest() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpVrtcal.of("JustAnUnusedVrtcalParam")))) - .build())) - .id("request_id") - .build(); + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpVrtcal.of("JustAnUnusedVrtcalParam"))))); // when final Result>> result = vrtcalBidder.makeHttpRequests(bidRequest); @@ -82,6 +78,23 @@ public void makeHttpRequestsShouldNotModifyIncomingRequest() { .containsOnly(bidRequest); } + @Test + public void makeHttpRequestShouldReturnCorrectHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(Function.identity()); + + // when + final Result>> result = vrtcalBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(0); + assertThat(result.getValue()).flatExtracting(httpRequest -> httpRequest.getHeaders().entries()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), + tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString())); + } + @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given @@ -141,13 +154,28 @@ public void makeBidsShouldAlwaysReturnBannerBid() throws JsonProcessingException .containsOnly(BidderBid.of(Bid.builder().build(), banner, "USD")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(vrtcalBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + private static BidRequest givenBidRequest( + Function bidRequestCustomizer, + Function impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static BidRequest givenBidRequest(Function impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static Imp givenImp(Function impCustomizer) { + return impCustomizer.apply(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpVrtcal.of("JustAnUnusedVrtcalParam"))))) + .build(); } private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) diff --git a/src/test/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidderTest.java b/src/test/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidderTest.java index 03b68fc7b11..ed868095fed 100644 --- a/src/test/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yeahmobi/YeahmobiBidderTest.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.function.Function; -import static java.util.Collections.emptyMap; +import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -61,8 +61,24 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { final Result>> result = yeahmobiBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Invalid ExtImpYeahmobi value"); + assertThat(result.getErrors()).containsExactly(BidderError.badInput("Invalid ExtImpYeahmobi value")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorOfEveryNotValidImp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList(givenImp(impBuilder -> impBuilder + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))), + givenImp(identity()))) + .build(); + + // when + final Result>> result = yeahmobiBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badInput("Impression id=123, has invalid Ext")); } @Test @@ -79,8 +95,9 @@ public void makeHttpRequestsShouldCreateCorrectURL() { // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1); - assertThat(result.getValue().get(0).getUri()).isEqualTo("https://gw-zoneId-bid.yeahtargeter.com/prebid/bid"); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://gw-zoneId-bid.yeahtargeter.com/prebid/bid"); } @Test @@ -94,7 +111,7 @@ public void makeHttpRequestsShouldAddNativeRequestIfEmpty() { final BidRequest bidRequest = givenBidRequest( impBuilder -> impBuilder .banner(Banner.builder().build()) - .xNative(Native.builder().request(nativeRequest).build())); + .xNative(Native.builder().request(nativeRequest).build())); // when final Result>> result = yeahmobiBidder.makeHttpRequests(bidRequest); @@ -106,12 +123,33 @@ public void makeHttpRequestsShouldAddNativeRequestIfEmpty() { + "{\"id\":7,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}}"; assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getXNative) .extracting(Native::getRequest) - .containsOnly(expectedNativeRequest); + .containsExactly(expectedNativeRequest); + } + + @Test + public void makeHttpRequestsShouldAddEmptyNativeRequestIfRequestNotPresent() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder + .banner(Banner.builder().build()) + .xNative(Native.builder().build())); + + // when + final Result>> result = yeahmobiBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getXNative) + .extracting(Native::getRequest) + .containsExactly("{\"native\":{}}"); } @Test @@ -137,12 +175,12 @@ public void makeHttpRequestsShouldNotNativeRequestIfAlreadyExists() { + "{\"id\":7,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}}"; assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getXNative) .extracting(Native::getRequest) - .containsOnly(expectedNativeRequest); + .containsExactly(expectedNativeRequest); } @Test @@ -154,9 +192,11 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { final Result> result = yeahmobiBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Failed to decode: Unrecognized token"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); assertThat(result.getValue()).isEmpty(); } @@ -204,14 +244,15 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR")); } @Test - public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall(BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build())) + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) .build(), mapper.writeValueAsString( givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); @@ -222,15 +263,14 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws Js // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "EUR")); } @Test - public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException { // given - final HttpCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build())) + final HttpCall httpCall = givenHttpCall(BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build())) .build(), mapper.writeValueAsString( givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); @@ -241,26 +281,26 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "EUR")); } @Test - public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() { + public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException { // given - final HttpCall httpCall = HttpCall - .success(null, HttpResponse.of(204, null, null), null); + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when final Result> result = yeahmobiBidder.makeBids(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(yeahmobiBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "EUR")); } private static BidRequest givenBidRequest( @@ -279,13 +319,14 @@ private static BidRequest givenBidRequest(Function impCustomizer) { return impCustomizer.apply(Imp.builder() .id("123") - .banner(Banner.builder().id("banner_id").build()).ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpYeahmobi.of("pubId", "zoneId"))))) + .banner(Banner.builder().id("banner_id").build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYeahmobi.of("pubId", "zoneId"))))) .build(); } private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("EUR") .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) .build(); @@ -294,7 +335,6 @@ private static BidResponse givenBidResponse(Function givenHttpCall(BidRequest bidRequest, String body) { return HttpCall.success( HttpRequest.builder().payload(bidRequest).build(), - HttpResponse.of(200, null, body), - null); + HttpResponse.of(200, null, body), null); } } diff --git a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java index 2f41265c6b3..ecd64ccf507 100644 --- a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java @@ -27,16 +27,14 @@ import java.math.BigDecimal; import java.time.Instant; +import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; -import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.tuple; @@ -61,9 +59,31 @@ public void creationShouldFailOnInvalidEndpointUrl() { } @Test - public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { + public void makeHttpRequestsShouldReturnErrorIfEndpointUrlComposingFails() { // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpYieldlab.builder() + .adslotId("invalid path") + .build()))) + .build())) + .build(); + + // when + final Result>> result = yieldlabBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Invalid url: https://test.endpoint.com/invalid path"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + }); + } + @Test + public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { + // given final Map targeting = new HashMap<>(); targeting.put("key1", "value1"); targeting.put("key2", "value2"); @@ -106,7 +126,6 @@ public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { .flatExtracting(r -> r.getHeaders().entries()) .extracting(Map.Entry::getKey, Map.Entry::getValue) .containsOnly( - tuple("Content-Type", "application/json;charset=utf-8"), tuple("Accept", "application/json"), tuple("User-Agent", "Agent"), tuple("X-Forwarded-For", "ip"), @@ -115,41 +134,74 @@ public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { } @Test - public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + public void constructExtImpShouldWorkWithDuplicateKeysTargeting() { // given - final HttpCall httpCall = givenHttpCall(null, "invalid"); + final Map targeting = new HashMap<>(); + targeting.put("key1", "value1"); - // when - final Result> result = yieldlabBidder.makeBids(httpCall, null); + final List imps = new ArrayList<>(); + imps.add(Imp.builder() + .banner(Banner.builder().w(1).h(1).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpYieldlab.builder() + .adslotId("1") + .supplyId("2") + .adSize("adSize") + .targeting(targeting) + .extId("extId") + .build()))) + .build()); + imps.add(Imp.builder() + .banner(Banner.builder().w(1).h(1).build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpYieldlab.builder() + .adslotId("2") + .supplyId("2") + .adSize("adSize") + .targeting(targeting) + .extId("extId") + .build()))) + .build()); + final BidRequest bidRequest = BidRequest.builder() + .imp(imps) + .device(Device.builder().ip("ip").ua("Agent").language("fr").devicetype(1).build()) + .regs(Regs.of(1, ExtRegs.of(1, "usPrivacy"))) + .user(User.builder().buyeruid("buyeruid").ext(ExtUser.builder().consent("consent").build()).build()) + .site(Site.builder().page("http://www.example.com").build()) + .build(); + + // when + final Result>> result = yieldlabBidder.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()).hasSize(1); - assertThat(result.getErrors().get(0).getMessage()).startsWith("Unrecognized token 'invalid':"); - assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_server_response); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .allSatisfy(uri -> { + assertThat(uri).startsWith("https://test.endpoint.com/1,2?content=json&pvid=true&ts="); + assertThat(uri).endsWith("&t=key1%3Dvalue1&ids=buyeruid&yl_rtb_ifa&" + + "yl_rtb_devicetype=1&gdpr=1&consent=consent"); + }); } @Test - public void makeBidsShouldReturnEmptyResultWhenResponseWithNoContent() { + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given - final HttpCall httpCall = HttpCall - .success(null, HttpResponse.of(204, null, null), null); + final HttpCall httpCall = givenHttpCall("invalid"); // when final Result> result = yieldlabBidder.makeBids(httpCall, null); // then - assertThat(result.getErrors()).isEmpty(); + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_server_response + && error.getMessage().startsWith("Unrecognized token 'invalid")); assertThat(result.getValue()).isEmpty(); } @Test public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingException { // given - - final Map targeting = new HashMap<>(); - targeting.put("key1", "value1"); - targeting.put("key2", "value2"); final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .id("test-imp-id") @@ -171,7 +223,7 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio final YieldlabResponse yieldlabResponse = YieldlabResponse.of(1, 201d, "yieldlab", "728x90", 1234, 5678, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5"); - final HttpCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(yieldlabResponse)); + final HttpCall httpCall = givenHttpCall(mapper.writeValueAsString(yieldlabResponse)); // when final Result> result = yieldlabBidder.makeBids(httpCall, bidRequest); @@ -179,7 +231,9 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio // then final String timestamp = String.valueOf((int) Instant.now().getEpochSecond()); final int weekNumber = Calendar.getInstance().get(Calendar.WEEK_OF_YEAR); - final String adm = String.format("", timestamp); + final String adm = String.format( + "", + timestamp); final BidderBid expected = BidderBid.of( Bid.builder() .id("1") @@ -193,46 +247,11 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio .build(), BidType.banner, "EUR"); - assertThat(result.getValue().get(0).getBid().getAdm()).isEqualTo(adm); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).doesNotContainNull() - .hasSize(1).element(0).isEqualTo(expected); - } - - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(yieldlabBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - - private static BidRequest givenBidRequest( - Function bidRequestCustomizer, - Function impCustomizer) { - - return bidRequestCustomizer.apply(BidRequest.builder() - .imp(singletonList(givenImp(impCustomizer)))) - .build(); - } - - private static BidRequest givenBidRequest(Function impCustomizer) { - return givenBidRequest(identity(), impCustomizer); - } - - private static Imp givenImp(Function impCustomizer) { - return impCustomizer.apply(Imp.builder() - .id("123") - .banner(Banner.builder().id("banner_id").build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpYieldlab.builder() - .adslotId("1") - .supplyId("2") - .adSize("adSize") - .targeting(singletonMap("key", "value")) - .extId("extId") - .build())))) - .build(); + assertThat(result.getValue()).containsExactly(expected); } - private static HttpCall givenHttpCall(BidRequest bidRequest, String body) { + private static HttpCall givenHttpCall(String body) { return HttpCall.success( HttpRequest.builder().build(), HttpResponse.of(200, null, body), diff --git a/src/test/java/org/prebid/server/bidder/yieldmo/YieldmoBidderTest.java b/src/test/java/org/prebid/server/bidder/yieldmo/YieldmoBidderTest.java index 205bcae706c..48695b219dc 100644 --- a/src/test/java/org/prebid/server/bidder/yieldmo/YieldmoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yieldmo/YieldmoBidderTest.java @@ -4,9 +4,11 @@ import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +import io.netty.handler.codec.http.HttpHeaderValues; import org.junit.Before; import org.junit.Test; import org.prebid.server.VertxTest; @@ -16,23 +18,27 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.yieldmo.proto.YieldmoImpExt; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.yieldmo.ExtImpYieldmo; +import org.prebid.server.util.HttpUtil; import java.util.List; +import java.util.Map; import java.util.function.Function; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; public class YieldmoBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://test.endpoint.com"; - + private static final String PLACEMENT_VALUE = "placementId"; private YieldmoBidder yieldmoBidder; @Before @@ -48,8 +54,8 @@ public void creationShouldFailOnInvalidEndpointUrl() { @Test public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // given - final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder - .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); // when final Result>> result = yieldmoBidder.makeHttpRequests(bidRequest); @@ -60,6 +66,42 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { assertThat(result.getValue()).isEmpty(); } + @Test + public void makeHttpRequestsShouldReturnExtPlacementFromYieldmoPlacement() { + // given + final BidRequest bidRequest = givenBidRequest(Function.identity()); + + // when + final Result>> result = yieldmoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(0); + + final YieldmoImpExt expectedExt = YieldmoImpExt.of(PLACEMENT_VALUE); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsOnly(mapper.valueToTree(expectedExt)); + } + + @Test + public void makeHttpRequestShouldReturnCorrectHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(Function.identity()); + + // when + final Result>> result = yieldmoBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(0); + assertThat(result.getValue()).flatExtracting(httpRequest -> httpRequest.getHeaders().entries()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), + tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString())); + } + @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given @@ -104,7 +146,7 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso } @Test - public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + public void makeBidsShouldReturnVideoBidByDefault() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( BidRequest.builder() @@ -116,6 +158,25 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { // when final Result> result = yieldmoBidder.makeBids(httpCall, null); + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = yieldmoBidder.makeBids(httpCall, null); + // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) @@ -123,8 +184,22 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { } @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(yieldmoBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); + public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = yieldmoBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); } private static BidRequest givenBidRequest( @@ -144,12 +219,13 @@ private static Imp givenImp(Function impCustomiz return impCustomizer.apply(Imp.builder() .id("123") .banner(Banner.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldmo.of("placementId"))))) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldmo.of(PLACEMENT_VALUE))))) .build(); } private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) diff --git a/src/test/java/org/prebid/server/bidder/yieldone/YieldoneBidderTest.java b/src/test/java/org/prebid/server/bidder/yieldone/YieldoneBidderTest.java index 7bdbaa285ae..e0f10224d81 100644 --- a/src/test/java/org/prebid/server/bidder/yieldone/YieldoneBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yieldone/YieldoneBidderTest.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.function.Function; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -57,7 +56,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { .imp(singletonList(Imp.builder() .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(500).build())).build()) .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))).build())) - .build(); + .build(); // when final Result>> result = yieldoneBidder.makeHttpRequests(bidRequest); @@ -119,7 +118,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { final HttpCall httpCall = givenHttpCall("false"); // when - final Result> result = yieldoneBidder.makeBids(httpCall, null); + final Result> result = yieldoneBidder.makeBids(httpCall, givenBidRequest(identity())); // then assertThat(result.getErrors().get(0).getType()).isEqualTo(BidderError.Type.bad_input); @@ -197,15 +196,10 @@ public void makeBidsShouldReturnErrorWithUnknownBidTypeIfNotSupportedBidType() t // then assertThat(result.getErrors()).hasSize(1) - .containsOnly(BidderError.badInput("Failed to find impression 123")); + .containsOnly(BidderError.badInput("Unknown impression type with id 123")); assertThat(result.getValue()).isEmpty(); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(yieldoneBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static BidRequest givenBidRequest( Function bidRequestCustomizer, Function impCustomizer) { @@ -229,6 +223,7 @@ private static Imp givenImp(Function impCustomiz private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) .build(); diff --git a/src/test/java/org/prebid/server/bidder/zeroclickfraud/ZeroclickfraudBidderTest.java b/src/test/java/org/prebid/server/bidder/zeroclickfraud/ZeroclickfraudBidderTest.java index cbc51c50afb..46e16869b67 100644 --- a/src/test/java/org/prebid/server/bidder/zeroclickfraud/ZeroclickfraudBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/zeroclickfraud/ZeroclickfraudBidderTest.java @@ -24,7 +24,6 @@ import java.util.function.Function; import static java.util.Arrays.asList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -285,11 +284,6 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessi .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); } - @Test - public void extractTargetingShouldReturnEmptyMap() { - assertThat(zeroclickfraudBidder.extractTargeting(mapper.createObjectNode())).isEqualTo(emptyMap()); - } - private static BidRequest givenBidRequest(Object extImpDatablocks) { return BidRequest.builder() .imp(singletonList(Imp.builder() diff --git a/src/test/java/org/prebid/server/cache/CacheServiceTest.java b/src/test/java/org/prebid/server/cache/CacheServiceTest.java index c9e89fe1b35..7af0cdcb3a7 100644 --- a/src/test/java/org/prebid/server/cache/CacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CacheServiceTest.java @@ -1,10 +1,11 @@ package org.prebid.server.cache; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; import io.vertx.core.Future; import org.junit.Before; import org.junit.Rule; @@ -15,14 +16,13 @@ import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidInfo; import org.prebid.server.cache.model.CacheContext; import org.prebid.server.cache.model.CacheHttpRequest; -import org.prebid.server.cache.model.CacheIdInfo; +import org.prebid.server.cache.model.CacheInfo; import org.prebid.server.cache.model.CacheServiceResult; import org.prebid.server.cache.model.CacheTtl; import org.prebid.server.cache.model.DebugHttpCall; -import org.prebid.server.cache.proto.BidCacheResult; -import org.prebid.server.cache.proto.request.BannerValue; import org.prebid.server.cache.proto.request.BidCacheRequest; import org.prebid.server.cache.proto.request.PutObject; import org.prebid.server.cache.proto.response.BidCacheResponse; @@ -33,9 +33,12 @@ import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.metric.Metrics; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.MediaType; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.vast.VastModifier; import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; @@ -46,16 +49,17 @@ import java.time.Instant; import java.time.ZoneId; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.concurrent.TimeoutException; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -63,7 +67,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.times; @@ -82,6 +85,8 @@ public class CacheServiceTest extends VertxTest { @Mock private EventsService eventsService; @Mock + private VastModifier vastModifier; + @Mock private Metrics metrics; private Clock clock; @@ -103,12 +108,13 @@ public void setUp() throws MalformedURLException, JsonProcessingException { httpClient, new URL("http://cache-service/cache"), "http://cache-service-host/cache?uuid=", + vastModifier, eventsService, metrics, clock, jacksonMapper); - eventsContext = EventsContext.builder().build(); + eventsContext = EventsContext.builder().auctionId("auctionId").build(); final TimeoutFactory timeoutFactory = new TimeoutFactory(clock); timeout = timeoutFactory.create(500L); @@ -164,190 +170,6 @@ public void getCachedAssetURLShouldReturnExpectedValue() { assertThat(cachedAssetURL).isEqualTo("http://cache-service-host/cache?uuid="); } - @Test - public void cacheBidsShouldNeverCallCacheServiceIfNoBidsPassed() { - // when - final List result = cacheService.cacheBids(emptyList(), timeout, "accountId").result(); - - // then - verifyZeroInteractions(httpClient); - assertThat(result).isEqualTo(emptyList()); - } - - @Test - public void cacheBidsShouldPerformHttpRequestWithExpectedTimeout() { - // when - cacheService.cacheBids(singleBidList(), timeout, "accountId"); - - // then - verify(httpClient).post(anyString(), any(), any(), eq(500L)); - } - - @Test - public void cacheBidsShouldFailIfGlobalTimeoutAlreadyExpired() { - // when - final Future future = cacheService.cacheBids(singleBidList(), expiredTimeout, "accountId"); - - // then - assertThat(future.failed()).isTrue(); - assertThat(future.cause()).isInstanceOf(TimeoutException.class); - verifyZeroInteractions(httpClient); - } - - @Test - public void cacheBidsShouldFailIfReadingHttpResponseFails() { - // given - givenHttpClientProducesException(new RuntimeException("Response exception")); - - // when - final Future future = cacheService.cacheBids(singleBidList(), timeout, "accountId"); - - // then - assertThat(future.failed()).isTrue(); - assertThat(future.cause()).isInstanceOf(RuntimeException.class) - .hasMessage("Response exception"); - } - - @Test - public void cacheBidsShouldFailIfResponseCodeIsNot200() { - // given - givenHttpClientReturnsResponse(503, "response"); - - // when - final Future future = cacheService.cacheBids(singleBidList(), timeout, "accountId"); - - // then - verify(metrics).updateCacheRequestFailedTime(eq("accountId"), anyLong()); - - assertThat(future.failed()).isTrue(); - assertThat(future.cause()).isInstanceOf(PreBidException.class) - .hasMessage("HTTP status code 503"); - } - - @Test - public void cacheBidsShouldFailIfResponseBodyCouldNotBeParsed() { - // given - givenHttpClientReturnsResponse(200, "response"); - - // when - final Future future = cacheService.cacheBids(singleBidList(), timeout, "accountId"); - - // then - verify(metrics).updateCacheRequestFailedTime(eq("accountId"), anyLong()); - - assertThat(future.failed()).isTrue(); - assertThat(future.cause()).isInstanceOf(PreBidException.class); - } - - @Test - public void cacheBidsShouldFailIfCacheEntriesNumberDoesNotMatchBidsNumber() { - // given - givenHttpClientReturnsResponse(200, "{}"); - - // when - final Future future = cacheService.cacheBids(singleBidList(), timeout, "accountId"); - - // then - assertThat(future.failed()).isTrue(); - assertThat(future.cause()).isInstanceOf(PreBidException.class) - .hasMessage("The number of response cache objects doesn't match with bids"); - } - - @Test - public void cacheBidsShouldMakeHttpRequestUsingConfigurationParams() throws MalformedURLException { - // given - cacheService = new CacheService( - mediaTypeCacheTtl, - httpClient, - new URL("https://cache-service-host:8888/cache"), - "https://cache-service-host:8080/cache?uuid=", - eventsService, - metrics, - clock, - jacksonMapper); - - // when - cacheService.cacheBids(singleBidList(), timeout, "accountId"); - - // then - verify(httpClient).post(eq("https://cache-service-host:8888/cache"), any(), any(), anyLong()); - } - - @Test - public void cacheBidsShouldPerformHttpRequestWithExpectedBody() throws Exception { - // given - final String adm3 = ""; - final String adm4 = "\"\""; - - // when - cacheService.cacheBids(asList( - givenBid(builder -> builder.adm("adm1").nurl("nurl1").height(100).width(200)), - givenBid(builder -> builder.adm("adm2").nurl("nurl2").height(300).width(400)), - givenBid(builder -> builder.adm(adm3).mediaType(MediaType.video)), - givenBid(builder -> builder.adm(adm4).mediaType(MediaType.video))), - timeout, - "accountId"); - - // then - verify(metrics, times(2)).updateCacheCreativeSize(eq("accountId"), eq(4)); - verify(metrics, times(1)).updateCacheCreativeSize(eq("accountId"), eq(103)); - verify(metrics, times(1)).updateCacheCreativeSize(eq("accountId"), eq(118)); - - final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); - assertThat(bidCacheRequest.getPuts()).hasSize(4) - .containsOnly( - PutObject.builder().type("json").value( - mapper.valueToTree(BannerValue.of("adm1", "nurl1", 200, 100))).build(), - PutObject.builder().type("json").value( - mapper.valueToTree(BannerValue.of("adm2", "nurl2", 400, 300))).build(), - PutObject.builder().type("xml").value(new TextNode(adm3)).build(), - PutObject.builder().type("xml").value(new TextNode(adm4)).build()); - } - - @Test - public void cacheBidsShouldReturnExpectedResult() { - // given and when - final Future> future = cacheService.cacheBids(singleBidList(), timeout, "accountId"); - - // then - verify(metrics).updateCacheRequestSuccessTime(eq("accountId"), anyLong()); - - final List bidCacheResults = future.result(); - assertThat(bidCacheResults).hasSize(1) - .containsOnly(BidCacheResult.of("uuid1", "http://cache-service-host/cache?uuid=uuid1")); - } - - @Test - public void cacheBidsVideoOnlyShouldPerformHttpRequestWithExpectedBody() throws IOException { - // when - cacheService.cacheBidsVideoOnly(asList( - givenBid(builder -> builder.mediaType(MediaType.banner).adm("adm1")), - givenBid(builder -> builder.mediaType(MediaType.video).adm("adm2"))), - timeout, - "accountId"); - - // then - verify(metrics, times(1)).updateCacheCreativeSize(eq("accountId"), eq(4)); - - final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); - assertThat(bidCacheRequest.getPuts()).hasSize(1) - .containsOnly(PutObject.builder().type("xml").value(new TextNode("adm2")).build()); - } - - @Test - public void cacheBidsVideoOnlyShouldReturnExpectedResult() { - // given and when - final Future> future = cacheService.cacheBidsVideoOnly( - singletonList(givenBid(builder -> builder.mediaType(MediaType.video))), timeout, "accountId"); - - // then - final List bidCacheResults = future.result(); - assertThat(bidCacheResults).hasSize(1) - .containsOnly(BidCacheResult.of("uuid1", "http://cache-service-host/cache?uuid=uuid1")); - } - @Test public void cacheBidsOpenrtbShouldNeverCallCacheServiceIfNoBidsPassed() { // when @@ -361,11 +183,10 @@ public void cacheBidsOpenrtbShouldNeverCallCacheServiceIfNoBidsPassed() { public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedTimeout() { // when cacheService.cacheBidsOpenrtb( - singletonList(givenBidOpenrtb(identity())), + singletonList(givenBidInfo(identity())), givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) .build(), eventsContext); @@ -377,11 +198,10 @@ public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedTimeout() { public void cacheBidsOpenrtbShouldTolerateGlobalTimeoutAlreadyExpired() { // when final Future future = cacheService.cacheBidsOpenrtb( - singletonList(givenBidOpenrtb(identity())), + singletonList(givenBidInfo(identity())), givenAuctionContext().toBuilder().timeout(expiredTimeout).build(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) .build(), eventsContext); @@ -395,107 +215,124 @@ public void cacheBidsOpenrtbShouldTolerateGlobalTimeoutAlreadyExpired() { @Test public void cacheBidsOpenrtbShouldStoreWinUrl() { // given - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); - + final EventsContext eventsContext = EventsContext.builder() + .auctionId("auctionId") + .enabledForAccount(true) + .enabledForRequest(true) + .build(); // when cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder - .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), + singletonList(givenBidInfo(builder -> builder.id("bidId1"), BidType.banner, "bidder", + "lineItemId")), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) .build(), - EventsContext.builder().enabledForAccount(true).enabledForRequest(true).build()); + eventsContext); // then - verify(eventsService).winUrl(eq("bidId1"), eq("bidder"), eq("accountId"), isNull(), isNull()); + verify(eventsService).winUrl(eq("bidId1"), eq("bidder"), eq("accountId"), eq("lineItemId"), eq(true), + eq(EventsContext.builder().enabledForAccount(true).enabledForRequest(true) + .auctionId("auctionId").build())); } @Test public void cacheBidsOpenrtbShouldTolerateReadingHttpResponseFails() throws JsonProcessingException { // given givenHttpClientProducesException(new RuntimeException("Response exception")); - - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); + final BidInfo bidinfo = givenBidInfo(builder -> builder.id("bidId1")); // when final Future future = cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder - .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), + singletonList(bidinfo), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) .build(), eventsContext); // then + verify(metrics).updateCacheRequestFailedTime(eq("accountId"), anyLong()); + final CacheServiceResult result = future.result(); - final CacheHttpRequest request = givenCacheHttpRequest(bid); assertThat(result.getCacheBids()).isEmpty(); assertThat(result.getError()).isInstanceOf(RuntimeException.class).hasMessage("Response exception"); - assertThat(result.getHttpCall()).isNotNull() - .isEqualTo(DebugHttpCall.builder().requestUri(request.getUri()).requestBody(request.getBody()) - .endpoint("http://cache-service/cache").responseTimeMillis(0).build()); + + final CacheHttpRequest request = givenCacheHttpRequest(bidinfo.getBid()); + assertThat(result.getHttpCall()) + .isEqualTo(DebugHttpCall.builder() + .requestHeaders(givenDebugHeaders()) + .endpoint("http://cache-service/cache") + .requestBody(request.getBody()) + .requestUri(request.getUri()) + .responseTimeMillis(0) + .build()); } @Test public void cacheBidsOpenrtbShouldTolerateResponseCodeIsNot200() throws JsonProcessingException { // given givenHttpClientReturnsResponse(503, "response"); - - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); + final BidInfo bidinfo = givenBidInfo(builder -> builder.id("bidId1")); // when final Future future = cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder - .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), + singletonList(bidinfo), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) .build(), eventsContext); // then final CacheServiceResult result = future.result(); - final CacheHttpRequest request = givenCacheHttpRequest(bid); assertThat(result.getCacheBids()).isEmpty(); assertThat(result.getError()).isInstanceOf(PreBidException.class).hasMessage("HTTP status code 503"); - assertThat(result.getHttpCall()).isNotNull() - .isEqualTo(DebugHttpCall.builder().endpoint("http://cache-service/cache") - .requestBody(request.getBody()).requestUri(request.getUri()).responseStatus(503) - .responseBody("response").responseTimeMillis(0).build()); + + final CacheHttpRequest request = givenCacheHttpRequest(bidinfo.getBid()); + assertThat(result.getHttpCall()) + .isEqualTo(DebugHttpCall.builder() + .endpoint("http://cache-service/cache") + .requestBody(request.getBody()) + .requestUri(request.getUri()) + .responseStatus(503) + .requestHeaders(givenDebugHeaders()) + .responseBody("response") + .responseTimeMillis(0) + .build()); } @Test public void cacheBidsOpenrtbShouldTolerateResponseBodyCouldNotBeParsed() throws JsonProcessingException { // given givenHttpClientReturnsResponse(200, "response"); - - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); + final BidInfo bidinfo = givenBidInfo(builder -> builder.id("bidId1")); // when final Future future = cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder - .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), + singletonList(bidinfo), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) .build(), eventsContext); // then final CacheServiceResult result = future.result(); - final CacheHttpRequest request = givenCacheHttpRequest(bid); assertThat(result.getCacheBids()).isEmpty(); assertThat(result.getError()).isInstanceOf(PreBidException.class).hasMessage("Cannot parse response: response"); - assertThat(result.getHttpCall()).isNotNull() - .isEqualTo(DebugHttpCall.builder().endpoint("http://cache-service/cache") - .requestUri(request.getUri()).requestBody(request.getBody()) - .responseStatus(200).responseBody("response").responseTimeMillis(0).build()); + + final CacheHttpRequest request = givenCacheHttpRequest(bidinfo.getBid()); + assertThat(result.getHttpCall()) + .isEqualTo(DebugHttpCall.builder() + .endpoint("http://cache-service/cache") + .requestUri(request.getUri()) + .requestBody(request.getBody()) + .requestHeaders(givenDebugHeaders()) + .responseStatus(200) + .responseBody("response") + .responseTimeMillis(0) + .build()); } @Test @@ -503,146 +340,168 @@ public void cacheBidsOpenrtbShouldTolerateCacheEntriesNumberDoesNotMatchBidsNumb throws JsonProcessingException { // given givenHttpClientReturnsResponse(200, "{}"); - - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); + final BidInfo bidinfo = givenBidInfo(builder -> builder.id("bidId1")); // when final Future future = cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder - .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), + singletonList(bidinfo), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) .build(), eventsContext); // then final CacheServiceResult result = future.result(); - final CacheHttpRequest request = givenCacheHttpRequest(bid); assertThat(result.getCacheBids()).isEmpty(); - assertThat(result.getError()).isNotNull().isInstanceOf(PreBidException.class) + assertThat(result.getError()).isInstanceOf(PreBidException.class) .hasMessage("The number of response cache objects doesn't match with bids"); + + final CacheHttpRequest request = givenCacheHttpRequest(bidinfo.getBid()); assertThat(result.getHttpCall()).isNotNull() - .isEqualTo(DebugHttpCall.builder().endpoint("http://cache-service/cache") - .requestBody(request.getBody()).requestUri(request.getUri()) - .responseStatus(200).responseBody("{}").responseTimeMillis(0).build()); + .isEqualTo(DebugHttpCall.builder() + .endpoint("http://cache-service/cache") + .requestBody(request.getBody()) + .requestUri(request.getUri()) + .requestHeaders(givenDebugHeaders()) + .responseStatus(200).responseBody("{}") + .responseTimeMillis(0) + .build()); } @Test public void cacheBidsOpenrtbShouldReturnExpectedDebugInfo() throws JsonProcessingException { // given - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); + final BidInfo bidinfo = givenBidInfo(builder -> builder.id("bidId1")); // when final Future future = cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder - .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), + singletonList(bidinfo), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) .build(), eventsContext); // then final CacheServiceResult result = future.result(); - final CacheHttpRequest request = givenCacheHttpRequest(bid); - assertThat(result.getHttpCall()).isNotNull() - .isEqualTo(DebugHttpCall.builder().endpoint("http://cache-service/cache") - .requestUri(request.getUri()).requestBody(request.getBody()) - .responseStatus(200).responseBody("{\"responses\":[{\"uuid\":\"uuid1\"}]}") - .responseTimeMillis(0).build()); + final CacheHttpRequest request = givenCacheHttpRequest(bidinfo.getBid()); + assertThat(result.getHttpCall()) + .isEqualTo(DebugHttpCall.builder() + .endpoint("http://cache-service/cache") + .requestUri(request.getUri()) + .requestBody(request.getBody()) + .requestHeaders(givenDebugHeaders()) + .responseStatus(200) + .responseBody("{\"responses\":[{\"uuid\":\"uuid1\"}]}") + .responseTimeMillis(0) + .build()); } @Test public void cacheBidsOpenrtbShouldReturnExpectedCacheBids() { // given - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.id("bidId1").impid("impId1")); + final BidInfo bidinfo = givenBidInfo(builder -> builder.id("bidId1")); // when final Future future = cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder - .imp(singletonList(givenImp(builder -> builder.id("impId1"))))), + singletonList(bidinfo), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder", singletonList("bidId1"))) .build(), eventsContext); // then final CacheServiceResult result = future.result(); assertThat(result.getCacheBids()).hasSize(1) - .containsEntry(bid, CacheIdInfo.of("uuid1", null)); + .containsEntry(bidinfo.getBid(), CacheInfo.of("uuid1", null, null, null)); } @Test public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedBody() throws IOException { // given - final com.iab.openrtb.response.Bid bid1 = givenBidOpenrtb(builder -> builder.id("bid1").impid("impId1")); - final com.iab.openrtb.response.Bid bid2 = givenBidOpenrtb(builder -> builder.id("bid2").impid("impId2") - .adm("adm2")); - final Imp imp1 = givenImp(identity()); - final Imp imp2 = givenImp(builder -> builder.id("impId2").video(Video.builder().build())); + final ObjectNode bidExt2 = mapper.valueToTree( + ExtPrebid.of(ExtBidPrebid.builder().bidid("generatedId").build(), + emptyMap())); + final String receivedBid2Adm = "adm2"; + final BidInfo bidInfo1 = givenBidInfo(builder -> builder.id("bidId1"), BidType.banner, "bidder1"); + final BidInfo bidInfo2 = givenBidInfo(builder -> builder.id("bidId2").adm(receivedBid2Adm).ext(bidExt2), + BidType.video, "bidder2"); + + final EventsContext eventsContext = EventsContext.builder() + .auctionId("auctionId") + .auctionTimestamp(1000L) + .build(); // when cacheService.cacheBidsOpenrtb( - asList(bid1, bid2), - givenAuctionContext( - accountBuilder -> accountBuilder.id("accountId"), - bidRequestBuilder -> bidRequestBuilder - .imp(asList(imp1, imp2))), + asList(bidInfo1, bidInfo2), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder2", singletonList("bid2"))) - .bidderToBidIds(singletonMap("bidder1", asList("bid1", "bid2"))) .build(), - EventsContext.builder().auctionTimestamp(1000L).build()); + eventsContext); // then + // Second value is adm length for each verify(metrics, times(1)).updateCacheCreativeSize(eq("accountId"), eq(0)); verify(metrics, times(2)).updateCacheCreativeSize(eq("accountId"), eq(4)); + final Bid bid1 = bidInfo1.getBid(); + final Bid bid2 = bidInfo2.getBid(); + final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); assertThat(bidCacheRequest.getPuts()).hasSize(3) .containsOnly( - PutObject.builder().type("json").value(mapper.valueToTree(bid1)).build(), - PutObject.builder().type("json").value(mapper.valueToTree(bid2)).build(), - PutObject.builder().type("xml").value(new TextNode(bid2.getAdm())).build()); + PutObject.builder().aid("auctionId").type("json").value(mapper.valueToTree(bid1)).build(), + PutObject.builder().aid("auctionId").type("json").value(mapper.valueToTree(bid2)).build(), + PutObject.builder().aid("auctionId").type("xml").value(new TextNode(receivedBid2Adm)).build()); } @Test - public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlFromBid() throws IOException { + public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() throws IOException { + // given + final BidInfo bidInfo = givenBidInfo( + bidBuilder -> bidBuilder.id("bidId1").exp(10), + impBuilder -> impBuilder.id("impId1").exp(20)); + // when - cacheService.cacheBidsOpenrtb( - singletonList(givenBidOpenrtb(builder -> builder.impid("impId1").exp(10))), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder - .imp(singletonList(givenImp(buider -> buider.id("impId1").exp(20))))), + final Future future = cacheService.cacheBidsOpenrtb( + singletonList(bidInfo), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) + .cacheBidsTtl(30) .build(), eventsContext); // then final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); assertThat(bidCacheRequest.getPuts()).hasSize(1) - .extracting(PutObject::getExpiry) + .extracting(PutObject::getTtlseconds) .containsOnly(10); + + final CacheServiceResult result = future.result(); + assertThat(result.getCacheBids().values()) + .flatExtracting(CacheInfo::getTtl) + .containsExactly(10); } @Test - public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlFromImp() throws IOException { + public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() throws IOException { + // given + final BidInfo bidInfo = givenBidInfo( + bidBuilder -> bidBuilder.id("bidId1"), + impBuilder -> impBuilder.id("impId1").exp(10)); + // when - cacheService.cacheBidsOpenrtb( - singletonList(givenBidOpenrtb(identity())), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder - .imp(singletonList(givenImp(buider -> buider.exp(10))))), + final Future future = cacheService.cacheBidsOpenrtb( + singletonList(bidInfo), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) .cacheBidsTtl(20) .build(), eventsContext); @@ -650,19 +509,23 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlFromImp() throw // then final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); assertThat(bidCacheRequest.getPuts()).hasSize(1) - .extracting(PutObject::getExpiry) + .extracting(PutObject::getTtlseconds) .containsOnly(10); + + final CacheServiceResult result = future.result(); + assertThat(result.getCacheBids().values()) + .flatExtracting(CacheInfo::getTtl) + .containsExactly(10); } @Test - public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlFromRequest() throws IOException { + public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() throws IOException { // when - cacheService.cacheBidsOpenrtb( - singletonList(givenBidOpenrtb(identity())), + final Future future = cacheService.cacheBidsOpenrtb( + singletonList(givenBidInfo(bidBuilder -> bidBuilder.id("bidId1"))), givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) .cacheBidsTtl(10) .build(), eventsContext); @@ -670,70 +533,88 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlFromRequest() t // then final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); assertThat(bidCacheRequest.getPuts()).hasSize(1) - .extracting(PutObject::getExpiry) + .extracting(PutObject::getTtlseconds) .containsOnly(10); + + final CacheServiceResult result = future.result(); + assertThat(result.getCacheBids().values()) + .flatExtracting(CacheInfo::getTtl) + .containsExactly(10); } @Test - public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlFromAccountBannerTtl() throws IOException { + public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBannerTtl() + throws IOException { // given cacheService = new CacheService( CacheTtl.of(20, null), httpClient, new URL("http://cache-service/cache"), "http://cache-service-host/cache?uuid=", + vastModifier, eventsService, metrics, clock, jacksonMapper); // when - cacheService.cacheBidsOpenrtb( - singletonList(givenBidOpenrtb(identity())), + final Future future = cacheService.cacheBidsOpenrtb( + singletonList(givenBidInfo(bidBuilder -> bidBuilder.id("bidId1"))), givenAuctionContext( - accountBuilder -> accountBuilder.bannerCacheTtl(10), + accountBuilder -> accountBuilder.auction(AccountAuctionConfig.builder() + .bannerCacheTtl(10) + .build()), identity()), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) .build(), eventsContext); // then final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); assertThat(bidCacheRequest.getPuts()).hasSize(1) - .extracting(PutObject::getExpiry) + .extracting(PutObject::getTtlseconds) .containsOnly(10); + + final CacheServiceResult result = future.result(); + assertThat(result.getCacheBids().values()) + .flatExtracting(CacheInfo::getTtl) + .containsExactly(10); } @Test - public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlFromMediaTypeTtl() throws IOException { + public void cacheBidsOpenrtbShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl() throws IOException { // given cacheService = new CacheService( CacheTtl.of(10, null), httpClient, new URL("http://cache-service/cache"), "http://cache-service-host/cache?uuid=", + vastModifier, eventsService, metrics, clock, jacksonMapper); // when - cacheService.cacheBidsOpenrtb( - singletonList(givenBidOpenrtb(identity())), + final Future future = cacheService.cacheBidsOpenrtb( + singletonList(givenBidInfo(bidBuilder -> bidBuilder.id("bidId1"))), givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) .build(), eventsContext); // then final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); assertThat(bidCacheRequest.getPuts()).hasSize(1) - .extracting(PutObject::getExpiry) + .extracting(PutObject::getTtlseconds) .containsOnly(10); + + final CacheServiceResult result = future.result(); + assertThat(result.getCacheBids().values()) + .flatExtracting(CacheInfo::getTtl) + .containsExactly(10); } @Test @@ -744,37 +625,41 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithTtlFromMediaTypeWhenAccoun httpClient, new URL("http://cache-service/cache"), "http://cache-service-host/cache?uuid=", + vastModifier, eventsService, metrics, clock, jacksonMapper); // when - cacheService.cacheBidsOpenrtb( - singletonList(givenBidOpenrtb(identity())), + final Future future = cacheService.cacheBidsOpenrtb( + singletonList(givenBidInfo(bidBuilder -> bidBuilder.id("bidId1"))), givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) .build(), eventsContext); // then final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); assertThat(bidCacheRequest.getPuts()).hasSize(1) - .extracting(PutObject::getExpiry) + .extracting(PutObject::getTtlseconds) .containsOnly(10); + + final CacheServiceResult result = future.result(); + assertThat(result.getCacheBids().values()) + .flatExtracting(CacheInfo::getTtl) + .containsExactly(10); } @Test - public void cacheBidsOpenrtbShouldSendCacheRequestWithNoTtl() throws IOException { + public void cacheBidsOpenrtbShouldSendCacheRequestWithNoTtlAndSetEmptyTtl() throws IOException { // when - cacheService.cacheBidsOpenrtb( - singletonList(givenBidOpenrtb(identity())), + final Future future = cacheService.cacheBidsOpenrtb( + singletonList(givenBidInfo(bidBuilder -> bidBuilder.id("bidId1"))), givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) .build(), eventsContext); @@ -783,319 +668,87 @@ public void cacheBidsOpenrtbShouldSendCacheRequestWithNoTtl() throws IOException assertThat(bidCacheRequest.getPuts()).hasSize(1) .extracting(PutObject::getExpiry) .containsNull(); + + final CacheServiceResult result = future.result(); + assertThat(result.getCacheBids().values()) + .flatExtracting(CacheInfo::getTtl) + .containsNull(); } @Test public void cacheBidsOpenrtbShouldReturnExpectedResultForBids() { // given - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(identity()); + final BidInfo bidInfo = givenBidInfo(bidBuilder -> bidBuilder.id("bidId1")); // when final Future future = cacheService.cacheBidsOpenrtb( - singletonList(bid), + singletonList(bidInfo), givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) .build(), eventsContext); // then assertThat(future.result().getCacheBids()).hasSize(1) - .containsEntry(bid, CacheIdInfo.of("uuid1", null)); + .containsEntry(bidInfo.getBid(), CacheInfo.of("uuid1", null, null, null)); } @Test public void cacheBidsOpenrtbShouldReturnExpectedResultForVideoBids() { // given - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder.impid("impId1")); - final Imp imp = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); + final BidInfo bidInfo = givenBidInfo(bidBuilder -> bidBuilder.id("bidId1").adm("adm1"), BidType.video, + "bidder"); // when final Future future = cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(singletonList(imp))), + singletonList(bidInfo), + givenAuctionContext(), CacheContext.builder() .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder1", singletonList("bidId1"))) .build(), eventsContext); // then assertThat(future.result().getCacheBids()).hasSize(1) - .containsEntry(bid, CacheIdInfo.of(null, "uuid1")); + .containsEntry(bidInfo.getBid(), CacheInfo.of(null, "uuid1", null, null)); } @Test public void cacheBidsOpenrtbShouldReturnExpectedResultForBidsAndVideoBids() throws JsonProcessingException { // given givenHttpClientReturnsResponse(200, mapper.writeValueAsString( - BidCacheResponse.of(asList(CacheObject.of("uuid1"), CacheObject.of("uuid2"), - CacheObject.of("videoUuid1"), CacheObject.of("videoUuid2"))))); + BidCacheResponse.of(asList( + CacheObject.of("uuid1"), + CacheObject.of("uuid2"), + CacheObject.of("videoUuid1"))))); - final com.iab.openrtb.response.Bid bid1 = givenBidOpenrtb(builder -> builder.impid("impId1")); - final com.iab.openrtb.response.Bid bid2 = givenBidOpenrtb(builder -> builder.impid("impId2")); - final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); - final Imp imp2 = givenImp(builder -> builder.id("impId2").video(Video.builder().build())); + final BidInfo bidInfo1 = givenBidInfo(builder -> builder.id("bidId1"), BidType.video, "bidder1"); + final BidInfo bidInfo2 = givenBidInfo(builder -> builder.id("bidId2"), BidType.banner, "bidder2"); // when final Future future = cacheService.cacheBidsOpenrtb( - asList(bid1, bid2), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(asList(imp1, imp2))), + asList(bidInfo1, bidInfo2), + givenAuctionContext(), CacheContext.builder() .shouldCacheBids(true) .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder1", singletonList("bidId1"))) - .bidderToBidIds(singletonMap("bidder2", singletonList("bidId2"))) .build(), eventsContext); // then assertThat(future.result().getCacheBids()).hasSize(2) .containsOnly( - entry(bid1, CacheIdInfo.of("uuid1", "videoUuid1")), - entry(bid2, CacheIdInfo.of("uuid2", "videoUuid2"))); - } - - @Test - public void cacheBidsOpenrtbShouldNotCacheVideoBidWithMissingImpId() { - // given - final com.iab.openrtb.response.Bid bid1 = givenBidOpenrtb(builder -> builder.impid("impId1")); - final com.iab.openrtb.response.Bid bid2 = givenBidOpenrtb(builder -> builder.impid("impId2")); - final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); - final Imp imp2 = givenImp(builder -> builder.id(null).video(Video.builder().build())); - - // when - final Future future = cacheService.cacheBidsOpenrtb( - asList(bid1, bid2), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(asList(imp1, imp2))), - CacheContext.builder() - .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder1", singletonList("bidId1"))) - .build(), - eventsContext); - - // then - assertThat(future.result().getCacheBids()).hasSize(1) - .containsEntry(bid1, CacheIdInfo.of(null, "uuid1")); - } - - @Test - public void cacheBidsOpenrtbShouldWrapEmptyAdmFieldUsingNurlFieldValue() throws IOException { - // given - final com.iab.openrtb.response.Bid bid1 = givenBidOpenrtb(builder -> builder.id("bid1").impid("impId1") - .adm("adm1")); - final com.iab.openrtb.response.Bid bid2 = givenBidOpenrtb(builder -> builder.id("bid2").impid("impId1") - .nurl("adm2")); - final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); - - // when - cacheService.cacheBidsOpenrtb( - asList(bid1, bid2), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(singletonList(imp1))), - CacheContext.builder() - .shouldCacheBids(true) - .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder1", singletonList("bid1"))) - .bidderToBidIds(singletonMap("bidder1", asList("bid1", "bid2"))) - .build(), - eventsContext); - - // then - final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); - assertThat(bidCacheRequest.getPuts()).hasSize(4) - .containsOnly( - PutObject.builder().type("json").value(mapper.valueToTree(bid1)).build(), - PutObject.builder().type("json").value(mapper.valueToTree(bid2)).build(), - PutObject.builder().type("xml").value(new TextNode("adm1")).build(), - PutObject.builder().type("xml").value(new TextNode( - "prebid.org wrapper" - + "" - + "")) - .build()); - } - - @Test - public void cacheBidsOpenrtbShouldNotModifyVastXmlWhenBidIdIsNotInToModifyList() throws IOException { - // given - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> - builder.id("bid1").impid("impId1").adm("adm")); - final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); - - // when - cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(singletonList(imp1))), - CacheContext.builder() - .shouldCacheBids(true) - .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder", singletonList("bid2"))) - .bidderToBidIds(singletonMap("bidder", singletonList("bid1"))) - .build(), - eventsContext); - - // then - final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); - assertThat(bidCacheRequest.getPuts()).hasSize(2) - .containsOnly( - PutObject.builder().type("json").value(mapper.valueToTree(bid)).build(), - PutObject.builder().type("xml").value(new TextNode("adm")).build()); - } - - @Test - public void cacheBidsOpenrtbShouldNotAddTrackingImpToBidAdmWhenXmlDoesNotContainImpTag() throws IOException { - // given - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> - builder.id("bid1").impid("impId1").adm("no impression tag")); - final Imp imp1 = givenImp(builder -> builder.id("impId1").video(Video.builder().build())); - - // when - cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(singletonList(imp1))), - CacheContext.builder() - .shouldCacheBids(true) - .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder", singletonList("bid2"))) - .bidderToBidIds(singletonMap("bidder", singletonList("bid1"))) - .build(), - eventsContext); - - // then - final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); - assertThat(bidCacheRequest.getPuts()).hasSize(2) - .containsOnly( - PutObject.builder().type("json").value(mapper.valueToTree(bid)).build(), - PutObject.builder().type("xml").value(new TextNode("no impression tag")).build()); - } - - @Test - public void cacheBidsOpenrtbShouldAddTrackingLinkToImpTagWhenItIsEmpty() throws IOException { - // given - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder - .id("bid1") - .impid("impId1") - .adm("")); - final Imp imp1 = givenImp(builder -> builder - .id("impId1") - .video(Video.builder().build())); - - given(eventsService.vastUrlTracking(anyString(), anyString(), any(), any(), any())) - .willReturn("https://test-event.com/event?t=imp&b=bid1&f=b&a=accountId"); - - // when - cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(singletonList(imp1))), - CacheContext.builder() - .shouldCacheBids(true) - .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder", singletonList("bid1"))) - .bidderToBidIds(singletonMap("bidder", singletonList("bid1"))) - .build(), - EventsContext.builder().enabledForAccount(true).enabledForRequest(false).build()); - - // then - final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); - assertThat(bidCacheRequest.getPuts()).hasSize(2) - .containsOnly( - PutObject.builder() - .type("json") - .value(mapper.valueToTree(bid)) - .build(), - PutObject.builder() - .type("xml") - .value(new TextNode("")) - .build()); - } - - @Test - public void cacheBidsOpenrtbShouldAddTrackingImpToBidAdmXmlWhenThatBidShouldBeModifiedAndContainsImpTag() - throws IOException { - // given - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder - .id("bid1") - .impid("impId1") - .adm("http:/test.com")); - final Imp imp1 = givenImp(builder -> builder - .id("impId1") - .video(Video.builder().build())); - - given(eventsService.vastUrlTracking(any(), any(), any(), any(), any())) - .willReturn("https://test-event.com/event?t=imp&b=bid1&f=b&a=accountId"); - - // when - cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(singletonList(imp1))), - CacheContext.builder() - .shouldCacheBids(true) - .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder", singletonList("bid1"))) - .bidderToBidIds(singletonMap("bidder", singletonList("bid1"))) - .build(), - EventsContext.builder().enabledForAccount(true).enabledForRequest(false).build()); - - // then - final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); - assertThat(bidCacheRequest.getPuts()).hasSize(2) - .containsOnly( - PutObject.builder() - .type("json") - .value(mapper.valueToTree(bid)) - .build(), - PutObject.builder() - .type("xml") - .value(new TextNode("http:/test.com" - + "" - + "")) - .build()); - } - - @Test - public void cacheBidsOpenrtbShouldNotAddTrackingImpWhenEventsNotEnabled() throws IOException { - // given - final com.iab.openrtb.response.Bid bid = givenBidOpenrtb(builder -> builder - .id("bid1") - .impid("impId1") - .adm("http:/test.com")); - final Imp imp1 = givenImp(builder -> builder - .id("impId1") - .video(Video.builder().build())); - - // when - cacheService.cacheBidsOpenrtb( - singletonList(bid), - givenAuctionContext(bidRequestBuilder -> bidRequestBuilder.imp(singletonList(imp1))), - CacheContext.builder() - .shouldCacheBids(true) - .shouldCacheVideoBids(true) - .bidderToVideoBidIdsToModify(singletonMap("bidder", singletonList("bid1"))) - .bidderToBidIds(singletonMap("bidder", singletonList("bid1"))) - .build(), - EventsContext.builder().enabledForAccount(false).build()); - - // then - final BidCacheRequest bidCacheRequest = captureBidCacheRequest(); - assertThat(bidCacheRequest.getPuts()).hasSize(2) - .containsOnly( - PutObject.builder() - .type("json") - .value(mapper.valueToTree(bid)) - .build(), - PutObject.builder() - .type("xml") - .value(new TextNode("http:/test.com")) - .build()); - verifyZeroInteractions(eventsService); + entry(bidInfo1.getBid(), CacheInfo.of("uuid1", "videoUuid1", null, null)), + entry(bidInfo2.getBid(), CacheInfo.of("uuid2", null, null, null))); } @Test public void cachePutObjectsShouldTolerateGlobalTimeoutAlreadyExpired() { // when final Future future = cacheService.cachePutObjects( - singletonList(PutObject.builder().build()), emptySet(), "", "", expiredTimeout); + singletonList(PutObject.builder().build()), true, emptySet(), "", "", + expiredTimeout); // then assertThat(future.failed()).isTrue(); @@ -1105,7 +758,8 @@ public void cachePutObjectsShouldTolerateGlobalTimeoutAlreadyExpired() { @Test public void cachePutObjectsShouldReturnResultWithEmptyListWhenPutObjectsIsEmpty() { // when - final Future result = cacheService.cachePutObjects(emptyList(), emptySet(), null, null, null); + final Future result = cacheService.cachePutObjects(emptyList(), true, + emptySet(), null, null, null); // then verifyZeroInteractions(httpClient); @@ -1120,9 +774,7 @@ public void cachePutObjectsShouldModifyVastAndCachePutObjects() throws IOExcepti .bidid("bidId1") .bidder("bidder1") .timestamp(1L) - .value(new TextNode("" - + "prebid.org wrapper" - + "")) + .value(new TextNode("vast")) .build(); final PutObject secondPutObject = PutObject.builder() .type("xml") @@ -1132,26 +784,26 @@ public void cachePutObjectsShouldModifyVastAndCachePutObjects() throws IOExcepti .value(new TextNode("VAST")) .build(); - given(eventsService.vastUrlTracking(any(), any(), any(), any(), anyString())) - .willReturn("http://external-url/event"); + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")) + .willReturn(new TextNode("VAST")); // when cacheService.cachePutObjects( - asList(firstPutObject, secondPutObject), singleton("bidder1"), "account", "pbjs", timeout); + asList(firstPutObject, secondPutObject), true, singleton("bidder1"), "account", "pbjs", timeout); // then - verify(metrics, times(1)).updateCacheCreativeSize(eq("account"), eq(224)); - verify(metrics, times(1)).updateCacheCreativeSize(eq("account"), eq(4)); + verify(metrics).updateCacheCreativeSize(eq("account"), eq(12)); + verify(metrics).updateCacheCreativeSize(eq("account"), eq(4)); + + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), firstPutObject, "account", "pbjs"); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), secondPutObject, "account", "pbjs"); final PutObject modifiedFirstPutObject = firstPutObject.toBuilder() .bidid(null) .bidder(null) .timestamp(null) - .value(new TextNode("" - + "prebid.org wrapper" - + "" - + "")) + .value(new TextNode("modifiedVast")) .build(); final PutObject modifiedSecondPutObject = secondPutObject.toBuilder() .bidid(null) @@ -1163,30 +815,13 @@ public void cachePutObjectsShouldModifyVastAndCachePutObjects() throws IOExcepti .containsOnly(modifiedFirstPutObject, modifiedSecondPutObject); } - @Test - public void cachePutObjectsShouldCallEventsServiceWithExpectedArguments() { - // given - final PutObject firstPutObject = PutObject.builder() - .type("xml") - .bidid("bidId1") - .bidder("bidder1") - .timestamp(1000L) - .value(new TextNode("")) - .build(); - - // when - cacheService.cachePutObjects(singletonList(firstPutObject), singleton("bidder1"), "account", "pbjs", timeout); - - // then - verify(eventsService).vastUrlTracking(eq("bidId1"), eq("bidder1"), eq("account"), eq(1000L), eq("pbjs")); - } - private AuctionContext givenAuctionContext(UnaryOperator accountCustomizer, UnaryOperator bidRequestCustomizer) { final Account.AccountBuilder accountBuilder = Account.builder() .id("accountId"); final BidRequest.BidRequestBuilder bidRequestBuilder = BidRequest.builder() + .id("auctionId") .imp(singletonList(givenImp(identity()))); return AuctionContext.builder() .account(accountCustomizer.apply(accountBuilder).build()) @@ -1203,31 +838,54 @@ private AuctionContext givenAuctionContext() { return givenAuctionContext(identity(), identity()); } - private static List singleBidList() { - return singletonList(givenBid(identity())); + private static BidInfo givenBidInfo(UnaryOperator bidCustomizer) { + return givenBidInfo(bidCustomizer, UnaryOperator.identity()); } - private static Bid givenBid(UnaryOperator bidCustomizer) { - return bidCustomizer.apply(Bid.builder()).build(); + private static BidInfo givenBidInfo(UnaryOperator bidCustomizer, + UnaryOperator impCustomizer) { + return BidInfo.builder() + .bid(bidCustomizer.apply(Bid.builder()).build()) + .correspondingImp(impCustomizer.apply(Imp.builder()).build()) + .bidder("bidder") + .bidType(BidType.banner) + .build(); } - private static com.iab.openrtb.response.Bid givenBidOpenrtb( - UnaryOperator bidCustomizer) { + private static BidInfo givenBidInfo(UnaryOperator bidCustomizer, + BidType bidType, + String bidder) { + return BidInfo.builder() + .bid(bidCustomizer.apply(Bid.builder()).build()) + .correspondingImp(givenImp(UnaryOperator.identity())) + .bidder(bidder) + .bidType(bidType) + .build(); + } - return bidCustomizer.apply(com.iab.openrtb.response.Bid.builder()).build(); + private static BidInfo givenBidInfo(UnaryOperator bidCustomizer, + BidType bidType, + String bidder, + String lineItemId) { + return givenBidInfo(bidCustomizer, bidType, bidder).toBuilder() + .lineItemId(lineItemId) + .build(); } private static Imp givenImp(UnaryOperator impCustomizer) { return impCustomizer.apply(Imp.builder()).build(); } - private static CacheHttpRequest givenCacheHttpRequest(com.iab.openrtb.response.Bid... bids) - throws JsonProcessingException { + private static CacheHttpRequest givenCacheHttpRequest(Bid... bids) throws JsonProcessingException { final List putObjects; if (bids != null) { putObjects = new ArrayList<>(); - for (com.iab.openrtb.response.Bid bid : bids) { - putObjects.add(PutObject.builder().type("json").value(mapper.valueToTree(bid)).build()); + for (Bid bid : bids) { + putObjects.add(PutObject.builder() + .aid("auctionId") + .type("json") + .value(mapper.valueToTree(bid)) + .build()); } } else { putObjects = null; @@ -1253,4 +911,11 @@ private BidCacheRequest captureBidCacheRequest() throws IOException { verify(httpClient).post(anyString(), any(), captor.capture(), anyLong()); return mapper.readValue(captor.getValue(), BidCacheRequest.class); } + + private Map> givenDebugHeaders() { + final Map> headers = new HashMap<>(); + headers.put("Accept", singletonList("application/json")); + headers.put("Content-Type", singletonList("application/json;charset=utf-8")); + return headers; + } } diff --git a/src/test/java/org/prebid/server/deals/AdminCentralServiceTest.java b/src/test/java/org/prebid/server/deals/AdminCentralServiceTest.java new file mode 100644 index 00000000000..22fa213ec11 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/AdminCentralServiceTest.java @@ -0,0 +1,437 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.model.AdminAccounts; +import org.prebid.server.deals.model.AdminCentralResponse; +import org.prebid.server.deals.model.AdminLineItems; +import org.prebid.server.deals.model.Command; +import org.prebid.server.deals.model.LogCriteriaFilter; +import org.prebid.server.deals.model.LogTracer; +import org.prebid.server.deals.model.ServicesCommand; +import org.prebid.server.log.CriteriaManager; +import org.prebid.server.settings.CachingApplicationSettings; +import org.prebid.server.settings.SettingsCache; +import org.prebid.server.settings.proto.request.InvalidateSettingsCacheRequest; +import org.prebid.server.settings.proto.request.UpdateSettingsCacheRequest; + +import java.util.Collections; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class AdminCentralServiceTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + private AdminCentralService adminCentralService; + + @Mock + private LineItemService lineItemService; + + @Mock + private DeliveryProgressService deliveryProgressService; + + @Mock + private CriteriaManager criteriaManager; + + @Mock + private SettingsCache settingsCache; + + @Mock + private SettingsCache ampSettingsCache; + + @Mock + private CachingApplicationSettings cachingApplicationSettings; + + @Mock + private Suspendable suspendable; + + @Before + public void setUp() { + adminCentralService = new AdminCentralService(criteriaManager, lineItemService, deliveryProgressService, + settingsCache, ampSettingsCache, cachingApplicationSettings, + jacksonMapper, singletonList(suspendable)); + } + + @Test + public void processAdminCentralEventShouldAddCriteriaWhenTraceLogAndCriteriaFilterArePresentAndCmdIsStart() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of( + LogTracer.of("start", false, 800L, LogCriteriaFilter.of(null, null, null)), null, null, null, null, + null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(criteriaManager).addCriteria(any(), anyLong()); + } + + @Test + public void processAdminCentralEventShouldAddCriteriaWhenTraceLogAndCriteriaFilterArePresentAndCmdIsStop() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(LogTracer.of("stop", false, 0L, null), + null, null, null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(criteriaManager).stop(); + } + + @Test + public void processAdminCentralEventShouldStopServicesWhenServicesStopCommandIsPresent() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, null, null, null, null, + ServicesCommand.of("stop")); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(suspendable).suspend(); + } + + @Test + public void processAdminCentralEventShouldNotStopServicesWhenServicesCommandIsNotStop() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, null, null, null, null, + ServicesCommand.of("invalid")); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(suspendable); + } + + @Test + public void processAdminCentralEventShouldAddCriteriaAndStopServices() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of( + LogTracer.of("start", false, 800L, LogCriteriaFilter.of(null, null, null)), null, null, null, null, + ServicesCommand.of("stop")); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(suspendable).suspend(); + verify(criteriaManager).addCriteria(any(), anyLong()); + } + + @Test + public void processAdminCentralEventShouldNotCallCriteriaManagerWhenCommandIsNull() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of( + LogTracer.of(null, false, 800L, LogCriteriaFilter.of(null, null, null)), null, null, null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(criteriaManager); + } + + @Test + public void processAdminCentralEventShouldNotCallCriteriaManagerWhenItIsNotStartOrStop() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of( + LogTracer.of("invalid", false, 800L, LogCriteriaFilter.of(null, null, null)), null, null, + null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(criteriaManager); + } + + @Test + public void processAdminCentralEventShouldNotCallSettingsCacheWhenCommandWasNotDefined() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, Command.of(null, null), + Command.of(null, null), null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(settingsCache); + verifyZeroInteractions(ampSettingsCache); + } + + @Test + public void processAdminCentralEventShouldNotCallSettingsCacheWhenBodyWasNotDefined() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, Command.of("save", null), + Command.of("save", null), null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(settingsCache); + verifyZeroInteractions(ampSettingsCache); + } + + @Test + public void processAdminCentralEventShouldCallSettings() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, Command.of("save", null), + Command.of("save", null), null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(settingsCache); + verifyZeroInteractions(ampSettingsCache); + } + + @Test + public void processAdminCentralEventShouldCallSaveAmpSettingsCache() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, null, + Command.of("save", jacksonMapper.mapper().valueToTree(UpdateSettingsCacheRequest + .of(Collections.emptyMap(), Collections.emptyMap()))), null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(ampSettingsCache).save(any(), any()); + } + + @Test + public void processAdminCentralEventShouldCallInvalidateAmpSettingsCache() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, null, + Command.of("invalidate", jacksonMapper.mapper().valueToTree(InvalidateSettingsCacheRequest + .of(Collections.emptyList(), Collections.emptyList()))), null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(ampSettingsCache).invalidate(any(), any()); + } + + @Test + public void processAdminCentralEventShouldNotCallAmpSettingsCacheWhenCantParseBody() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, null, + Command.of("save", jacksonMapper.mapper().createObjectNode().put("requests", 1)), null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(ampSettingsCache); + } + + @Test + public void processAdminCentralEventShouldCallSaveSettingsCache() throws JsonProcessingException { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + Command.of("save", jacksonMapper.mapper().valueToTree(UpdateSettingsCacheRequest + .of(Collections.singletonMap("requestId", + jacksonMapper.mapper().writeValueAsString(BidRequest.builder().id("requestId") + .build())), + Collections.singletonMap("impId", + jacksonMapper.mapper().writeValueAsString(Imp.builder().id("impId") + .build()))))), + null, null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(settingsCache).save(any(), any()); + } + + @Test + public void processAdminCentralEventShouldCallInvalidateSettingsCache() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + Command.of("invalidate", jacksonMapper.mapper().valueToTree(InvalidateSettingsCacheRequest + .of(Collections.emptyList(), Collections.emptyList()))), null, null, null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(settingsCache).invalidate(any(), any()); + } + + @Test + public void processAdminCentralEventShouldNotCallSettingsCacheWhenCantParseBody() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + Command.of("save", jacksonMapper.mapper().createObjectNode().put("requests", 1)), null, null, null, + null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(settingsCache); + } + + @Test + public void processAdminCentralEventShouldCallInvalidateLineItemsById() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + null, null, Command.of("invalidate", jacksonMapper.mapper() + .valueToTree(AdminLineItems.of(singletonList("lineItemId")))), null, null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(lineItemService).invalidateLineItemsByIds(eq(singletonList("lineItemId"))); + verify(deliveryProgressService).invalidateLineItemsByIds(eq(singletonList("lineItemId"))); + } + + @Test + public void processAdminCentralEventShouldCallInvalidateAllLineItems() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + null, null, Command.of("invalidate", null), null, null); + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(lineItemService).invalidateLineItems(); + verify(deliveryProgressService).invalidateLineItems(); + } + + @Test + public void processAdminCentralEventShouldNotCallInvalidateWhenCmdNotDefined() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + null, null, Command.of(null, null), null, null); + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyNoMoreInteractions(lineItemService); + verifyNoMoreInteractions(deliveryProgressService); + } + + @Test + public void processAdminCentralEventShouldNotCallInvalidateWhenCmdHasValueOtherToInvalidate() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + null, null, Command.of("save", null), null, null); + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyNoMoreInteractions(lineItemService); + verifyNoMoreInteractions(deliveryProgressService); + } + + @Test + public void processAdminCentralEventShouldNotCallInvalidateWhenCantParseBody() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + null, null, Command.of("invalidate", mapper.createObjectNode() + .set("ids", mapper.valueToTree(AdminLineItems.of(singletonList("5"))))), null, + null); + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyNoMoreInteractions(lineItemService); + verifyNoMoreInteractions(deliveryProgressService); + } + + @Test + public void processAdminCentralEventShouldNotCallInvalidateAccountsWhenCommandIsNotDefined() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + null, null, null, Command.of(null, mapper.createObjectNode() + .set("ids", mapper.valueToTree(AdminAccounts.of(singletonList("1001"))))), null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(cachingApplicationSettings); + } + + @Test + public void processAdminCentralEventShouldNotCallInvalidateAccountsWhenInvalidCommandValue() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + null, null, null, Command.of("invalid", mapper.valueToTree(AdminAccounts.of(singletonList("1001")))), + null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(cachingApplicationSettings); + } + + @Test + public void processAdminCentralEventShouldNotCallInvalidateAccountsCantParseBody() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + null, null, null, Command.of("invalidate", mapper.createObjectNode() + .set("accounts", mapper.valueToTree(AdminAccounts.of(singletonList("5"))))), null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verifyZeroInteractions(cachingApplicationSettings); + } + + @Test + public void processAdminCentralEventShouldInvalidateAccounts() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + null, null, null, Command.of("invalidate", + mapper.valueToTree(AdminAccounts.of(asList("1001", "1002")))), null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(cachingApplicationSettings).invalidateAccountCache(eq("1001")); + verify(cachingApplicationSettings).invalidateAccountCache(eq("1002")); + } + + @Test + public void processAdminCentralEventShouldInvalidateAllAccounts() { + // given + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, + null, null, null, Command.of("invalidate", mapper.createObjectNode() + .set("ids", mapper.valueToTree(AdminAccounts.of(null)))), null); + + // when + adminCentralService.processAdminCentralEvent(adminCentralResponse); + + // then + verify(cachingApplicationSettings).invalidateAllAccountCache(); + } +} diff --git a/src/test/java/org/prebid/server/deals/AlertHttpServiceTest.java b/src/test/java/org/prebid/server/deals/AlertHttpServiceTest.java new file mode 100644 index 00000000000..af9af63f06e --- /dev/null +++ b/src/test/java/org/prebid/server/deals/AlertHttpServiceTest.java @@ -0,0 +1,233 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import io.vertx.core.Future; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.model.AlertEvent; +import org.prebid.server.deals.model.AlertPriority; +import org.prebid.server.deals.model.AlertProxyProperties; +import org.prebid.server.deals.model.AlertSource; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.vertx.http.HttpClient; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.stream.IntStream; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class AlertHttpServiceTest extends VertxTest { + + @Rule + public final MockitoRule rule = MockitoJUnit.rule(); + + @Mock + private HttpClient httpClient; + + private ZonedDateTime now; + + private Clock clock; + + private AlertHttpService alertHttpService; + + @Before + public void setUp() { + clock = Clock.fixed(Instant.parse("2019-07-26T10:00:00Z"), ZoneOffset.UTC); + now = ZonedDateTime.now(clock); + final HashMap servicesAlertPeriod = new HashMap<>(); + servicesAlertPeriod.put("pbs-error", 3L); + alertHttpService = new AlertHttpService(jacksonMapper, httpClient, clock, DeploymentProperties.builder() + .pbsVendor("pbsVendor") + .pbsRegion("pbsRegion").pbsHostId("pbsHostId").subSystem("pbsSubSystem").system("pbsSystem") + .dataCenter("pbsDataCenter").infra("pbsInfra").profile("pbsProfile").build(), + AlertProxyProperties.builder().password("password").username("username").timeoutSec(5) + .url("http://localhost") + .alertTypes(servicesAlertPeriod).enabled(true).build()); + } + + @Test + public void alertShouldNotSendAlertWhenServiceIsNotEnabled() { + // given + alertHttpService = new AlertHttpService(jacksonMapper, httpClient, clock, DeploymentProperties.builder() + .pbsVendor("pbsVendor").pbsRegion("pbsRegion").pbsHostId("pbsHostId").subSystem("pbsSubSystem") + .system("pbsSystem").dataCenter("pbsDataCenter").infra("pbsInfra").profile("pbsProfile").build(), + AlertProxyProperties.builder().password("password").username("username").timeoutSec(5) + .alertTypes(emptyMap()) + .url("http://localhost").enabled(false).build()); + + // when + alertHttpService.alert("pbs", AlertPriority.HIGH, "errorMessage"); + + // then + verifyZeroInteractions(httpClient); + } + + @Test + public void alertShouldSendAlertWhenServiceIsEnabled() throws JsonProcessingException { + // given + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture()); + + // when + alertHttpService.alert("pbs", AlertPriority.HIGH, "errorMessage"); + + // then + final List requestPayloadObject = getRequestPayload(); + final String id = requestPayloadObject.get(0).getId(); + assertThat(requestPayloadObject) + .isEqualTo(singletonList(AlertEvent.builder().id(id).action("RAISE").priority(AlertPriority.HIGH) + .updatedAt(now).name("pbs").details("errorMessage") + .source(AlertSource.builder().env("pbsProfile").dataCenter("pbsDataCenter").region("pbsRegion") + .system("pbsSystem").subSystem("pbsSubSystem").hostId("pbsHostId").build()).build())); + } + + @Test + public void alertWithPeriodShouldSendAlertFirstTimeWithPassedAlertPriority() throws JsonProcessingException { + // given + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture()); + + // when + alertHttpService.alertWithPeriod("pbs", "pbs-error", AlertPriority.MEDIUM, "errorMessage"); + + // then + final List requestPayloadObject = getRequestPayload(); + final String id = requestPayloadObject.get(0).getId(); + assertThat(requestPayloadObject) + .isEqualTo(singletonList(AlertEvent.builder().id(id).action("RAISE").priority(AlertPriority.MEDIUM) + .updatedAt(now).name("pbs-error").details("Service pbs failed to send request 1 time(s) " + + "with error message : errorMessage") + .source(AlertSource.builder().env("pbsProfile").dataCenter("pbsDataCenter").region("pbsRegion") + .system("pbsSystem").subSystem("pbsSubSystem").hostId("pbsHostId").build()).build())); + } + + @Test + public void alertWithPeriodShouldSendAlertWithPassedPriorityAndWithHighPriorityAfterPeriodLimitReached() + throws JsonProcessingException { + // given + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture()); + + // when + IntStream.range(0, 3).forEach(ignored -> + alertHttpService.alertWithPeriod("pbs", "pbs-error", AlertPriority.MEDIUM, "errorMessage")); + + // then + final ArgumentCaptor requestBodyCaptor = ArgumentCaptor.forClass(String.class); + verify(httpClient, times(2)).post(anyString(), any(), requestBodyCaptor.capture(), anyLong()); + + final List requests = requestBodyCaptor.getAllValues(); + final List alertEvents1 = parseAlertEvents(requests.get(0)); + + final String id = alertEvents1.get(0).getId(); + + assertThat(alertEvents1) + .isEqualTo(singletonList(AlertEvent.builder().id(id).action("RAISE").priority(AlertPriority.MEDIUM) + .updatedAt(now).name("pbs-error").details("Service pbs failed to send request 1 time(s) " + + "with error message : errorMessage") + .source(AlertSource.builder().env("pbsProfile").dataCenter("pbsDataCenter").region("pbsRegion") + .system("pbsSystem").subSystem("pbsSubSystem").hostId("pbsHostId").build()).build())); + + final List alertEvents2 = parseAlertEvents(requests.get(1)); + final String id2 = alertEvents2.get(0).getId(); + + assertThat(alertEvents2) + .isEqualTo(singletonList(AlertEvent.builder().id(id2).action("RAISE").priority(AlertPriority.HIGH) + .updatedAt(now).name("pbs-error").details("Service pbs failed to send request 3 time(s) " + + "with error message : errorMessage") + .source(AlertSource.builder().env("pbsProfile").dataCenter("pbsDataCenter").region("pbsRegion") + .system("pbsSystem").subSystem("pbsSubSystem").hostId("pbsHostId").build()).build())); + } + + @Test + public void alertWithPeriodShouldSendAlertTwoTimesForUnknownServiceWithDefaultPeriod() + throws JsonProcessingException { + // given + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture()); + + // when + IntStream.range(0, 15).forEach(ignored -> + alertHttpService.alertWithPeriod("unknown", "unknown-error", AlertPriority.MEDIUM, "errorMessage")); + + // then + final ArgumentCaptor requestBodyCaptor = ArgumentCaptor.forClass(String.class); + verify(httpClient, times(2)).post(anyString(), any(), requestBodyCaptor.capture(), anyLong()); + + final List requests = requestBodyCaptor.getAllValues(); + final List alertEvents1 = parseAlertEvents(requests.get(0)); + + final String id = alertEvents1.get(0).getId(); + + assertThat(alertEvents1) + .isEqualTo(singletonList(AlertEvent.builder().id(id).action("RAISE").priority(AlertPriority.MEDIUM) + .updatedAt(now).name("unknown-error").details("Service unknown failed to send request" + + " 1 time(s) with error message : errorMessage") + .source(AlertSource.builder().env("pbsProfile").dataCenter("pbsDataCenter").region("pbsRegion") + .system("pbsSystem").subSystem("pbsSubSystem").hostId("pbsHostId").build()).build())); + + final List alertEvents2 = parseAlertEvents(requests.get(1)); + final String id2 = alertEvents2.get(0).getId(); + + assertThat(alertEvents2) + .isEqualTo(singletonList(AlertEvent.builder().id(id2).action("RAISE").priority(AlertPriority.HIGH) + .updatedAt(now).name("unknown-error").details("Service unknown failed to send request" + + " 15 time(s) with error message : errorMessage") + .source(AlertSource.builder().env("pbsProfile").dataCenter("pbsDataCenter").region("pbsRegion") + .system("pbsSystem").subSystem("pbsSubSystem").hostId("pbsHostId").build()).build())); + } + + @Test + public void alertWithPeriodShouldSendAlertFirstTimeWithPassedAlertPriorityForUnknownService() + throws JsonProcessingException { + // given + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture()); + + // when + alertHttpService.alertWithPeriod("unknown", "unknown-error", AlertPriority.MEDIUM, "errorMessage"); + + // then + final List requestPayloadObject = getRequestPayload(); + final String id = requestPayloadObject.get(0).getId(); + assertThat(requestPayloadObject) + .isEqualTo(singletonList(AlertEvent.builder().id(id).action("RAISE").priority(AlertPriority.MEDIUM) + .updatedAt(now).name("unknown-error").details("Service unknown failed to send request " + + "1 time(s) with error message : errorMessage") + .source(AlertSource.builder().env("pbsProfile").dataCenter("pbsDataCenter").region("pbsRegion") + .system("pbsSystem").subSystem("pbsSubSystem").hostId("pbsHostId").build()).build())); + } + + private List getRequestPayload() throws JsonProcessingException { + final ArgumentCaptor requestBodyCaptor = ArgumentCaptor.forClass(String.class); + verify(httpClient).post(anyString(), any(), requestBodyCaptor.capture(), anyLong()); + return parseAlertEvents(requestBodyCaptor.getValue()); + } + + private List parseAlertEvents(String row) throws JsonProcessingException { + return mapper.readValue(row, + new TypeReference>() { + }); + } +} diff --git a/src/test/java/org/prebid/server/deals/DealsProcessorTest.java b/src/test/java/org/prebid/server/deals/DealsProcessorTest.java new file mode 100644 index 00000000000..0530fba389b --- /dev/null +++ b/src/test/java/org/prebid/server/deals/DealsProcessorTest.java @@ -0,0 +1,954 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Pmp; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.User; +import io.vertx.core.Future; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.deals.deviceinfo.DeviceInfoService; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.model.DeepDebugLog; +import org.prebid.server.deals.model.DeviceInfo; +import org.prebid.server.deals.model.MatchLineItemsResult; +import org.prebid.server.deals.model.UserData; +import org.prebid.server.deals.model.UserDetails; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.deals.proto.LineItemSize; +import org.prebid.server.geolocation.GeoLocationService; +import org.prebid.server.geolocation.model.GeoInfo; +import org.prebid.server.log.CriteriaLogManager; +import org.prebid.server.proto.openrtb.ext.request.ExtDeal; +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; +import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtDeviceVendor; +import org.prebid.server.proto.openrtb.ext.request.ExtGeo; +import org.prebid.server.proto.openrtb.ext.request.ExtGeoVendor; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.ExtUserTime; +import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal; +import org.prebid.server.settings.model.Account; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.function.Function.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class DealsProcessorTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private LineItemService lineItemService; + @Mock + private DeviceInfoService deviceInfoService; + @Mock + private GeoLocationService geoLocationService; + @Mock + private UserService userService; + @Mock + private CriteriaLogManager criteriaLogManager; + + private DealsProcessor dealsProcessor; + + private static final Clock CLOCK = Clock.fixed(Instant.parse("2019-10-10T00:01:00Z"), ZoneOffset.UTC); + + @Before + public void setUp() { + dealsProcessor = new DealsProcessor( + lineItemService, + deviceInfoService, + geoLocationService, + userService, + CLOCK, + jacksonMapper, + criteriaLogManager); + } + + @Test + public void populateDealsInfoShouldReturnOriginalContextIfAccountHasNoDeals() { + // given + given(lineItemService.accountHasDeals(any())).willReturn(false); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(givenAccount(identity())) + .bidRequest(givenBidRequest(identity())) + .build(); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(result).isSameAs(auctionContext); + } + + @Test + public void populateDealsInfoShouldPopulateDevice() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(deviceInfoService.getDeviceInfo(any())) + .willReturn(Future.succeededFuture(DeviceInfo.builder() + .vendor("vendor") + .deviceTypeRaw("mobile") + .os("os") + .osVersion("osVersion") + .browser("browser") + .browserVersion("browserVersion") + .language("ENG") + .carrier("AT&T") + .manufacturer("Apple") + .model("iPhone 8") + .build())); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder() + .ip("ip") + .ua("ua") + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(640, 480)))) + .build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + final ExtDevice expectedExtDevice = ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(640, 480))); + expectedExtDevice.addProperty("vendor", mapper.valueToTree(ExtDeviceVendor.builder() + .type("mobile") + .os("os") + .osver("osVersion") + .browser("browser") + .browserver("browserVersion") + .make("Apple") + .model("iPhone 8") + .language("ENG") + .carrier("AT&T") + .build())); + assertThat(result.getBidRequest().getDevice()).isEqualTo(Device.builder() + .ip("ip") + .ua("ua") + .ext(expectedExtDevice) + .build()); + } + + @Test + public void populateDealsInfoShouldPopulateDeviceGeo() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(geoLocationService.lookup(any(), any())) + .willReturn(Future.succeededFuture(GeoInfo.builder() + .vendor("vendor") + .continent("continent") + .country("country") + .region("region") + .regionCode(1) + .city("city") + .metroGoogle("metroGoogle") + .metroNielsen(516) + .zip("12345") + .connectionSpeed("broadband") + .lat(11.11F) + .lon(22.22F) + .build())); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder() + .geo(Geo.builder().zip("zip").build()) + .build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + final ExtGeo expectedExtGeo = ExtGeo.of(); + expectedExtGeo.addProperty("vendor", mapper.valueToTree(ExtGeoVendor.builder() + .continent("continent") + .country("country") + .region(1) + .metro(516) + .city("city") + .zip("12345") + .build())); + final ExtDevice expectedDevice = ExtDevice.of(null, null); + expectedDevice.addProperty("vendor", mapper.valueToTree(ExtDeviceVendor.builder() + .connspeed("broadband") + .build())); + assertThat(result.getBidRequest().getDevice()).isEqualTo(Device.builder() + .geo(Geo.builder() + .zip("zip") + .region("region") + .metro("metroGoogle") + .lat(11.11F) + .lon(22.22F) + .ext(expectedExtGeo) + .build()) + .ext(expectedDevice) + .build()); + } + + @Test + public void populateDealsInfoShouldPopulateUserWithUserDetails() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(userService.getUserDetails(any(), any())).willReturn(Future.succeededFuture( + UserDetails.of(singletonList(UserData.of(null, "rubicon", + singletonList(org.prebid.server.deals.model.Segment.of("segmentId")))), + singletonList("fcapId")))); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().ip("ip").ua("ua").build()) + .user(User.builder() + .ext(ExtUser.builder().consent("consent").build()) + .build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(result.getBidRequest().getUser()).isEqualTo(User.builder() + .data(singletonList(Data.builder().id("rubicon") + .segment(singletonList(Segment.builder().id("segmentId").build())).build())) + .ext(ExtUser.builder() + .consent("consent") + .fcapIds(singletonList("fcapId")) + .time(ExtUserTime.of(5, 0)) + .build()) + .build()); + } + + @Test + public void populateDealsInfoShouldUseForGeoLocationIpV6IfIpV4IsNotDefined() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().ipv6("ipv6").build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + verify(geoLocationService).lookup(eq("ipv6"), any()); + } + + @Test + public void populateDealsInfoShouldPopulateUserExtWithGeoInfo() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(geoLocationService.lookup(any(), any())) + .willReturn(Future.succeededFuture(GeoInfo.builder() + .vendor("vendor") + .timeZone(ZoneId.of("America/Los_Angeles")) + .build())); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().ip("ip").ua("ua").build()) + .user(User.builder() + .ext(ExtUser.builder().consent("consent").build()) + .build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(result.getBidRequest().getUser()).isEqualTo(User.builder() + .ext(ExtUser.builder() + .consent("consent") + .time(ExtUserTime.of(4, 17)) + .build()) + .build()); + } + + @Test + public void populateDealsInfoShouldPopulateUserExtWithGeoInfoIfTimeZoneIsMissing() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(geoLocationService.lookup(any(), any())) + .willReturn(Future.succeededFuture(GeoInfo.builder() + .vendor("vendor") + .build())); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().ip("ip").ua("ua").build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(result.getBidRequest().getUser()).isEqualTo(User.builder() + .ext(ExtUser.builder() + .time(ExtUserTime.of(5, 0)) + .build()) + .build()); + } + + @Test + public void populateDealsInfoShouldPopulateUserWithEmptyCappedIds() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(userService.getUserDetails(any(), any())) + .willReturn(Future.succeededFuture(UserDetails.of(null, null))); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().ip("ip").ua("ua").build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(result.getBidRequest().getUser()).isEqualTo(User.builder() + .ext(ExtUser.builder() + .fcapIds(emptyList()) + .time(ExtUserTime.of(5, 0)) + .build()) + .build()); + } + + @Test + public void populateDealsInfoShouldPopulateUserWithNullCappedIds() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().ip("ip").ua("ua").build()) + .user(User.builder() + .ext(ExtUser.builder().consent("consent").build()) + .build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(result.getBidRequest().getUser()).isEqualTo(User.builder() + .ext(ExtUser.builder() + .consent("consent") + .time(ExtUserTime.of(5, 0)) + .build()) + .build()); + } + + @Test + public void populateDealsInfoShouldEnrichImpWithDeals() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(lineItemService.findMatchingLineItems(any(), any())) + .willReturn(MatchLineItemsResult.of(singletonList(LineItem.of( + LineItemMetaData.builder() + .lineItemId("lineItemId") + .extLineItemId("extLineItemId") + .source("bidder") + .dealId("dealId") + .build(), + null, null, ZonedDateTime.now(CLOCK))))); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build()) + .imp(singletonList(Imp.builder() + .pmp(Pmp.builder() + .deals(singletonList(Deal.builder().id("existingDealId").build())) + .build()) + .build()))); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(singletonList(result)) + .extracting(AuctionContext::getBidRequest) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getPmp) + .flatExtracting(Pmp::getDeals) + .containsOnly( + Deal.builder().id("existingDealId").build(), + Deal.builder() + .id("dealId") + .ext(mapper.valueToTree( + ExtDeal.of(ExtDealLine.of("lineItemId", "extLineItemId", null, "bidder")))) + .build()); + } + + @Test + public void populateDealsInfoShouldEnrichImpWithDealsAndAddLineItemSizesIfSizesIntersectionMatched() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(lineItemService.findMatchingLineItems(any(), any())) + .willReturn(MatchLineItemsResult.of(singletonList(LineItem.of( + LineItemMetaData.builder().sizes(singletonList(LineItemSize.of(200, 20))).build(), null, null, + ZonedDateTime.now(CLOCK))))); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build()) + .imp(singletonList(Imp.builder() + .banner(Banner.builder() + .format(asList( + Format.builder().w(100).h(10).build(), + Format.builder().w(200).h(20).build())) + .build()) + .build()))); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(singletonList(result)) + .extracting(AuctionContext::getBidRequest) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getPmp) + .flatExtracting(Pmp::getDeals) + .extracting(Deal::getExt) + .extracting(node -> mapper.treeToValue(node, ExtDeal.class)) + .containsOnly(ExtDeal.of( + ExtDealLine.of(null, null, singletonList(Format.builder().w(200).h(20).build()), null))); + } + + @Test + public void populateDealsInfoShouldEnrichImpWithDealsAndNotAddLineItemSizesIfSizesIntersectionNotMatched() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(lineItemService.findMatchingLineItems(any(), any())) + .willReturn(MatchLineItemsResult.of(singletonList(LineItem.of( + LineItemMetaData.builder().sizes(singletonList(LineItemSize.of(200, 20))).build(), null, null, + ZonedDateTime.now(CLOCK))))); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build()) + .imp(singletonList(Imp.builder() + .banner(Banner.builder() + .format(singletonList(Format.builder().w(100).h(10).build())) + .build()) + .build()))); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(singletonList(result)) + .extracting(AuctionContext::getBidRequest) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getPmp) + .flatExtracting(Pmp::getDeals) + .extracting(Deal::getExt) + .extracting(node -> mapper.treeToValue(node, ExtDeal.class)) + .containsOnly(ExtDeal.of(ExtDealLine.of(null, null, null, null))); + } + + @Test + public void removeDealsOnlyBiddersWithoutDealsShouldRemoveDealsOnlyBiddersWithoutDealsWhenPmpIsNull() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final ObjectNode extImp = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set( + "rubicon", mapper.createObjectNode().set("dealsonly", BooleanNode.TRUE)) + .set("appnexus", mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE)))); + + final Imp imp = Imp.builder().pmp(null).ext(extImp).build(); + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final Imp result = DealsProcessor.removeDealsOnlyBiddersWithoutDeals(imp, auctionContext); + + // then + assertThat(result).isEqualTo(Imp.builder() + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode().set( + "appnexus", mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE))))) + .build()); + } + + @Test + public void removeDealsOnlyBiddersWithoutDealsShouldRemoveDealsOnlyBiddersWithoutDealsWhenPmpDealsIsEmpty() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final ObjectNode extImp = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set( + "rubicon", mapper.createObjectNode().set("dealsonly", BooleanNode.TRUE)) + .set("appnexus", mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE)))); + + final Imp imp = Imp.builder() + .pmp(Pmp.builder().deals(emptyList()).build()) + .ext(extImp) + .build(); + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final Imp result = DealsProcessor.removeDealsOnlyBiddersWithoutDeals(imp, auctionContext); + + // then + assertThat(result).isEqualTo(Imp.builder() + .pmp(Pmp.builder().deals(emptyList()).build()) + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode().set( + "appnexus", mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE))))) + .build()); + } + + @Test + public void removeDealsOnlyBiddersWithoutDealsShouldRemoveDealsOnlyBiddersWithoutDeals() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final ObjectNode extImp = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set( + "rubicon", mapper.createObjectNode().set("dealsonly", BooleanNode.TRUE)) + .set("appnexus", mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE)))); + + final Imp imp = Imp.builder().ext(extImp).build(); + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build())); + + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final Imp result = DealsProcessor.removeDealsOnlyBiddersWithoutDeals(imp, auctionContext); + + // then + assertThat(result).isEqualTo(Imp.builder() + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode().set( + "appnexus", mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE))))) + .build()); + } + + @Test + public void populateDealsInfoShouldNotRemoveDealsOnlyBiddersWithoutDealsIfDealsOnlyFlagWasNotDefined() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build()) + .imp(singletonList(Imp.builder() + .ext(mapper.createObjectNode() + .set("appnexus", mapper.createObjectNode())) + .build()))); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(singletonList(result)) + .extracting(AuctionContext::getBidRequest) + .flatExtracting(BidRequest::getImp) + .containsOnly(Imp.builder() + .ext(mapper.createObjectNode().set("appnexus", mapper.createObjectNode())) + .build()); + } + + @Test + public void populateDealsInfoShouldNotRemoveDealsOnlyBiddersWithoutDealsIfNotValid() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build()) + .imp(singletonList(Imp.builder() + .ext(mapper.createObjectNode() + .set("invalid", mapper.createObjectNode().set("dealsonly", BooleanNode.TRUE))) + .build()))); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(singletonList(result)) + .extracting(AuctionContext::getBidRequest) + .flatExtracting(BidRequest::getImp) + .containsOnly(Imp.builder() + .ext(mapper.createObjectNode() + .set("invalid", mapper.createObjectNode().set("dealsonly", BooleanNode.TRUE))) + .build()); + } + + @Test + public void removeDealsOnlyBiddersWithoutDealsShouldRemoveDealsOnlyBiddersForResolvedBidderFromAliases() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final ObjectNode impExt = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set( + "appnexusAlias", mapper.createObjectNode().set("dealsonly", BooleanNode.TRUE)) + .set("rubiconAlias", mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE)))); + + final Imp imp = Imp.builder().ext(impExt).build(); + + final Map aliases = new HashMap<>(); + aliases.put("appnexusAlias", "appnexus"); + aliases.put("rubiconAlias", "rubicon"); + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build()) + .ext(ExtRequest.of(ExtRequestPrebid.builder().aliases(aliases).build()))); + + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final Imp result = DealsProcessor.removeDealsOnlyBiddersWithoutDeals(imp, auctionContext); + + // then + assertThat(result).isEqualTo(Imp.builder() + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode().set( + "rubiconAlias", + mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE))))) + .build()); + } + + @Test + public void removeDealsOnlyBiddersWithoutDealsShouldNotRemoveDealsOnlyBiddersWhenDealsOnlyBiddersNotFound() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final ObjectNode extImp = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set( + "rubicon", mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE)) + .set("appnexus", mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE)))); + + final Imp imp = Imp.builder().ext(extImp).build(); + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final Imp result = DealsProcessor.removeDealsOnlyBiddersWithoutDeals(imp, auctionContext); + + // then + assertThat(result).isEqualTo(Imp.builder().ext(extImp).build()); + } + + @Test + public void populateDealsInfoShouldNotRemoveDealsOnlyBiddersWithoutDealsAndNotDoChangesForDealsOnlyImp() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final ObjectNode extImp = mapper.createObjectNode(); + extImp.set("rubicon", mapper.createObjectNode().set("dealsonly", BooleanNode.TRUE)); + extImp.set("appnexus", mapper.createObjectNode().set("dealsonly", BooleanNode.TRUE)); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build()) + .imp(singletonList(Imp.builder() + .pmp(Pmp.builder().deals(singletonList(Deal.builder().build())).build()) + .ext(extImp) + .build()))); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(result.getBidRequest().getImp()).isSameAs(bidRequest.getImp()); + } + + @Test + public void removeDealsOnlyBiddersWithoutDealsShouldRemoveDealsOnlyBiddersWithoutDealsAndAddDebugMessage() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final ObjectNode extImp = mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set( + "rubicon", mapper.createObjectNode().set("dealsonly", BooleanNode.TRUE)) + .set("appnexus", mapper.createObjectNode().set("dealsonly", BooleanNode.FALSE)))); + + final Imp imp = Imp.builder() + .id("impId") + .pmp(Pmp.builder().deals(emptyList()).build()) + .ext(extImp) + .build(); + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + DealsProcessor.removeDealsOnlyBiddersWithoutDeals(imp, auctionContext); + + // then + assertThat(auctionContext.getDeepDebugLog().entries()).containsOnly( + ExtTraceDeal.of(null, ZonedDateTime.now(CLOCK), ExtTraceDeal.Category.cleanup, + "No Line Items from bidders rubicon matching imp with id impId and ready to serve." + + " Removing impression from request to these bidders because dealsonly flag" + + " is on for them.")); + } + + @Test + public void removeDealsOnlyBiddersWithoutDealsShouldRemoveDealsOnlyAllBiddersWithoutDealsAndAddDebugMessageWhen() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final Imp imp = Imp.builder() + .id("impId1") + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.createObjectNode() + .set("rubicon", mapper.createObjectNode().set("dealsonly", BooleanNode.TRUE))))) + .build(); + final BidRequest bidRequest = givenBidRequest(builder -> builder + .device(Device.builder().ip("ip").ua("ua").build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + DealsProcessor.removeDealsOnlyBiddersWithoutDeals(imp, auctionContext); + + // then + assertThat(auctionContext.getDeepDebugLog().entries()).containsOnly( + ExtTraceDeal.of(null, ZonedDateTime.now(CLOCK), ExtTraceDeal.Category.cleanup, + "No Line Items from bidders rubicon matching imp with id impId1 and ready to serve. " + + "Removing imp from requests to all bidders because dealsonly flag is on for" + + " these bidders and no other valid bidders in imp.")); + } + + @Test + public void populateDealsInfoShouldNotPopulateDeviceGeoIfGeolocationServiceIsNotDefined() { + // given + dealsProcessor = new DealsProcessor( + lineItemService, + deviceInfoService, + null, + userService, + CLOCK, + jacksonMapper, + criteriaLogManager); + givenResultForLineItemDeviceGeoUserServices(); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().build())); + + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(result.getBidRequest().getDevice().getGeo()).isNull(); + } + + @Test + public void populateDealsInfoShouldCreateExtDeviceIfDeviceInfoIsNotEmptyAndExtDidNotExistBefore() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(geoLocationService.lookup(any(), any())) + .willReturn(Future.succeededFuture(GeoInfo.builder() + .vendor("geoVendor") + .build())); + + given(deviceInfoService.getDeviceInfo(any())) + .willReturn(Future.succeededFuture(DeviceInfo.builder().vendor("deviceVendor") + .browser("browser").build())); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().build())); + + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + final ExtDevice expectedExtDevice = ExtDevice.of(null, null); + expectedExtDevice.addProperty("deviceVendor", + mapper.valueToTree(ExtDeviceVendor.builder().browser("browser").build())); + // then + assertThat(result.getBidRequest().getDevice().getExt()).isEqualTo(expectedExtDevice); + } + + @Test + public void populateDealsInfoShouldCreateExtDeviceIfGeoInfoIsNotEmptyAndExtDidNotExistBefore() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + given(geoLocationService.lookup(any(), any())) + .willReturn(Future.succeededFuture(GeoInfo.builder() + .vendor("geoVendor") + .connectionSpeed("100") + .continent("continent") + .build())); + + given(deviceInfoService.getDeviceInfo(any())) + .willReturn(Future.succeededFuture(DeviceInfo.builder() + .vendor("deviceVendor").build())); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().build())); + + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + final ExtDevice expectedExtDevice = ExtDevice.of(null, null); + expectedExtDevice.addProperty("geoVendor", + mapper.valueToTree(ExtDeviceVendor.builder().connspeed("100").build())); + assertThat(result.getBidRequest().getDevice().getExt()) + .isEqualTo(expectedExtDevice); + } + + @Test + public void populateDealsInfoShouldUseGeoInfoFromContext() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().build())); + final GeoInfo geoInfo = GeoInfo.builder() + .vendor("vendor") + .city("city") + .build(); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())).toBuilder() + .geoInfo(geoInfo) + .build(); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + final ExtGeo expectedExtGeo = ExtGeo.of(); + expectedExtGeo.addProperty("vendor", mapper.valueToTree(ExtGeoVendor.builder().city("city").build())); + assertThat(result.getBidRequest().getDevice()).isEqualTo(Device.builder() + .geo(Geo.builder().ext(expectedExtGeo).build()) + .build()); + verifyZeroInteractions(geoLocationService); + assertThat(result.getGeoInfo()).isSameAs(geoInfo); + } + + @Test + public void populateDealsInfoShouldStoreGeoInfoInContext() { + // given + givenResultForLineItemDeviceGeoUserServices(); + + final GeoInfo geoInfo = GeoInfo.builder() + .vendor("vendor") + .city("city") + .build(); + given(geoLocationService.lookup(any(), any())).willReturn(Future.succeededFuture(geoInfo)); + + final BidRequest bidRequest = givenBidRequest(builder -> builder + .imp(singletonList(Imp.builder().ext(mapper.createObjectNode()).build())) + .device(Device.builder().build())); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, givenAccount(identity())); + + // when + final AuctionContext result = dealsProcessor.populateDealsInfo(auctionContext).result(); + + // then + assertThat(result.getGeoInfo()).isSameAs(geoInfo); + } + + private void givenResultForLineItemDeviceGeoUserServices() { + given(lineItemService.accountHasDeals(any())).willReturn(true); + + given(deviceInfoService.getDeviceInfo(any())) + .willReturn(Future.failedFuture("deviceInfoService error")); + given(geoLocationService.lookup(any(), any())) + .willReturn(Future.failedFuture("geoLocationService error")); + given(userService.getUserDetails(any(), any())) + .willReturn(Future.failedFuture("userService error")); + + given(lineItemService.findMatchingLineItems(any(), any())) + .willReturn(MatchLineItemsResult.of(emptyList())); + } + + private static BidRequest givenBidRequest( + Function customizer) { + return customizer.apply(BidRequest.builder()).build(); + } + + private static Account givenAccount(Function customizer) { + return customizer.apply(Account.builder().id("accountId")).build(); + } + + private static AuctionContext givenAuctionContext(BidRequest bidRequest, Account account) { + return AuctionContext.builder() + .bidRequest(bidRequest) + .account(account) + .deepDebugLog(DeepDebugLog.create(true, CLOCK)) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/deals/DeliveryProgressReportFactoryTest.java b/src/test/java/org/prebid/server/deals/DeliveryProgressReportFactoryTest.java new file mode 100644 index 00000000000..ef9daed10eb --- /dev/null +++ b/src/test/java/org/prebid/server/deals/DeliveryProgressReportFactoryTest.java @@ -0,0 +1,201 @@ +package org.prebid.server.deals; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.lineitem.DeliveryProgress; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.lineitem.LineItemStatus; +import org.prebid.server.deals.lineitem.LostToLineItem; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.deals.proto.DeliverySchedule; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.deals.proto.Token; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; +import org.prebid.server.deals.proto.report.DeliveryProgressReportBatch; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.LongAdder; + +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +public class DeliveryProgressReportFactoryTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private LineItemService lineItemService; + + private DeliveryProgressReportFactory deliveryProgressReportFactory; + + private ZonedDateTime now; + + @Before + public void setUp() { + deliveryProgressReportFactory = new DeliveryProgressReportFactory( + DeploymentProperties.builder().pbsHostId("pbsHost").pbsRegion("pbsRegion") + .pbsVendor("pbsVendor").build(), 2, lineItemService); + + now = ZonedDateTime.now(Clock.fixed(Instant.parse("2019-07-26T10:00:00Z"), ZoneOffset.UTC)); + } + + @Test + public void fromDeliveryProgressShouldCreateReportWithTop2Competitors() { + // given + given(lineItemService.getLineItemById(anyString())) + .willReturn(LineItem.of( + LineItemMetaData.builder() + .accountId("accountId") + .deliverySchedules(singletonList(DeliverySchedule.builder() + .startTimeStamp(now.minusHours(1)) + .endTimeStamp(now.plusHours(1)) + .updatedTimeStamp(now.minusHours(1)) + .build())) + .source("rubicon") + .build(), + null, null, now)); + + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now.minusHours(3), lineItemService); + deliveryProgress.setEndTimeStamp(now.minusHours(2)); + + final Map lostTo = new ConcurrentHashMap<>(); + lostTo.put("lineItemId1", LostToLineItem.of("lineItemId1", makeLongAdderWithValue(100L))); + lostTo.put("lineItemId2", LostToLineItem.of("lineItemId2", makeLongAdderWithValue(50L))); + lostTo.put("lineItemId3", LostToLineItem.of("lineItemId3", makeLongAdderWithValue(80L))); + lostTo.put("lineItemId4", LostToLineItem.of("lineItemId4", makeLongAdderWithValue(120L))); + deliveryProgress.getLineItemIdToLost().put("lineItemId5", lostTo); + deliveryProgress.getLineItemStatuses().put("lineItemId5", LineItemStatus.of("lineItemId5")); + + // when + final DeliveryProgressReport deliveryProgressReport = deliveryProgressReportFactory + .fromDeliveryProgress(deliveryProgress, now, false); + + // then + assertThat(deliveryProgressReport.getLineItemStatus()) + .flatExtracting(org.prebid.server.deals.proto.report.LineItemStatus::getLostToLineItems) + .extracting(org.prebid.server.deals.proto.report.LostToLineItem::getLineItemSource, + org.prebid.server.deals.proto.report.LostToLineItem::getLineItemId, + org.prebid.server.deals.proto.report.LostToLineItem::getCount) + .containsOnly( + tuple("rubicon", "lineItemId4", 120L), + tuple("rubicon", "lineItemId1", 100L)); + } + + @Test + public void fromDeliveryProgressShouldCreateOverallReport() { + // given + given(lineItemService.getLineItemById(anyString())) + .willReturn(LineItem.of( + LineItemMetaData.builder() + .accountId("accountId") + .deliverySchedules(singletonList(DeliverySchedule.builder() + .startTimeStamp(now.minusHours(1)) + .endTimeStamp(now.plusHours(1)) + .updatedTimeStamp(now.minusHours(1)) + .tokens(singleton(Token.of(1, 100))) + .build())) + .source("rubicon") + .build(), + null, null, now)); + + final DeliveryProgress deliveryProgress = mock(DeliveryProgress.class); + given(deliveryProgress.getRequests()).willReturn(new LongAdder()); + given(deliveryProgress.getLineItemStatuses()).willReturn(singletonMap("lineItemId1", + LineItemStatus.of("lineItemId1"))); + deliveryProgress.setEndTimeStamp(now.minusHours(2)); + + // when + final DeliveryProgressReport deliveryProgressReport = deliveryProgressReportFactory + .fromDeliveryProgress(deliveryProgress, now, true); + + // then + assertThat(deliveryProgressReport.getLineItemStatus()) + .extracting(org.prebid.server.deals.proto.report.LineItemStatus::getReadyAt, + org.prebid.server.deals.proto.report.LineItemStatus::getPacingFrequency, + org.prebid.server.deals.proto.report.LineItemStatus::getSpentTokens) + .containsOnly(tuple("2019-07-26T10:00:00.000Z", 72000L, 0L)); + } + + @Test + public void fromDeliveryProgressShouldDropLineItemsWithoutDeliverySchedule() { + // given + given(lineItemService.getLineItemById(anyString())) + .willReturn(LineItem.of( + LineItemMetaData.builder() + .accountId("accountId") + .source("rubicon") + .build(), + null, null, now)); + + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now.minusHours(3), lineItemService); + deliveryProgress.setEndTimeStamp(now.minusHours(2)); + + // when + final DeliveryProgressReport deliveryProgressReport = deliveryProgressReportFactory + .fromDeliveryProgress(deliveryProgress, now, false); + + // then + assertThat(deliveryProgressReport.getLineItemStatus()).isEmpty(); + } + + @Test + public void batchFromDeliveryProgressShouldCreateTwoReportsInBatchWithSameId() { + // given + given(lineItemService.getLineItemById(anyString())) + .willReturn(LineItem.of( + LineItemMetaData.builder() + .accountId("accountId") + .deliverySchedules(singletonList(DeliverySchedule.builder() + .startTimeStamp(now.minusHours(1)) + .endTimeStamp(now.plusHours(1)) + .updatedTimeStamp(now.minusHours(1)) + .build())) + .source("rubicon") + .build(), + null, null, now)); + + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now.minusHours(3), lineItemService); + deliveryProgress.setEndTimeStamp(now.minusHours(2)); + + deliveryProgress.getLineItemStatuses().put("lineItemId1", LineItemStatus.of("lineItemId1")); + deliveryProgress.getLineItemStatuses().put("lineItemId2", LineItemStatus.of("lineItemId2")); + deliveryProgress.getLineItemStatuses().put("lineItemId3", LineItemStatus.of("lineItemId3")); + + // when + final DeliveryProgressReportBatch deliveryProgressReportBatch = deliveryProgressReportFactory + .batchFromDeliveryProgress(deliveryProgress, null, now, 2, false); + + // then + final Set reports = deliveryProgressReportBatch.getReports(); + assertThat(reports).hasSize(2) + .extracting(DeliveryProgressReport::getReportId) + .containsOnly(deliveryProgressReportBatch.getReportId()); + assertThat(reports) + .extracting(deliveryProgressReport -> deliveryProgressReport.getLineItemStatus().size()) + .containsOnly(1, 2); + } + + private static LongAdder makeLongAdderWithValue(Long value) { + final LongAdder longAdder = new LongAdder(); + longAdder.add(value); + return longAdder; + } +} diff --git a/src/test/java/org/prebid/server/deals/DeliveryProgressServiceTest.java b/src/test/java/org/prebid/server/deals/DeliveryProgressServiceTest.java new file mode 100644 index 00000000000..c4ca10d531e --- /dev/null +++ b/src/test/java/org/prebid/server/deals/DeliveryProgressServiceTest.java @@ -0,0 +1,361 @@ +package org.prebid.server.deals; + +import org.assertj.core.api.iterable.ThrowingExtractor; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.deals.lineitem.DeliveryPlan; +import org.prebid.server.deals.lineitem.DeliveryProgress; +import org.prebid.server.deals.lineitem.DeliveryToken; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.lineitem.LineItemStatus; +import org.prebid.server.deals.lineitem.LostToLineItem; +import org.prebid.server.deals.model.DeliveryProgressProperties; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.deals.proto.DeliverySchedule; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.deals.proto.Price; +import org.prebid.server.deals.proto.Token; +import org.prebid.server.deals.proto.report.Event; +import org.prebid.server.deals.proto.report.LineItemStatusReport; +import org.prebid.server.log.CriteriaLogManager; +import org.prebid.server.settings.model.Account; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.LongAdder; +import java.util.stream.IntStream; + +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +public class DeliveryProgressServiceTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private LineItemService lineItemService; + @Mock + private DeliveryStatsService deliveryStatsService; + @Mock + private DeliveryProgressReportFactory deliveryProgressReportFactory; + @Mock + private CriteriaLogManager criteriaLogManager; + + private DeliveryProgressService deliveryProgressService; + + private final Clock clock = Clock.fixed(Instant.parse("2019-07-26T10:00:00Z"), ZoneOffset.UTC); + private ZonedDateTime now; + + @Before + public void setUp() { + now = ZonedDateTime.now(clock); + + deliveryProgressService = new DeliveryProgressService( + DeliveryProgressProperties.of(200L, 20), + lineItemService, + deliveryStatsService, + deliveryProgressReportFactory, + clock, + criteriaLogManager); + } + + @Test + public void updateLineItemsShouldUpdateCurrentDeliveryReportIfUpdatedPlanUpdateTimeStampIsInFuture() { + // given + final LineItemMetaData firstPlanResponse = givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), now); + + final LineItemMetaData secondPlanResponse = givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now.plusMinutes(1), singleton(Token.of(1, 200)))), now); + + LineItem lineItem1 = LineItem.of(firstPlanResponse, null, null, now); + LineItem lineItem12 = LineItem.of(secondPlanResponse, null, null, now); + + given(lineItemService.getLineItems()).willReturn( + singletonList(lineItem1), + singletonList(lineItem12)); + + given(lineItemService.getLineItemById(anyString())).willReturn( + lineItem12); + + // when + deliveryProgressService.processDeliveryProgressUpdateEvent(); + recordLineItemsServed(40, "lineItem1"); + deliveryProgressService.processDeliveryProgressUpdateEvent(); + + // then + // trigger overall progress passing to report factory + deliveryProgressService.getOverallDeliveryProgressReport(); + + final ArgumentCaptor overallProgressCaptor = ArgumentCaptor.forClass(DeliveryProgress.class); + verify(deliveryProgressReportFactory).fromDeliveryProgress(overallProgressCaptor.capture(), any(), + anyBoolean()); + + final DeliveryProgress overallProgress = overallProgressCaptor.getValue(); + assertThat(overallProgress).isNotNull(); + assertThat(overallProgress.getLineItemStatuses()).isNotNull(); + assertThat(overallProgress.getLineItemStatuses().keySet()) + .containsOnly("lineItem1"); + final LineItemStatus overallLineItemStatus = overallProgress.getLineItemStatuses().get("lineItem1"); + assertThat(overallLineItemStatus).isNotNull(); + assertThat(overallLineItemStatus.getDeliveryPlans()) + .extracting(DeliveryPlan::getPlanId, DeliveryPlan::getStartTimeStamp, DeliveryPlan::getEndTimeStamp, + DeliveryPlan::getUpdatedTimeStamp) + .containsOnly(tuple("planId1", now.minusHours(1), now.plusHours(1), now.plusMinutes(1))); + assertThat(overallLineItemStatus.getDeliveryPlans()) + .flatExtracting(DeliveryPlan::getDeliveryTokens) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, token -> token.getSpent().sum()) + .containsOnly(tuple(1, 200, 40L)); + + // trigger current progress passing to delivery stats + deliveryProgressService.shutdown(); + + final ArgumentCaptor currentProgressCaptor = ArgumentCaptor.forClass(DeliveryProgress + .class); + verify(deliveryStatsService).addDeliveryProgress(currentProgressCaptor.capture(), any()); + + final DeliveryProgress currentProgress = currentProgressCaptor.getValue(); + assertThat(currentProgress).isNotNull(); + assertThat(currentProgress.getLineItemStatuses()).isNotNull(); + final LineItemStatus currentLineItemStatus = currentProgress.getLineItemStatuses().get("lineItem1"); + assertThat(currentLineItemStatus).isNotNull(); + assertThat(currentLineItemStatus.getDeliveryPlans()) + .extracting(DeliveryPlan::getPlanId, DeliveryPlan::getStartTimeStamp, DeliveryPlan::getEndTimeStamp, + DeliveryPlan::getUpdatedTimeStamp) + .containsOnly(tuple("planId1", now.minusHours(1), now.plusHours(1), now.plusMinutes(1))); + assertThat(currentLineItemStatus.getDeliveryPlans()) + .flatExtracting(DeliveryPlan::getDeliveryTokens) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, token -> token.getSpent().sum()) + .containsOnly(tuple(1, 200, 40L)); + } + + @SuppressWarnings("unchecked") + @Test + public void processAuctionEventShouldUpdateCurrentPlan() { + // given + final String lineItemId1 = "lineItemId1"; + final String lineItemId2 = "lineItemId2"; + + final LineItem lineItem1 = LineItem.of(givenLineItemMetaData(lineItemId1, "1001", "rubicon", + singletonList(givenDeliverySchedule("plan1", now.minusHours(1), now.plusHours(1), + new HashSet<>(asList(Token.of(1, 100), Token.of(2, 100))))), + now), null, null, now); + + final LineItem lineItem2 = LineItem.of(givenLineItemMetaData(lineItemId2, "1001", "rubicon", + singletonList(givenDeliverySchedule("plan2", now.minusHours(1), now.plusHours(1), + new HashSet<>(asList(Token.of(1, 100), Token.of(2, 100))))), + now), null, null, now); + + given(lineItemService.getLineItemById(eq(lineItemId1))).willReturn(lineItem1); + given(lineItemService.getLineItemById(eq(lineItemId2))).willReturn(lineItem2); + + recordLineItemsServed(150, lineItemId1); + + final TxnLog txnLog = TxnLog.create(); + txnLog.lineItemSentToClientAsTopMatch().addAll(asList(lineItemId1, lineItemId2)); + txnLog.lineItemsSentToClient().addAll(asList(lineItemId1, lineItemId2)); + txnLog.lineItemsMatchedDomainTargeting().addAll(asList(lineItemId1, lineItemId2)); + txnLog.lineItemsMatchedWholeTargeting().addAll(asList(lineItemId1, lineItemId2)); + txnLog.lineItemsMatchedTargetingFcapped().addAll(asList(lineItemId1, lineItemId2)); + txnLog.lineItemsMatchedTargetingFcapLookupFailed().addAll(asList(lineItemId1, lineItemId2)); + txnLog.lineItemsSentToBidder().put("rubicon", new HashSet<>(asList(lineItemId1, lineItemId2))); + txnLog.lineItemsSentToBidderAsTopMatch().put("rubicon", singleton(lineItemId1)); + txnLog.lineItemsSentToBidderAsTopMatch().put("appnexus", singleton(lineItemId2)); + txnLog.lineItemsReceivedFromBidder().put("rubicon", new HashSet<>(asList(lineItemId1, lineItemId2))); + txnLog.lineItemsResponseInvalidated().addAll(asList(lineItemId1, lineItemId2)); + txnLog.lostMatchingToLineItems().put(lineItemId1, singleton(lineItemId2)); + + // when and then + deliveryProgressService.processAuctionEvent(AuctionContext.builder() + .account(Account.empty("1001")) + .txnLog(txnLog) + .build()); + deliveryProgressService.createDeliveryProgressReports(now); + + final ArgumentCaptor deliveryProgressReportCaptor = + ArgumentCaptor.forClass(DeliveryProgress.class); + verify(deliveryStatsService).addDeliveryProgress(deliveryProgressReportCaptor.capture(), any()); + final DeliveryProgress deliveryProgress = deliveryProgressReportCaptor.getValue(); + assertThat(deliveryProgress.getRequests().sum()).isEqualTo(151); + + final Set lineItemStatuses = new HashSet<>(deliveryProgress.getLineItemStatuses().values()); + + checkLineItemStatusStats(lineItemStatuses, LineItemStatus::getDomainMatched, 1L, 1L); + checkLineItemStatusStats(lineItemStatuses, LineItemStatus::getTargetMatched, 1L, 1L); + checkLineItemStatusStats(lineItemStatuses, LineItemStatus::getTargetMatchedButFcapped, 1L, 1L); + checkLineItemStatusStats(lineItemStatuses, LineItemStatus::getTargetMatchedButFcapLookupFailed, 1L, 1L); + checkLineItemStatusStats(lineItemStatuses, LineItemStatus::getSentToBidder, 1L, 1L); + checkLineItemStatusStats(lineItemStatuses, LineItemStatus::getReceivedFromBidder, 1L, 1L); + checkLineItemStatusStats(lineItemStatuses, LineItemStatus::getReceivedFromBidderInvalidated, 1L, 1L); + checkLineItemStatusStats(lineItemStatuses, LineItemStatus::getSentToClientAsTopMatch, 151L, 1L); + checkLineItemStatusStats(lineItemStatuses, LineItemStatus::getSentToBidderAsTopMatch, 1L, 1L); + checkLineItemStatusStats(lineItemStatuses, LineItemStatus::getSentToClient, 1L, 1L); + + assertThat(deliveryProgress.getLineItemIdToLost()) + .extracting(lineItemId1) + .extracting(lineItemId2) + .extracting(lostToLineItem -> ((LostToLineItem) lostToLineItem).getCount().sum()) + .containsOnly(1L); + + assertThat(lineItemStatuses) + .flatExtracting(LineItemStatus::getDeliveryPlans) + .flatExtracting(DeliveryPlan::getDeliveryTokens) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, + deliveryToken -> deliveryToken.getSpent().sum()) + .containsOnly( + tuple(1, 100, 100L), + tuple(2, 100, 51L), + tuple(1, 100, 1L), + tuple(2, 100, 0L)); + + assertThat(lineItem1.getActiveDeliveryPlan().getDeliveryTokens()) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, token -> token.getSpent().sum()) + .containsExactly( + tuple(1, 100, 100L), + tuple(2, 100, 51L)); + + assertThat(lineItem2.getActiveDeliveryPlan().getDeliveryTokens()) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, token -> token.getSpent().sum()) + .containsExactly( + tuple(1, 100, 1L), + tuple(2, 100, 0L)); + } + + @Test + public void trackWinEventShouldCreateLineItemStatusAndUpdateWinEventsMetric() { + // given + final LineItem lineItem = LineItem.of(givenLineItemMetaData("lineItemId1", "1001", "rubicon", + singletonList(givenDeliverySchedule("plan1", now.minusHours(1), now.plusHours(1), + new HashSet<>(asList(Token.of(1, 100), Token.of(2, 100))))), + now), null, null, now); + + given(lineItemService.getLineItemById(eq("lineItemId1"))).willReturn(lineItem); + + // when + deliveryProgressService.processLineItemWinEvent("lineItemId1"); + + // then + // trigger current progress passing to delivery stats + deliveryProgressService.shutdown(); + + final ArgumentCaptor currentProgressCaptor = ArgumentCaptor.forClass(DeliveryProgress.class); + verify(deliveryStatsService).addDeliveryProgress(currentProgressCaptor.capture(), any()); + + final DeliveryProgress currentProgress = currentProgressCaptor.getValue(); + assertThat(currentProgress).isNotNull(); + assertThat(currentProgress.getLineItemStatuses().entrySet()).hasSize(1) + .extracting(Map.Entry::getValue) + .flatExtracting(LineItemStatus::getEvents) + .extracting(Event::getType, event -> event.getCount().sum()) + .containsOnly(tuple("win", 1L)); + } + + @Test + public void getLineItemStatusReportShouldReturnExpectedResult() { + // given + final DeliverySchedule deliverySchedule = givenDeliverySchedule("plan1", now.minusHours(1), now.plusHours(1), + singleton(Token.of(1, 100))); + final LineItem lineItem = LineItem.of(givenLineItemMetaData("lineItemId1", "1001", "rubicon", + singletonList(deliverySchedule), now), null, null, now); + given(lineItemService.getLineItemById(anyString())).willReturn(lineItem); + + // when + final LineItemStatusReport report = deliveryProgressService.getLineItemStatusReport("lineItemId1", now); + + // then + assertThat(report).isEqualTo(LineItemStatusReport.builder() + .lineItemId("lineItemId1") + .deliverySchedule(org.prebid.server.deals.proto.report.DeliverySchedule.builder() + .planId("plan1") + .planStartTimeStamp("2019-07-26T09:00:00.000Z") + .planExpirationTimeStamp("2019-07-26T11:00:00.000Z") + .planUpdatedTimeStamp("2019-07-26T09:00:00.000Z") + .tokens(singleton(org.prebid.server.deals.proto.report.Token.of(1, 100, 0L, null))) + .build()) + .readyToServeTimestamp(now) + .spentTokens(0L) + .pacingFrequency(72000L) + .build()); + } + + private static LineItemMetaData givenLineItemMetaData( + String lineItemId, String account, String bidderCode, List deliverySchedules, + ZonedDateTime now) { + + return LineItemMetaData.builder() + .lineItemId(lineItemId) + .dealId("dealId") + .status("active") + .accountId(account) + .source(bidderCode) + .price(Price.of(BigDecimal.ONE, "USD")) + .relativePriority(5) + .startTimeStamp(now.minusHours(1)) + .endTimeStamp(now.plusHours(1)) + .updatedTimeStamp(now) + .deliverySchedules(deliverySchedules) + .build(); + } + + private static DeliverySchedule givenDeliverySchedule(String planId, ZonedDateTime start, ZonedDateTime end, + ZonedDateTime updated, Set tokens) { + return DeliverySchedule.builder() + .planId(planId) + .startTimeStamp(start) + .endTimeStamp(end) + .updatedTimeStamp(updated) + .tokens(tokens) + .build(); + } + + private static DeliverySchedule givenDeliverySchedule(String planId, ZonedDateTime start, ZonedDateTime end, + Set tokens) { + return givenDeliverySchedule(planId, start, end, start, tokens); + } + + private static void checkLineItemStatusStats(Set lineItemStatuses, + ThrowingExtractor stat, + Long... values) { + assertThat(lineItemStatuses) + .extracting(stat) + .extracting(LongAdder::sum) + .containsOnly(values); + } + + private void recordLineItemsServed(int times, String... lineItemIds) { + final TxnLog txnLog = TxnLog.create(); + AuctionContext auctionContext = AuctionContext.builder() + .account(Account.empty("1001")) + .txnLog(txnLog) + .build(); + txnLog.lineItemSentToClientAsTopMatch().addAll(asList(lineItemIds)); + IntStream.range(0, times).forEach(i -> deliveryProgressService.processAuctionEvent(auctionContext)); + + } +} diff --git a/src/test/java/org/prebid/server/deals/DeliveryStatsServiceTest.java b/src/test/java/org/prebid/server/deals/DeliveryStatsServiceTest.java new file mode 100644 index 00000000000..7cfa3dedaa4 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/DeliveryStatsServiceTest.java @@ -0,0 +1,428 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import org.apache.http.HttpHeaders; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.lineitem.DeliveryProgress; +import org.prebid.server.deals.model.AlertPriority; +import org.prebid.server.deals.model.DeliveryStatsProperties; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; +import org.prebid.server.deals.proto.report.DeliveryProgressReportBatch; +import org.prebid.server.deals.proto.report.LineItemStatus; +import org.prebid.server.metric.Metrics; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.NavigableSet; +import java.util.concurrent.TimeoutException; +import java.util.zip.GZIPInputStream; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class DeliveryStatsServiceTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private DeliveryProgressReportFactory deliveryProgressReportFactory; + @Mock + private AlertHttpService alertHttpService; + @Mock + private HttpClient httpClient; + @Mock + private Clock clock; + @Mock + private Metrics metrics; + + @Mock + private Vertx vertx; + + private DeliveryStatsService deliveryStatsService; + + private ZonedDateTime now; + + @Mock + private LineItemService lineItemService; + + @Before + public void setUp() { + now = ZonedDateTime.now(Clock.fixed(Instant.parse("2019-07-26T10:00:00Z"), ZoneOffset.UTC)); + given(clock.instant()).willReturn(now.toInstant()); + given(clock.getZone()).willReturn(ZoneOffset.UTC); + + deliveryStatsService = new DeliveryStatsService( + DeliveryStatsProperties.builder() + .endpoint("localhost/delivery") + .cachedReportsNumber(3) + .timeoutMs(500L) + .reportsIntervalMs(0) + .batchesIntervalMs(0) + .username("username") + .password("password") + .build(), + deliveryProgressReportFactory, + alertHttpService, + httpClient, + metrics, + clock, + vertx, + jacksonMapper); + } + + @SuppressWarnings("unchecked") + @Test + public void sendDeliveryProgressReportShouldSendBothBatches() { + // given + givenDeliveryProgressHttpResponse(httpClient, 200, null); + + given(deliveryProgressReportFactory.batchFromDeliveryProgress(any(), any(), any(), anyInt(), anyBoolean())) + .willReturn( + DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("1") + .lineItemStatus(emptySet()) + .dataWindowEndTimeStamp(now.minusHours(2).toString()).build()), "1", + now.minusHours(2).toString()), + DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("2") + .lineItemStatus(emptySet()) + .dataWindowEndTimeStamp(now.minusHours(1).toString()).build()), "2", + now.minusHours(1).toString())); + + final DeliveryProgress deliveryProgress1 = DeliveryProgress.of(now.minusHours(3), lineItemService); + final DeliveryProgress deliveryProgress2 = DeliveryProgress.of(now.minusHours(2), lineItemService); + + // when + deliveryStatsService.addDeliveryProgress(deliveryProgress1, emptyMap()); + deliveryStatsService.addDeliveryProgress(deliveryProgress2, emptyMap()); + deliveryStatsService.sendDeliveryProgressReports(); + + // then + verify(httpClient, times(2)).post(anyString(), any(), anyString(), anyLong()); + final NavigableSet reports = (NavigableSet) + ReflectionTestUtils.getField(deliveryStatsService, "requiredBatches"); + assertThat(reports).isEmpty(); + verify(metrics, times(2)).updateDeliveryRequestMetric(eq(true)); + } + + @SuppressWarnings("unchecked") + @Test + public void sendDeliveryProgressReportShouldSendOneBatchAndCacheFailedBatch() { + // given + final DeliveryProgress deliveryProgress1 = DeliveryProgress.of(now.minusHours(3), lineItemService); + final DeliveryProgress deliveryProgress2 = DeliveryProgress.of(now.minusHours(2), lineItemService); + + given(deliveryProgressReportFactory.batchFromDeliveryProgress(any(), any(), any(), anyInt(), anyBoolean())) + .willReturn( + DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("1") + .lineItemStatus(emptySet()) + .dataWindowEndTimeStamp(now.minusHours(2).toString()).build()), "1", + now.minusHours(2).toString()), + DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("2") + .lineItemStatus(emptySet()) + .dataWindowEndTimeStamp(now.minusHours(1).toString()).build()), "2", + now.minusHours(1).toString())); + + deliveryStatsService.addDeliveryProgress(deliveryProgress1, null); + deliveryStatsService.addDeliveryProgress(deliveryProgress2, null); + + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, null)), + Future.failedFuture(new TimeoutException())); + + deliveryStatsService.sendDeliveryProgressReports(); + + // when and then + verify(httpClient, times(2)).post(anyString(), any(), anyString(), anyLong()); + final NavigableSet reports = (NavigableSet) + ReflectionTestUtils.getField(deliveryStatsService, "requiredBatches"); + assertThat(reports).hasSize(1); + verify(metrics).updateDeliveryRequestMetric(eq(true)); + verify(metrics).updateDeliveryRequestMetric(eq(false)); + } + + @Test + public void sendDeliveryProgressReportShouldSendFirstReportFromFirstBatchFailOnSecondsAndCacheOther() { + // given + final DeliveryProgress deliveryProgress1 = DeliveryProgress.of(now.minusHours(3), lineItemService); + final DeliveryProgress deliveryProgress2 = DeliveryProgress.of(now.minusHours(2), lineItemService); + + given(deliveryProgressReportFactory.batchFromDeliveryProgress(any(), any(), any(), anyInt(), anyBoolean())) + .willReturn( + DeliveryProgressReportBatch.of( + new HashSet<>(asList(DeliveryProgressReport.builder().reportId("1") + .lineItemStatus(singleton(LineItemStatus.builder().lineItemId("1") + .build())) + .dataWindowEndTimeStamp(now.minusHours(2).toString()).build(), + DeliveryProgressReport.builder().reportId("1") + .lineItemStatus(singleton(LineItemStatus.builder().lineItemId("2") + .build())) + .dataWindowEndTimeStamp(now.minusHours(2).toString()).build())), + "1", now.minusHours(2).toString()), + DeliveryProgressReportBatch.of( + new HashSet<>(asList(DeliveryProgressReport.builder().reportId("2") + .lineItemStatus(singleton(LineItemStatus.builder().lineItemId("1") + .build())) + .dataWindowEndTimeStamp(now.minusHours(3).toString()).build(), + DeliveryProgressReport.builder().reportId("2") + .lineItemStatus(singleton(LineItemStatus.builder().lineItemId("2") + .build())) + .dataWindowEndTimeStamp(now.minusHours(3).toString()).build())), + "2", now.minusHours(3).toString())); + + deliveryStatsService.addDeliveryProgress(deliveryProgress1, null); + deliveryStatsService.addDeliveryProgress(deliveryProgress2, null); + + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, null)), + Future.failedFuture(new TimeoutException())); + + deliveryStatsService.sendDeliveryProgressReports(); + + // when and then + verify(httpClient, times(2)).post(anyString(), any(), anyString(), anyLong()); + final NavigableSet reports = (NavigableSet) + ReflectionTestUtils.getField(deliveryStatsService, "requiredBatches"); + assertThat(reports).hasSize(2) + .flatExtracting(DeliveryProgressReportBatch::getReports) + .hasSize(3); + verify(metrics).updateDeliveryRequestMetric(eq(true)); + verify(metrics).updateDeliveryRequestMetric(eq(false)); + } + + @Test + public void sendDeliveryProgressReportShouldHandleFailedBatchesCacheLimitWhenResponseStatusIsBadRequest() { + // given + given(deliveryProgressReportFactory.batchFromDeliveryProgress(any(), any(), any(), anyInt(), anyBoolean())) + .willReturn( + DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("1") + .dataWindowEndTimeStamp(now.minusHours(4).toString()).build()), "1", + now.minusHours(4).toString()), + DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("2") + .dataWindowEndTimeStamp(now.minusHours(3).toString()).build()), "2", + now.minusHours(3).toString()), + DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("3") + .dataWindowEndTimeStamp(now.minusHours(2).toString()).build()), "3", + now.minusHours(2).toString()), + DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("4") + .dataWindowEndTimeStamp(now.minusHours(1).toString()).build()), "4", + now.minusHours(1).toString())); + + final DeliveryProgress deliveryProgress1 = DeliveryProgress.of(now.minusHours(5), lineItemService); + final DeliveryProgress deliveryProgress2 = DeliveryProgress.of(now.minusHours(4), lineItemService); + final DeliveryProgress deliveryProgress3 = DeliveryProgress.of(now.minusHours(3), lineItemService); + final DeliveryProgress deliveryProgress4 = DeliveryProgress.of(now.minusHours(2), lineItemService); + + deliveryStatsService.addDeliveryProgress(deliveryProgress1, null); + deliveryStatsService.addDeliveryProgress(deliveryProgress2, null); + deliveryStatsService.addDeliveryProgress(deliveryProgress3, null); + deliveryStatsService.addDeliveryProgress(deliveryProgress4, null); + + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(400, null, null))); + + deliveryStatsService.sendDeliveryProgressReports(); + + // when and then + verify(httpClient, times(1)).post(anyString(), any(), anyString(), anyLong()); + @SuppressWarnings("unchecked") final NavigableSet reports = (NavigableSet) + ReflectionTestUtils.getField(deliveryStatsService, "requiredBatches"); + assertThat(reports).hasSize(3); + } + + @Test + public void sendDeliveryProgressReportShouldShouldRemoveReportFromQueueWhenDelStatsRespondWith409Conflict() { + // given + givenDeliveryProgressHttpResponse(httpClient, 409, null); + + given(deliveryProgressReportFactory.batchFromDeliveryProgress(any(), any(), any(), anyInt(), anyBoolean())) + .willReturn(DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("1") + .lineItemStatus(emptySet()) + .dataWindowEndTimeStamp(now.minusHours(2).toString()).build()), "1", + now.minusHours(2).toString())); + + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now.minusHours(3), lineItemService); + + // when + deliveryStatsService.addDeliveryProgress(deliveryProgress, emptyMap()); + deliveryStatsService.sendDeliveryProgressReports(); + + // then + final NavigableSet reports = + (NavigableSet) ReflectionTestUtils.getField(deliveryStatsService, "requiredBatches"); + assertThat(reports).isEmpty(); + } + + @Test + public void sendDeliveryProgressReportShouldCallAlertServiceWhenRequestFailed() { + // given + given(deliveryProgressReportFactory.batchFromDeliveryProgress(any(), any(), any(), anyInt(), anyBoolean())) + .willReturn(DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("1") + .dataWindowEndTimeStamp(now.minusHours(2).toString()).build()), "1", + now.minusHours(2).toString())); + + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now.minusHours(3), lineItemService); + + deliveryStatsService.addDeliveryProgress(deliveryProgress, emptyMap()); + + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.failedFuture(new TimeoutException("Timeout"))); + + // when + deliveryStatsService.sendDeliveryProgressReports(); + + // then + verify(alertHttpService).alertWithPeriod(eq("deliveryStats"), eq("pbs-delivery-stats-client-error"), + eq(AlertPriority.MEDIUM), + eq("Report was not send to delivery stats service with a reason: Sending report with id = 1 failed" + + " in a reason: Timeout")); + } + + @Test + public void sendDeliveryProgressReportShouldCallAlertServiceResetWhenRequestWasSuccessful() { + // given + givenDeliveryProgressHttpResponse(httpClient, 200, null); + + given(deliveryProgressReportFactory.batchFromDeliveryProgress(any(), any(), any(), anyInt(), anyBoolean())) + .willReturn(DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("1") + .lineItemStatus(emptySet()) + .dataWindowEndTimeStamp(now.minusHours(2).toString()).build()), "1", + now.minusHours(2).toString())); + + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now.minusHours(3), lineItemService); + + // when + deliveryStatsService.addDeliveryProgress(deliveryProgress, emptyMap()); + deliveryStatsService.sendDeliveryProgressReports(); + + // then + verify(alertHttpService).resetAlertCount(eq("pbs-delivery-stats-client-error")); + } + + @Test + public void suspendShouldSetSuspendFlagAndReportShouldNotBeSent() { + // given + given(deliveryProgressReportFactory.batchFromDeliveryProgress(any(), any(), any(), anyInt(), anyBoolean())) + .willReturn(DeliveryProgressReportBatch.of(singleton(DeliveryProgressReport.builder().reportId("1") + .dataWindowEndTimeStamp(now.minusHours(4).toString()).build()), "1", + now.minusHours(4).toString())); + + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now.minusHours(5), lineItemService); + + deliveryStatsService.addDeliveryProgress(deliveryProgress, null); + + // when + deliveryStatsService.suspend(); + deliveryStatsService.sendDeliveryProgressReports(); + + // then + verifyZeroInteractions(httpClient); + } + + @Test + public void sendDeliveryProgressReportShouldSendGzippedBody() throws JsonProcessingException { + // given + final DeliveryStatsService deliveryStatsService = new DeliveryStatsService( + DeliveryStatsProperties.builder() + .endpoint("localhost/delivery") + .cachedReportsNumber(3) + .timeoutMs(500L) + .reportsIntervalMs(0) + .requestCompressionEnabled(true) + .username("username") + .password("password") + .build(), + deliveryProgressReportFactory, + alertHttpService, + httpClient, + metrics, + clock, + vertx, + jacksonMapper); + + givenDeliveryProgressHttpResponse(httpClient, 200, null); + + final DeliveryProgressReport deliveryProgressReport = DeliveryProgressReport.builder().reportId("1") + .lineItemStatus(emptySet()) + .dataWindowEndTimeStamp(now.minusHours(2).toString()).build(); + given(deliveryProgressReportFactory.updateReportTimeStamp(any(), any())).willReturn(deliveryProgressReport); + given(deliveryProgressReportFactory.batchFromDeliveryProgress(any(), any(), any(), anyInt(), anyBoolean())) + .willReturn( + DeliveryProgressReportBatch.of(singleton(deliveryProgressReport), "1", + now.minusHours(2).toString())); + + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now.minusHours(3), lineItemService); + + // when + deliveryStatsService.addDeliveryProgress(deliveryProgress, emptyMap()); + deliveryStatsService.sendDeliveryProgressReports(); + + // then + final ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(byte[].class); + final ArgumentCaptor headerCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient, times(1)).request(any(), anyString(), headerCaptor.capture(), bodyCaptor.capture(), + anyLong()); + // verify body was compressed well + final byte[] compressedRequestBody = bodyCaptor.getValue(); + final String decompressedRequestBody = decompress(compressedRequestBody); + assertThat(mapper.readValue(decompressedRequestBody, DeliveryProgressReport.class)) + .isEqualTo(deliveryProgressReport); + // verify Content-encoding header was added + final MultiMap headers = headerCaptor.getValue(); + assertThat(headers.get(HttpHeaders.CONTENT_ENCODING)).isEqualTo("gzip"); + } + + private static String decompress(byte[] byteArray) { + final StringBuilder body = new StringBuilder(); + try (GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(byteArray)); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(gzipInputStream, + StandardCharsets.UTF_8))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + body.append(line); + } + return body.toString(); + } catch (IOException e) { + throw new RuntimeException("Error occurred while decompressing gzipped body"); + } + } + + private static void givenDeliveryProgressHttpResponse(HttpClient httpClient, int statusCode, String response) { + final HttpClientResponse httpClientResponse = HttpClientResponse.of(statusCode, null, response); + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(httpClientResponse)); + } +} diff --git a/src/test/java/org/prebid/server/deals/LineItemServiceTest.java b/src/test/java/org/prebid/server/deals/LineItemServiceTest.java new file mode 100644 index 00000000000..d09410d0b2c --- /dev/null +++ b/src/test/java/org/prebid/server/deals/LineItemServiceTest.java @@ -0,0 +1,1900 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.User; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.deals.events.ApplicationEventService; +import org.prebid.server.deals.lineitem.DeliveryPlan; +import org.prebid.server.deals.lineitem.DeliveryToken; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.model.DeepDebugLog; +import org.prebid.server.deals.model.MatchLineItemsResult; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.deals.proto.DeliverySchedule; +import org.prebid.server.deals.proto.FrequencyCap; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.deals.proto.Price; +import org.prebid.server.deals.proto.Token; +import org.prebid.server.deals.targeting.TargetingDefinition; +import org.prebid.server.log.CriteriaLogManager; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal; +import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal.Category; +import org.prebid.server.settings.model.Account; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.IntStream; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.assertj.core.util.Lists.emptyList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class LineItemServiceTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private TargetingService targetingService; + @Mock + private BidderCatalog bidderCatalog; + @Mock + private CurrencyConversionService conversionService; + @Mock + private ApplicationEventService applicationEventService; + @Mock + private Clock clock; + @Mock + private CriteriaLogManager criteriaLogManager; + + private LineItemService lineItemService; + + private ZonedDateTime now; + + @Before + public void setUp() { + now = ZonedDateTime.now(Clock.fixed(Instant.parse("2019-07-26T10:00:00Z"), ZoneOffset.UTC)); + + given(clock.instant()).willReturn(now.toInstant()); + given(clock.getZone()).willReturn(ZoneOffset.UTC); + + given(conversionService.convertCurrency(any(), anyMap(), anyString(), anyString(), any())) + .willReturn(BigDecimal.ONE); + + lineItemService = new LineItemService(2, targetingService, bidderCatalog, conversionService, + applicationEventService, "USD", clock, criteriaLogManager); + } + + @Test + public void updateLineItemsShouldRemoveLineItemIfItIsNotActiveFromPlannerResponse() { + // given + final List firstPlanResponse = asList( + givenLineItemMetaData("lineItem1", now, "1", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), Function.identity()), + givenLineItemMetaData("lineItem2", now, "2", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), Function.identity())); + + final List secondPlanResponse = asList( + givenLineItemMetaData("lineItem1", now, "1", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), + builder -> builder.status("inactive")), + givenLineItemMetaData("lineItem2", now, "2", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), Function.identity())); + + // when and then + lineItemService.updateLineItems(firstPlanResponse, true); + assertThat(lineItemService.getLineItemById("lineItem1")).isNotNull(); + assertThat(lineItemService.getLineItemById("lineItem2")).isNotNull(); + lineItemService.updateLineItems(secondPlanResponse, true); + assertThat(lineItemService.getLineItemById("lineItem1")).isNull(); + assertThat(lineItemService.getLineItemById("lineItem2")).isNotNull(); + } + + @Test + public void updateLineItemsShouldRemoveLineItemIfItHasEndTimeInPastInPlannerResponse() { + // given + final List firstPlanResponse = asList( + givenLineItemMetaData("lineItem1", now, "1", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), Function.identity()), + givenLineItemMetaData("lineItem2", now, "2", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), Function.identity())); + + final List secondPlanResponse = asList( + givenLineItemMetaData("lineItem1", now, "1", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), + builder -> builder.endTimeStamp(now.minusHours(1))), + givenLineItemMetaData("lineItem2", now, "2", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), Function.identity())); + + // when and then + lineItemService.updateLineItems(firstPlanResponse, true); + assertThat(lineItemService.getLineItemById("lineItem1")).isNotNull(); + assertThat(lineItemService.getLineItemById("lineItem2")).isNotNull(); + lineItemService.updateLineItems(secondPlanResponse, true); + assertThat(lineItemService.getLineItemById("lineItem1")).isNull(); + assertThat(lineItemService.getLineItemById("lineItem2")).isNotNull(); + } + + @Test + public void updateLineItemsShouldRemoveLineItemIfItHasEndTimeInPastInMemory() { + // given + final List firstPlanResponse = asList( + givenLineItemMetaData("lineItem1", now, "1", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), + builder -> builder.endTimeStamp(now.plusSeconds(1))), + givenLineItemMetaData("lineItem2", now, "2", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), Function.identity())); + + // when and then + lineItemService.updateLineItems(firstPlanResponse, true, now); + assertThat(lineItemService.getLineItemById("lineItem1")).isNotNull(); + assertThat(lineItemService.getLineItemById("lineItem2")).isNotNull(); + lineItemService.updateLineItems(emptyList(), true, now.plusSeconds(2)); + assertThat(lineItemService.getLineItemById("lineItem1")).isNull(); + assertThat(lineItemService.getLineItemById("lineItem2")).isNotNull(); + } + + @Test + public void updateLineItemsShouldNotRemoveLineItemIfItWasMissedInPlannerResponse() { + // given + final List firstPlanResponse = asList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), now), + givenLineItemMetaData("lineItem2", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), now)); + + final List secondPlanResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), now)); + + // when and then + lineItemService.updateLineItems(firstPlanResponse, true); + assertThat(lineItemService.getLineItemById("lineItem1")).isNotNull(); + assertThat(lineItemService.getLineItemById("lineItem2")).isNotNull(); + lineItemService.updateLineItems(secondPlanResponse, true); + assertThat(lineItemService.getLineItemById("lineItem1")).isNotNull(); + assertThat(lineItemService.getLineItemById("lineItem2")).isNotNull(); + } + + @Test + public void updateLineItemShouldSaveLineItemIfItHasEmptyDeliverySchedules() { + // given + final List firstPlanResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", emptyList(), now)); + + // when and then + lineItemService.updateLineItems(firstPlanResponse, true); + assertThat(lineItemService.getLineItemById("lineItem1")).isNotNull(); + } + + @Test + public void updateLineItemShouldSaveLineItemIfItDoesNotHaveDeliverySchedules() { + // given + final List firstPlanResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", null, now)); + + // when and then + lineItemService.updateLineItems(firstPlanResponse, true); + assertThat(lineItemService.getLineItemById("lineItem1")).isNotNull(); + } + + @Test + public void updateLineItemsShouldUpdateCurrentPlanIfUpdatedPlanUpdateTimeStampIsInFuture() { + // given + final List firstPlanResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), now)); + + final List secondPlanResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now.plusMinutes(1), singleton(Token.of(1, 200)))), now)); + + // when + lineItemService.updateLineItems(firstPlanResponse, true); + incSpentTokens(10, "lineItem1"); + lineItemService.updateLineItems(secondPlanResponse, true); + + // then + final LineItem lineItem = lineItemService.getLineItemById("lineItem1"); + assertThat(lineItem).isNotNull(); + + final DeliveryPlan activeDeliveryPlan = lineItem.getActiveDeliveryPlan(); + + assertThat(activeDeliveryPlan).isNotNull() + .extracting(DeliveryPlan::getPlanId, DeliveryPlan::getStartTimeStamp, DeliveryPlan::getEndTimeStamp, + DeliveryPlan::getUpdatedTimeStamp) + .containsOnly("planId1", now.minusHours(1), now.plusHours(1), now.plusMinutes(1)); + assertThat(activeDeliveryPlan.getDeliveryTokens()) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, token -> token.getSpent().sum()) + .containsOnly(tuple(1, 200, 10L)); + + // 6 minutes after plan started + assertThat(lineItem.getReadyAt()).isEqualTo(now.minusHours(1).plusMinutes(6)); + } + + @Test + public void updateLineItemsShouldUpdateReadyAtBasedOnPlanStartTime() { + // given + final List firstPlanResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), now, singleton(Token.of(1, 100)))), now)); + + // when + lineItemService.updateLineItems(firstPlanResponse, true); + incSpentTokens(1, "lineItem1"); + + // then + final LineItem lineItem = lineItemService.getLineItemById("lineItem1"); + assertThat(lineItem).isNotNull(); + + // 1 * (120 * 60 * 1000) \ 100 tokens = 72,000 millis = 1 minute 12 seconds shift from plan startTime + assertThat(lineItem.getReadyAt()).isEqualTo(now.minusHours(1).plusMinutes(1).plusSeconds(12)); + } + + @Test + public void updateLineItemsShouldConvertPriceWhenLineItemMetaDataCurrencyIsDifferent() { + // given + final String defaultCurrency = "RUB"; + lineItemService = new LineItemService(2, targetingService, bidderCatalog, conversionService, + applicationEventService, defaultCurrency, clock, criteriaLogManager); + + final List planResponse = asList( + givenLineItemMetaData("lineItem1", null, null, + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusHours(1), + emptySet())), now), + givenLineItemMetaData("lineItem2", null, null, + singletonList(givenDeliverySchedule("planId2", now.minusHours(1), now.plusHours(1), + emptySet())), now)); + + final BigDecimal updatedCmp = BigDecimal.TEN; + given(conversionService.convertCurrency(any(), anyMap(), anyString(), anyString(), any())) + .willReturn(updatedCmp); + + // when + lineItemService.updateLineItems(planResponse, true); + + // then + final LineItem lineItem1 = lineItemService.getLineItemById("lineItem1"); + assertThat(lineItem1.getCpm()).isEqualTo(updatedCmp); + assertThat(lineItem1.getCurrency()).isEqualTo(defaultCurrency); + + final LineItem lineItem2 = lineItemService.getLineItemById("lineItem2"); + assertThat(lineItem2.getCpm()).isEqualTo(updatedCmp); + assertThat(lineItem2.getCurrency()).isEqualTo(defaultCurrency); + } + + @Test + public void updateLineItemsShouldCreateLineItemsWhenPlannerIsResponsive() { + // given + final List planResponse = asList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), singleton(Token.of(1, 100)))), now), + givenLineItemMetaData("lineItem2", "1002", "rubicon", + singletonList(givenDeliverySchedule("planId2", now.plusHours(1), + now.plusHours(2), singleton(Token.of(1, 100)))), now)); + + // when + lineItemService.updateLineItems(planResponse, true); + + // then + final LineItem lineItem1 = lineItemService.getLineItemById("lineItem1"); + assertThat(lineItem1).isNotNull(); + + final DeliveryPlan activeDeliveryPlan = lineItem1.getActiveDeliveryPlan(); + assertThat(activeDeliveryPlan).isNotNull() + .extracting(DeliveryPlan::getPlanId, DeliveryPlan::getStartTimeStamp, DeliveryPlan::getEndTimeStamp) + .containsOnly("planId1", now.minusHours(1), now.plusHours(1)); + assertThat(activeDeliveryPlan.getDeliveryTokens()) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal) + .containsOnly(tuple(1, 100)); + + assertThat(lineItem1.getReadyAt()).isEqualTo(now); + + final LineItem lineItem2 = lineItemService.getLineItemById("lineItem2"); + assertThat(lineItem2).isNotNull(); + assertThat(lineItem2.getActiveDeliveryPlan()).isNull(); + assertThat(lineItem2.getReadyAt()).isNull(); + } + + @Test + public void updateLineItemsShouldCreateLineItemsWithNullTargetingIfCantParse() { + // given + final List planResponse = singletonList( + LineItemMetaData.builder() + .lineItemId("lineItem1") + .status("active") + .dealId("dealId") + .accountId("1001") + .source("rubicon") + .price(Price.of(BigDecimal.ONE, "USD")) + .relativePriority(5) + .startTimeStamp(now.minusHours(1)) + .endTimeStamp(now.plusHours(1)) + .updatedTimeStamp(now) + .targeting(mapper.createObjectNode().set("$invalid", new TextNode("invalid"))) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), singleton(Token.of(1, 100))))) + .build()); + + // when + lineItemService.updateLineItems(planResponse, true); + + // then + final LineItem lineItem = lineItemService.getLineItemById("lineItem1"); + assertThat(lineItem).isNotNull(); + assertThat(lineItem.getTargetingDefinition()).isNull(); + } + + @Test + public void updateLineItemsShouldNotUpdateCurrentPlanIfUpdatedPlanUpdateTimeStampIsNotInFuture() { + // given + final List firstPlanResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), singleton(Token.of(1, 100)))), now)); + + final List secondPlanResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusHours(1), singleton(Token.of(1, 200)))), now)); + + // when + lineItemService.updateLineItems(firstPlanResponse, true); + incSpentTokens(10, "lineItem1"); + lineItemService.updateLineItems(secondPlanResponse, true); + + // then + final LineItem lineItem = lineItemService.getLineItemById("lineItem1"); + assertThat(lineItem).isNotNull(); + + final DeliveryPlan activeDeliveryPlan = lineItem.getActiveDeliveryPlan(); + assertThat(activeDeliveryPlan).isNotNull() + .extracting(DeliveryPlan::getPlanId, DeliveryPlan::getStartTimeStamp, DeliveryPlan::getEndTimeStamp) + .containsOnly("planId1", now.minusHours(1), now.plusHours(1)); + assertThat(activeDeliveryPlan.getDeliveryTokens()) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, token -> token.getSpent().sum()) + .containsOnly(tuple(1, 100, 10L)); + + // should be ready at 12 minutes after plan start + assertThat(lineItem.getReadyAt()).isEqualTo(now.minusHours(1).plusMinutes(12)); + } + + @Test + public void updateLineItemsShouldMergeLineItemsWhenPlannerIsNotResponsive() { + // given + given(clock.instant()).willReturn(now.toInstant(), now.toInstant(), now.plusSeconds(2).toInstant()); + given(clock.getZone()).willReturn(ZoneOffset.UTC); + + final Set expiredTokens = new HashSet<>(); + expiredTokens.add(Token.of(1, 100)); + expiredTokens.add(Token.of(3, 300)); + expiredTokens.add(Token.of(4, 400)); + expiredTokens.add(Token.of(5, 500)); + + final Set newActiveTokens = new HashSet<>(); + newActiveTokens.add(Token.of(1, 100)); + newActiveTokens.add(Token.of(2, 200)); + newActiveTokens.add(Token.of(3, 300)); + newActiveTokens.add(Token.of(4, 400)); + + final List planResponse = singletonList(givenLineItemMetaData( + "lineItem1", "1001", "rubicon", + asList( + givenDeliverySchedule("planId1", now.minusHours(1), now.plusSeconds(1), expiredTokens), + givenDeliverySchedule("planId2", now.plusSeconds(1), now.plusHours(1), newActiveTokens)), + now)); + + // when and then + lineItemService.updateLineItems(planResponse, true); + incSpentTokens(240, "lineItem1"); + lineItemService.updateLineItems(null, false); + lineItemService.advanceToNextPlan(now.plusSeconds(1)); + + verify(clock, times(2)).instant(); + + final LineItem lineItem = lineItemService.getLineItemById("lineItem1"); + + final DeliveryPlan activeDeliveryPlan = lineItem.getActiveDeliveryPlan(); + assertThat(activeDeliveryPlan).isNotNull(); + assertThat(activeDeliveryPlan.getDeliveryTokens()) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, + deliveryToken -> deliveryToken.getSpent().sum()) + .containsExactly( + tuple(1, 200, 100L), + tuple(2, 200, 0L), + tuple(3, 600, 140L), + tuple(4, 800, 0L), + tuple(5, 500, 0L)); + } + + @Test + public void updateLineItemShouldUpdatePlanWithoutActiveCurrentDeliveryPlan() { + // given + given(clock.instant()).willReturn(now.toInstant(), now.toInstant(), now.plusSeconds(3).toInstant()); + given(clock.getZone()).willReturn(ZoneOffset.UTC); + + final List planResponse = singletonList(givenLineItemMetaData( + "lineItem1", "1001", "rubicon", + asList( + givenDeliverySchedule("planId1", now.minusHours(2), + now.minusHours(1), singleton(Token.of(1, 100))), + givenDeliverySchedule("planId2", now.plusSeconds(2), + now.plusHours(1), singleton(Token.of(1, 100)))), + now)); + + // when and then + lineItemService.updateLineItems(planResponse, true); + lineItemService.updateLineItems(null, false); + lineItemService.advanceToNextPlan(now.plusSeconds(2)); + + verify(clock, times(2)).instant(); + + final LineItem lineItem = lineItemService.getLineItemById("lineItem1"); + + final DeliveryPlan activeDeliveryPlan = lineItem.getActiveDeliveryPlan(); + assertThat(activeDeliveryPlan).isNotNull(); + assertThat(activeDeliveryPlan.getDeliveryTokens()) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, + deliveryToken -> deliveryToken.getSpent().sum()) + .containsExactly(tuple(1, 100, 0L)); + } + + @Test + public void accountHasDealsShouldReturnTrue() { + // given + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusHours(1), + emptySet())), now)); + lineItemService.updateLineItems(planResponse, true); + + // when and then + assertThat(lineItemService.accountHasDeals(AuctionContext.builder() + .account(Account.builder().id("1001").build()).build())) + .isTrue(); + } + + @Test + public void accountHasDealsShouldReturnFalseWhenAccountIsEmptyString() { + // given + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusHours(1), + emptySet())), now)); + lineItemService.updateLineItems(planResponse, true); + + // when and then + assertThat(lineItemService.accountHasDeals(AuctionContext.builder().account(Account.builder().id("").build()) + .build())).isFalse(); + } + + @Test + public void accountHasDealsShouldReturnFalseWhenNoMatchingLineItemWereFound() { + // given + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "3003", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusHours(1), + emptySet())), now)); + lineItemService.updateLineItems(planResponse, true); + + // when and then + assertThat(lineItemService.accountHasDeals(AuctionContext.builder().account(Account.builder().id("1001") + .build()).build())).isFalse(); + } + + @Test + public void accountHasDealsShouldReturnFalseWhenMatchedLineItemIsNotActive() { + // given + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusHours(1), + emptySet())), now)); + lineItemService.updateLineItems(planResponse, true); + given(clock.instant()).willReturn(now.plusHours(2).toInstant()); + + // when and then + assertThat(lineItemService.accountHasDeals(AuctionContext.builder().account(Account.builder().id("1001") + .build()).build())).isFalse(); + } + + @Test + public void findMatchingLineItemsShouldReturnEmptyListWhenLineItemsIsEmpty() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).isEmpty(); + } + + @Test + public void findMatchingLineItemsShouldReturnEmptyListWhenNoLineItemsForAccount() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "accountIdUnknown", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100)))), now)); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).isEmpty(); + } + + @Test + public void findMatchingLineItemsShouldReturnEmptyListWhenNoBiddersMatched() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "accountId", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusMinutes(1), + singleton(Token.of(1, 100)))), now)); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("pubmatic")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).isEmpty(); + } + + @Test + public void findMatchingLineItemsShouldReturnLineItemsForMatchingBidder() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "accountId", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusMinutes(1), + singleton(Token.of(1, 100)))), now)); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("lineItem1"); + } + + @Test + public void findMatchingLineItemsShouldReturnLineItemsWhenLineItemsBidderIsAlias() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "accountId", "rubiAlias", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusMinutes(1), + singleton(Token.of(1, 100)))), now)); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("lineItem1"); + } + + @Test + public void findMatchingLineItemsShouldFilterLineItemsWithNotValidBidders() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + given(bidderCatalog.isValidName(eq("invalid"))).willReturn(false); + + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "accountId", "invalid", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusMinutes(1), + singleton(Token.of(1, 100)))), now)); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("invalid")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).isEmpty(); + } + + @Test + public void findMatchingLineItemsShouldFilterLineItemsWithDeprecatedBidders() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + given(bidderCatalog.isDeprecatedName(eq("deprecated"))).willReturn(true); + + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "accountId", "deprecated", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusMinutes(1), + singleton(Token.of(1, 100)))), now)); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("deprecated")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).isEmpty(); + } + + @Test + public void findMatchingLineItemsShouldReturnLineItemsWhenBidderFromInputBiddersIsAlias() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "accountId", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusMinutes(1), + singleton(Token.of(1, 100)))), now)); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("lineItem1"); + } + + @Test + public void findMatchingLineItemsShouldReturnEmptyListWhenAccountIsEmptyString() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()) + .toBuilder().account(Account.builder().id("").build()).build(); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = singletonList( + givenLineItemMetaData("lineItem1", "accountId", "rubicon", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), now.plusMinutes(1), + singleton(Token.of(1, 100)))), now)); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).isEmpty(); + } + + @Test + public void findMatchingLineItemsShouldFilterNotMatchingTargeting() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + given(targetingService.parseTargetingDefinition(any(), eq("id1"))) + .willReturn(TargetingDefinition.of(context -> false)); + given(targetingService.parseTargetingDefinition(any(), eq("id2"))) + .willReturn(TargetingDefinition.of(context -> true)); + given(targetingService.matchesTargeting(any(), any(), any())).willAnswer(withEvaluatedTargeting()); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .source("rubicon") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .source("appnexus") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + + assertThat(auctionContext.getDeepDebugLog().entries()).containsOnly( + ExtTraceDeal.of("id1", ZonedDateTime.now(clock), Category.targeting, + "Line Item id1 targeting did not match imp with id imp1"), + ExtTraceDeal.of("id2", ZonedDateTime.now(clock), Category.targeting, + "Line Item id2 targeting matched imp with id imp1"), + ExtTraceDeal.of("id2", ZonedDateTime.now(clock), Category.pacing, + "Matched Line Item id2 for bidder appnexus ready to serve. relPriority null")); + } + + @Test + public void findMatchingLineItemsShouldFilterNullTargeting() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + given(targetingService.parseTargetingDefinition(any(), eq("id1"))) + .willReturn(null); + given(targetingService.parseTargetingDefinition(any(), eq("id2"))) + .willReturn(TargetingDefinition.of(context -> true)); + given(targetingService.matchesTargeting(any(), any(), any())).willAnswer(withEvaluatedTargeting()); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .source("rubicon") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .source("appnexus") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + + assertThat(auctionContext.getDeepDebugLog().entries()).containsOnly( + ExtTraceDeal.of("id1", ZonedDateTime.now(clock), Category.targeting, + "Line Item id1 targeting was not defined or has incorrect format"), + ExtTraceDeal.of("id2", ZonedDateTime.now(clock), Category.targeting, + "Line Item id2 targeting matched imp with id imp1"), + ExtTraceDeal.of("id2", ZonedDateTime.now(clock), Category.pacing, + "Matched Line Item id2 for bidder appnexus ready to serve. relPriority null")); + } + + @Test + public void findMatchingLineItemsShouldFilterNotReadyLineItems() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .source("rubicon") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusMinutes(1), + now.plusDays(1), singleton(Token.of(1, 1))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .source("appnexus") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + lineItemService.getLineItemById("id1").incSpentToken(now.plusSeconds(1)); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + } + + @Test + public void findMatchingLineItemsShouldFilterLineItemsWithFcapIdsWhenUserDetailsFcapIsNull() { + // given + final AuctionContext auctionContext = givenAuctionContext(null); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .source("rubicon") + .accountId("accountId") + .frequencyCaps(singletonList(FrequencyCap.builder().fcapId("123").build())) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .source("appnexus") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + } + + @Test + public void findMatchingLineItemsShouldFilterFcappedLineItems() { + // given + final AuctionContext auctionContext = givenAuctionContext(asList("fcap2", "fcap3")); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .source("rubicon") + .accountId("accountId") + .frequencyCaps(singletonList(FrequencyCap.builder().fcapId("fcap2").build())) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .source("appnexus") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + assertThat(auctionContext.getTxnLog().lineItemsMatchedTargetingFcapped()).containsOnly("id1"); + assertThat(auctionContext.getDeepDebugLog().entries()).contains( + ExtTraceDeal.of("id1", ZonedDateTime.now(clock), Category.pacing, + "Matched Line Item id1 for bidder rubicon is frequency capped by fcap id fcap2.")); + } + + @Test + public void findMatchingLineItemsShouldFilterSameSentToBidderAsTopMatchLineItemsPerBidder() { + // given + final TxnLog txnLog = TxnLog.create(); + txnLog.lineItemsSentToBidderAsTopMatch().put("rubicon", new HashSet<>(singleton("id1"))); + final AuctionContext auctionContext = givenAuctionContext(emptyList()).toBuilder().txnLog(txnLog).build(); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .source("rubicon") + .accountId("accountId") + .relativePriority(2) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + } + + @Test + public void findMatchingLineItemsShouldFilterSameSentToBidderAsTopMatchLineItemsPerAllBidders() { + // given + final TxnLog txnLog = TxnLog.create(); + txnLog.lineItemsSentToBidderAsTopMatch().put("appnexus", new HashSet<>(singleton("id1"))); + final AuctionContext auctionContext = givenAuctionContext(emptyList()).toBuilder().txnLog(txnLog).build(); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .source("appnexus") + .accountId("accountId") + .relativePriority(1) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .source("rubicon") + .accountId("accountId") + .relativePriority(2) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + } + + @Test + public void findMatchingLineItemsShouldFilterLineItemsWithSameDealAndLowestPriorityTokenClassWithinBidder() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .dealId("dealId1") + .source("rubicon") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(3, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .dealId("dealId1") + .source("rubicon") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + } + + @Test + public void findMatchingLineItemsShouldFilterLineItemsWithSameDealIdAndLowestLineItemPriority() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .dealId("dealId1") + .source("rubicon") + .accountId("accountId") + .relativePriority(3) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .dealId("dealId1") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + } + + @Test + public void findMatchingLineItemsShouldFilterLineItemsWithSameDealIdAndLowestCpm() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .dealId("dealId1") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.ONE, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .dealId("dealId1") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.TEN, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + } + + @Test + public void findMatchingLineItemsShouldFilterLineItemsWithoutUnspentTokensAndIncrementDeferred() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .dealId("dealId1") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.ONE, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 0))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .dealId("dealId2") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.TEN, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(auctionContext.getTxnLog().lineItemsPacingDeferred()).contains("id1"); + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2"); + assertThat(auctionContext.getDeepDebugLog().entries()).contains( + ExtTraceDeal.of("id1", ZonedDateTime.now(clock), Category.pacing, + "Matched Line Item id1 for bidder rubicon does not have unspent tokens to be served")); + } + + @Test + public void findMatchingLineItemsShouldLimitLineItemsPerBidder() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .dealId("1") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.ONE, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .dealId("2") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.TEN, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id3") + .status("active") + .dealId("3") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.TEN, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2", "id3"); + } + + @Test + public void findMatchingLineItemsShouldReturnLineItemWithReadyToServeEqualToNow() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + givenClock(now, now.plusSeconds((now.plusMinutes(5).toEpochSecond() - now.toEpochSecond()) / 100)); + + givenBidderCatalog(); + + final List planResponse = singletonList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .dealId("1") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.ONE, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("futurePlanId", now.minusMinutes(1), + now.plusMinutes(5), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id1"); + } + + @Test + public void findMatchingLineItemsShouldReturnListOfDifferentBidders() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + // filtered by readyAt + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .dealId("1") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.ONE, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("futurePlanId", now.minusMinutes(1), + now.plusDays(1), singleton(Token.of(1, 1))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .dealId("2") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.TEN, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + // filtered by same deal Id with lowest priority + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id3") + .status("active") + .dealId("3") + .source("appnexus") + .accountId("accountId") + .relativePriority(2) + .price(Price.of(BigDecimal.TEN, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id4") + .status("active") + .dealId("3") + .source("appnexus") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.TEN, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + lineItemService.getLineItemById("id1").incSpentToken(now.plusSeconds(1)); + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final MatchLineItemsResult result = lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(result.getLineItems()).extracting(LineItem::getLineItemId).containsOnly("id2", "id4"); + } + + @Test + public void findMatchingLineItemsShouldRecordLineItemsInTxnLog() { + // given + final AuctionContext auctionContext = givenAuctionContext(asList("fcap2", "fcap3")); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .dealId("1") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.ONE, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .dealId("2") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.TEN, "USD")) + .frequencyCaps(singletonList(FrequencyCap.builder().fcapId("fcap3").build())) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id3") + .status("active") + .dealId("3") + .source("appnexus") + .accountId("accountId") + .relativePriority(2) + .price(Price.of(BigDecimal.ONE, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id4") + .status("active") + .dealId("4") + .source("appnexus") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.TEN, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + final TxnLog expectedTxnLog = TxnLog.create(); + expectedTxnLog.lineItemsMatchedWholeTargeting().addAll(asList("id1", "id2", "id3", "id4")); + expectedTxnLog.lineItemsMatchedTargetingFcapped().add("id2"); + expectedTxnLog.lineItemsReadyToServe().addAll(asList("id1", "id3", "id4")); + expectedTxnLog.lineItemsSentToBidderAsTopMatch().put("rubicon", singleton("id1")); + expectedTxnLog.lineItemsSentToBidderAsTopMatch().put("appnexus", singleton("id4")); + expectedTxnLog.lineItemsSentToBidder().get("rubicon").add("id1"); + expectedTxnLog.lineItemsSentToBidder().get("appnexus").addAll(asList("id3", "id4")); + expectedTxnLog.lostMatchingToLineItems().put("id3", singleton("id4")); + assertThat(auctionContext.getTxnLog()).isEqualTo(expectedTxnLog); + } + + @Test + public void findMatchingLineItemsShouldRecordLineItemsInTxnLogWhenPacingDeferred() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + givenTargetingService(); + + givenBidderCatalog(); + + final List planResponse = singletonList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id5") + .status("active") + .dealId("5") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.TEN, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(2), singleton(Token.of(1, 2))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + lineItemService.getLineItemById("id5").incSpentToken(now.plusSeconds(1)); + lineItemService.getLineItemById("id5").incSpentToken(now.plusSeconds(1)); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon")).build(); + + // when + lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + final TxnLog expectedTxnLog = TxnLog.create(); + expectedTxnLog.lineItemsMatchedWholeTargeting().addAll(singletonList("id5")); + expectedTxnLog.lineItemsPacingDeferred().addAll(singletonList("id5")); + assertThat(auctionContext.getTxnLog()).isEqualTo(expectedTxnLog); + } + + @Test + public void findMatchingLineItemsShouldRecordLineItemsInTxnLogWhenFcapLookupFailed() { + // given + final AuctionContext auctionContext = givenAuctionContext(null); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .dealId("1") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.ONE, "USD")) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .dealId("2") + .source("rubicon") + .accountId("accountId") + .relativePriority(1) + .price(Price.of(BigDecimal.TEN, "USD")) + .frequencyCaps(singletonList(FrequencyCap.builder().fcapId("fcap3").build())) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(auctionContext.getTxnLog().lineItemsMatchedTargetingFcapLookupFailed()).containsOnly("id2"); + assertThat(auctionContext.getDeepDebugLog().entries()).contains( + ExtTraceDeal.of("id2", ZonedDateTime.now(clock), Category.pacing, + "Failed to match fcap for Line Item id2 bidder rubicon in a reason of bad response" + + " from user data service")); + } + + @Test + public void findMatchingLineItemsShouldHasCorrectRandomDistribution() { + // given + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + lineItemService = new LineItemService(3, targetingService, bidderCatalog, conversionService, + applicationEventService, "USD", clock, criteriaLogManager); + + final List planResponse = asList( + givenLineItemMetaData("id1", now, "1", + singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), now, singleton(Token.of(1, 100)))), Function.identity()), + givenLineItemMetaData("id2", now, "2", + singletonList(givenDeliverySchedule("planId2", now.minusHours(1), + now.plusMinutes(1), now, singleton(Token.of(1, 100)))), Function.identity()), + givenLineItemMetaData("id3", now, "3", + singletonList(givenDeliverySchedule("planId3", now.minusHours(1), + now.plusMinutes(1), now, singleton(Token.of(1, 100)))), Function.identity())); + + lineItemService.updateLineItems(planResponse, true); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + final Map count = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + AuctionContext auctionContext = givenAuctionContext(emptyList()); + MatchLineItemsResult matchLineItemsResult = + lineItemService.findMatchingLineItems(auctionContext, imp); + count.compute(matchLineItemsResult.getLineItems().get(0).getLineItemId(), + (s, integer) -> integer != null ? ++integer : 1); + } + + // then + assertThat(count.get("id1")).isBetween(290, 390); + assertThat(count.get("id2")).isBetween(290, 390); + assertThat(count.get("id3")).isBetween(290, 390); + } + + @Test + public void findMatchingLineItemsShouldAddDebugMessages() { + // given + final AuctionContext auctionContext = givenAuctionContext(emptyList()); + + givenTargetingService(); + + givenClock(now, now.plusMinutes(1)); + + givenBidderCatalog(); + + final List planResponse = asList( + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id1") + .status("active") + .source("rubicon") + .accountId("accountId") + .relativePriority(2) + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusHours(1), + now.plusMinutes(1), singleton(Token.of(1, 100))))) + .build(), + LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId("id2") + .status("active") + .source("appnexus") + .accountId("accountId") + .deliverySchedules(singletonList(givenDeliverySchedule("planId1", now.minusMinutes(1), + now.plusDays(1), singleton(Token.of(1, 2))))) + .build()); + + lineItemService.updateLineItems(planResponse, true); + lineItemService.getLineItemById("id2").incSpentToken(now.plusSeconds(1)); + + final Imp imp = Imp.builder().id("imp1").ext(givenImpExt("rubicon", "appnexus")).build(); + + // when + lineItemService.findMatchingLineItems(auctionContext, imp); + + // then + assertThat(auctionContext.getDeepDebugLog().entries()).containsOnly( + ExtTraceDeal.of("id1", ZonedDateTime.now(clock), Category.targeting, + "Line Item id1 targeting matched imp with id imp1"), + ExtTraceDeal.of("id2", ZonedDateTime.now(clock), Category.targeting, + "Line Item id2 targeting matched imp with id imp1"), + ExtTraceDeal.of("id1", ZonedDateTime.now(clock), Category.pacing, + "Matched Line Item id1 for bidder rubicon ready to serve. relPriority 2"), + ExtTraceDeal.of("id2", ZonedDateTime.now(clock), Category.pacing, + "Matched Line Item id2 for bidder appnexus not ready to serve. Will be ready" + + " at 2019-07-26T21:59:30.000Z, current time is 2019-07-26T10:01:00.000Z")); + } + + private static LineItemMetaData givenLineItemMetaData( + String lineItemId, + ZonedDateTime now, + String dealId, + List deliverySchedules, + Function lineItemMetaDataCustomizer) { + return lineItemMetaDataCustomizer.apply(LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId(lineItemId) + .dealId(dealId) + .status("active") + .accountId("accountId") + .source("rubicon") + .price(Price.of(BigDecimal.ONE, "USD")) + .relativePriority(5) + .updatedTimeStamp(now) + .deliverySchedules(deliverySchedules)) + .build(); + } + + private static LineItemMetaData givenLineItemMetaData( + String lineItemId, String account, String bidderCode, List deliverySchedules, + ZonedDateTime now) { + + return LineItemMetaData.builder() + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .lineItemId(lineItemId) + .dealId("dealId") + .status("active") + .accountId(account) + .source(bidderCode) + .price(Price.of(BigDecimal.ONE, "USD")) + .relativePriority(5) + .startTimeStamp(now.minusHours(1)) + .endTimeStamp(now.plusHours(1)) + .updatedTimeStamp(now) + .deliverySchedules(deliverySchedules) + .build(); + } + + private static DeliverySchedule givenDeliverySchedule(String planId, ZonedDateTime start, ZonedDateTime end, + ZonedDateTime updated, Set tokens) { + return DeliverySchedule.builder() + .planId(planId) + .startTimeStamp(start) + .endTimeStamp(end) + .updatedTimeStamp(updated) + .tokens(tokens) + .build(); + } + + private static DeliverySchedule givenDeliverySchedule(String planId, ZonedDateTime start, ZonedDateTime end, + Set tokens) { + return givenDeliverySchedule(planId, start, end, start, tokens); + } + + private void incSpentTokens(int times, String... lineItemIds) { + for (final String lineItemId : lineItemIds) { + final LineItem lineItem = lineItemService.getLineItemById(lineItemId); + IntStream.range(0, times).forEach(i -> lineItem.incSpentToken(now)); + } + } + + private AuctionContext givenAuctionContext(List fcaps) { + return AuctionContext.builder() + .account(Account.builder().id("accountId").build()) + .deepDebugLog(DeepDebugLog.create(true, clock)) + .txnLog(TxnLog.create()) + .bidRequest(BidRequest.builder() + .user(User.builder().ext( + ExtUser.builder().fcapIds(fcaps).build()).build()) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("rubiAlias", "rubicon")) + .build())) + .build()) + .build(); + } + + private void givenTargetingService() { + given(targetingService.parseTargetingDefinition(any(), any())) + .willReturn(TargetingDefinition.of(context -> true)); + given(targetingService.matchesTargeting(any(), any(), any())).willAnswer(withEvaluatedTargeting()); + } + + private Answer withEvaluatedTargeting() { + return invocation -> ((TargetingDefinition) invocation.getArgument(2)).getRootExpression().matches(null); + } + + private void givenClock(ZonedDateTime... dateTimes) { + given(clock.instant()).willReturn( + dateTimes[0].toInstant(), + Arrays.stream(dateTimes).skip(1).map(ZonedDateTime::toInstant).toArray(Instant[]::new)); + given(clock.getZone()).willReturn(dateTimes[0].getZone()); + } + + private void givenBidderCatalog() { + given(bidderCatalog.isDeprecatedName(any())).willReturn(false); + given(bidderCatalog.isValidName(any())).willReturn(true); + } + + private ObjectNode givenImpExt(String... bidders) { + final ObjectNode extPrebidBidder = mapper.createObjectNode(); + Arrays.stream(bidders).forEach(extPrebidBidder::putNull); + + return mapper.createObjectNode().set( + "prebid", mapper.createObjectNode().set( + "bidder", extPrebidBidder)); + } +} diff --git a/src/test/java/org/prebid/server/deals/PlannerServiceTest.java b/src/test/java/org/prebid/server/deals/PlannerServiceTest.java new file mode 100644 index 00000000000..5620cdcf1a5 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/PlannerServiceTest.java @@ -0,0 +1,351 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.Future; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.BDDMockito; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.model.AlertPriority; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.deals.model.PlannerProperties; +import org.prebid.server.deals.proto.DeliverySchedule; +import org.prebid.server.deals.proto.FrequencyCap; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.deals.proto.Price; +import org.prebid.server.deals.proto.Token; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.concurrent.TimeoutException; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class PlannerServiceTest extends VertxTest { + + private static final String PLAN_ENDPOINT = "plan-endpoint"; + private static final String REGISTER_ENDPOINT = "register-endpoint"; + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + private static final String PBS_HOST = "pbs-host"; + private static final String PBS_REGION = "pbs-region"; + private static final String PBS_VENDOR = "pbs-vendor"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private HttpClient httpClient; + @Mock + private LineItemService lineItemService; + @Mock + private DeliveryProgressService deliveryProgressService; + @Mock + private AlertHttpService alertHttpService; + @Mock + private Metrics metrics; + @Mock + private Clock clock; + + private PlannerService plannerService; + + private ZonedDateTime now; + + @Before + public void setUp() throws JsonProcessingException { + clock = Clock.fixed(Instant.parse("2019-07-26T10:00:00Z"), ZoneOffset.UTC); + now = ZonedDateTime.now(clock); + + plannerService = new PlannerService( + PlannerProperties.builder() + .planEndpoint(PLAN_ENDPOINT) + .registerEndpoint(REGISTER_ENDPOINT) + .timeoutMs(100L) + .registerPeriodSeconds(60L) + .username(USERNAME) + .password(PASSWORD) + .build(), + DeploymentProperties.builder().pbsHostId(PBS_HOST).pbsRegion(PBS_REGION).pbsVendor(PBS_VENDOR).build(), + lineItemService, + deliveryProgressService, + alertHttpService, + httpClient, + metrics, + clock, + jacksonMapper); + + givenPlanHttpResponse(200, mapper.writeValueAsString( + asList(givenLineItemMetaData("lineItem1", "1001", "rubicon", now), + givenLineItemMetaData("lineItem2", "1002", "appnexus", now)))); + } + + @Test + public void updateLineItemMetaDataShouldRetryOnceWhenResponseCantBeParsed() { + // given + givenPlanHttpResponse(200, "{"); + + // when + plannerService.updateLineItemMetaData(); + + // then + verify(lineItemService, never()).updateLineItems(any(), anyBoolean()); + verify(alertHttpService).alertWithPeriod(eq("planner"), + eq("pbs-planner-client-error"), + eq(AlertPriority.MEDIUM), + eq("Failed to retrieve line items from GP. Reason: Cannot parse response: {")); + verify(metrics, times(2)).updatePlannerRequestMetric(eq(false)); + verify(httpClient, times(2)).get(anyString(), any(), anyLong()); + } + + @Test + public void updateLineItemMetaDataShouldRetryWhenHttpClientReturnsFailedFuture() { + // given + given(httpClient.get(anyString(), any(), anyLong())) + .willReturn(Future.failedFuture(new TimeoutException("Timeout has been exceeded"))); + + // when + plannerService.updateLineItemMetaData(); + + // then + verify(alertHttpService).alertWithPeriod(eq("planner"), eq("pbs-planner-client-error"), + eq(AlertPriority.MEDIUM), + eq("Failed to retrieve line items from GP. Reason: Timeout has been exceeded")); + verify(httpClient, times(2)).get(anyString(), any(), anyLong()); + } + + @Test + public void updateLineItemMetaDataShouldAlertWithoutRetryWhenPlannerReturnsEmptyLineItemList() + throws JsonProcessingException { + // given + givenPlanHttpResponse(200, mapper.writeValueAsString(emptyList())); + + // when + plannerService.updateLineItemMetaData(); + + // then + verify(alertHttpService).alertWithPeriod(eq("planner"), eq("pbs-planner-empty-response-error"), + eq(AlertPriority.LOW), eq("Response without line items was received from planner")); + verify(httpClient).get(anyString(), any(), anyLong()); + verify(lineItemService).updateLineItems(eq(emptyList()), anyBoolean()); + } + + @SuppressWarnings("unchecked") + @Test + public void getIdToLineItemsShouldReturnLineItemsSuccessfulInitialization() { + // when + plannerService.updateLineItemMetaData(); + + // then + final ArgumentCaptor> planResponseCaptor = ArgumentCaptor.forClass(List.class); + verify(lineItemService).updateLineItems(planResponseCaptor.capture(), anyBoolean()); + assertThat(planResponseCaptor.getValue()).isEqualTo(asList( + givenLineItemMetaData("lineItem1", "1001", "rubicon", now), + givenLineItemMetaData("lineItem2", "1002", "appnexus", now))); + verify(alertHttpService).resetAlertCount(eq("pbs-planner-empty-response-error")); + verify(metrics).updatePlannerRequestMetric(eq(true)); + verify(metrics).updateLineItemsNumberMetric(eq(2L)); + verify(metrics).updateRequestTimeMetric(eq(MetricName.planner_request_time), anyLong()); + } + + @SuppressWarnings("unchecked") + @Test + public void getIdToLineItemsShouldReturnOverwrittenMetaDataAfterSchedulerCallsRefresh() + throws JsonProcessingException { + // given + givenHttpClientReturnsResponses( + Future.succeededFuture(HttpClientResponse.of(200, null, + mapper.writeValueAsString(singletonList(LineItemMetaData.builder().lineItemId("id1") + .deliverySchedules(singletonList(DeliverySchedule.builder() + .planId("id1") + .startTimeStamp(now.minusHours(1)) + .endTimeStamp(now.plusHours(1)) + .updatedTimeStamp(now) + .tokens(singleton(Token.of(1, 300))).build())) + .accountId("1").build())))), + Future.succeededFuture(HttpClientResponse.of(200, null, + mapper.writeValueAsString(singletonList(LineItemMetaData.builder().lineItemId("id2") + .deliverySchedules(singletonList(DeliverySchedule.builder() + .planId("id2") + .startTimeStamp(now.minusHours(1)) + .endTimeStamp(now.plusHours(1)) + .updatedTimeStamp(now) + .tokens(singleton(Token.of(1, 300))).build())) + .accountId("2").build()))))); + + // when and then + plannerService.updateLineItemMetaData(); + + // fire request seconds time + plannerService.updateLineItemMetaData(); + + verify(httpClient, times(2)).get(anyString(), any(), anyLong()); + verify(lineItemService, times(2)).updateLineItems(any(), anyBoolean()); + + final ArgumentCaptor> planResponseCaptor = ArgumentCaptor.forClass(List.class); + verify(lineItemService, times(2)).updateLineItems(planResponseCaptor.capture(), anyBoolean()); + + assertThat(planResponseCaptor.getAllValues().get(1)) + .isEqualTo(singletonList(LineItemMetaData.builder().lineItemId("id2") + .deliverySchedules(singletonList(DeliverySchedule.builder() + .planId("id2") + .startTimeStamp(now.minusHours(1)) + .endTimeStamp(now.plusHours(1)) + .updatedTimeStamp(now) + .tokens(singleton(Token.of(1, 300))).build())) + .accountId("2").build())); + } + + @SuppressWarnings("unchecked") + @Test + public void getIdToLineItemMetaDataShouldReturnExpectedResult() { + // when + plannerService.updateLineItemMetaData(); + + // then + final ArgumentCaptor> planResponseCaptor = ArgumentCaptor.forClass(List.class); + verify(lineItemService).updateLineItems(planResponseCaptor.capture(), anyBoolean()); + + assertThat(planResponseCaptor.getValue()) + .containsOnly( + givenLineItemMetaData("lineItem1", "1001", "rubicon", now), + givenLineItemMetaData("lineItem2", "1002", "appnexus", now)); + } + + @SuppressWarnings("unchecked") + @Test + public void getIdToLineItemMetaDataShouldNotCallUpdateLineItemAfterFailedRequest() { + // given + givenPlanHttpResponse(404, null); + + // when + plannerService.updateLineItemMetaData(); + + // then + final ArgumentCaptor> planResponseCaptor = ArgumentCaptor.forClass(List.class); + verify(lineItemService, never()).updateLineItems(planResponseCaptor.capture(), anyBoolean()); + } + + @SuppressWarnings("unchecked") + @Test + public void getIdToLineItemMetaDataShouldNotCallUpdateLineItemsWhenBodyIsNull() { + // given + givenPlanHttpResponse(200, null); + + // when + plannerService.updateLineItemMetaData(); + + // then + final ArgumentCaptor> planResponseCaptor = ArgumentCaptor.forClass(List.class); + verify(lineItemService, never()).updateLineItems(planResponseCaptor.capture(), anyBoolean()); + } + + @SuppressWarnings("unchecked") + @Test + public void getIdToLineItemMetaDataShouldNotCallUpdateLineItemWhenBodyCantBeParsed() { + // given + givenPlanHttpResponse(200, "{"); + + // when + plannerService.updateLineItemMetaData(); + + // then + final ArgumentCaptor> planResponseCaptor = ArgumentCaptor.forClass(List.class); + verify(lineItemService, never()).updateLineItems(planResponseCaptor.capture(), anyBoolean()); + } + + @SuppressWarnings("unchecked") + @Test + public void getIdToLineItemsShouldNotCallLineItemsUpdateAfter404Response() { + // given + givenPlanHttpResponse(404, null); + + // when + plannerService.updateLineItemMetaData(); + + // then + final ArgumentCaptor> planResponseCaptor = ArgumentCaptor.forClass(List.class); + verify(lineItemService, never()).updateLineItems(planResponseCaptor.capture(), anyBoolean()); + } + + @SuppressWarnings("unchecked") + @Test + public void getIdToLineItemsShouldNotCallLineItemsUpdateWhenBodyIsNull() { + // given + givenPlanHttpResponse(200, null); + + // when + plannerService.updateLineItemMetaData(); + + // then + final ArgumentCaptor> planResponseCaptor = ArgumentCaptor.forClass(List.class); + verify(lineItemService, never()).updateLineItems(planResponseCaptor.capture(), anyBoolean()); + } + + private void givenPlanHttpResponse(int statusCode, String response) { + final HttpClientResponse httpClientResponse = HttpClientResponse.of(statusCode, null, response); + given(httpClient.get(startsWith(PLAN_ENDPOINT), any(), anyLong())) + .willReturn(Future.succeededFuture(httpClientResponse)); + } + + private static LineItemMetaData givenLineItemMetaData(String lineItemId, String account, String bidderCode, + ZonedDateTime now) { + return LineItemMetaData.builder() + .lineItemId(lineItemId) + .dealId("dealId") + .accountId(account) + .source(bidderCode) + .price(Price.of(BigDecimal.ONE, "USD")) + .relativePriority(5) + .startTimeStamp(now) + .endTimeStamp(now) + .updatedTimeStamp(now) + .frequencyCaps(singletonList(FrequencyCap.builder() + .fcapId("fcap").count(6L).periods(7).periodType("day").build())) + .deliverySchedules(singletonList(DeliverySchedule.builder() + .planId("plan") + .startTimeStamp(now.minusHours(1)) + .endTimeStamp(now.plusHours(1)) + .updatedTimeStamp(now) + .tokens(singleton(Token.of(1, 300))).build())) + .targeting(mapper.createObjectNode()) + .build(); + } + + @SafeVarargs + private final void givenHttpClientReturnsResponses(Future... futureHttpClientResponses) { + BDDMockito.BDDMyOngoingStubbing> stubbing = + given(httpClient.get(anyString(), any(), anyLong())); + + // setup multiple answers + for (Future futureHttpClientResponse : futureHttpClientResponses) { + stubbing = stubbing.willReturn(futureHttpClientResponse); + } + } +} diff --git a/src/test/java/org/prebid/server/deals/RegisterServiceTest.java b/src/test/java/org/prebid/server/deals/RegisterServiceTest.java new file mode 100644 index 00000000000..139bc81234b --- /dev/null +++ b/src/test/java/org/prebid/server/deals/RegisterServiceTest.java @@ -0,0 +1,270 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.deals.events.AdminEventService; +import org.prebid.server.deals.model.AdminCentralResponse; +import org.prebid.server.deals.model.AlertPriority; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.deals.model.PlannerProperties; +import org.prebid.server.deals.model.ServicesCommand; +import org.prebid.server.deals.proto.CurrencyServiceState; +import org.prebid.server.deals.proto.RegisterRequest; +import org.prebid.server.deals.proto.Status; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.health.HealthMonitor; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class RegisterServiceTest extends VertxTest { + + private static final String PLAN_ENDPOINT = "plan-endpoint"; + private static final String REGISTER_ENDPOINT = "register-endpoint"; + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + private static final String PBS_HOST = "pbs-host"; + private static final String PBS_REGION = "pbs-region"; + private static final String PBS_VENDOR = "pbs-vendor"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + private RegisterService registerService; + @Mock + private HealthMonitor healthMonitor; + @Mock + private CurrencyConversionService currencyConversionService; + @Mock + private AdminEventService adminEventService; + @Mock + private DeliveryProgressService deliveryProgressService; + @Mock + private HttpClient httpClient; + @Mock + private AlertHttpService alertHttpService; + @Mock + private Vertx vertx; + + private ZonedDateTime fixedDate; + + @Before + public void setUp() { + fixedDate = ZonedDateTime.now(Clock.fixed(Instant.parse("2019-07-26T10:00:00Z"), ZoneOffset.UTC)); + registerService = new RegisterService( + PlannerProperties.builder() + .planEndpoint(PLAN_ENDPOINT) + .registerEndpoint(REGISTER_ENDPOINT) + .timeoutMs(100L) + .registerPeriodSeconds(60L) + .username(USERNAME) + .password(PASSWORD) + .build(), + DeploymentProperties.builder().pbsHostId(PBS_HOST).pbsRegion(PBS_REGION).pbsVendor(PBS_VENDOR).build(), + adminEventService, + deliveryProgressService, + alertHttpService, + healthMonitor, + currencyConversionService, + httpClient, + vertx, + jacksonMapper); + + givenRegisterHttpResponse(200); + } + + @Test + public void initializeShouldSetRegisterTimer() throws JsonProcessingException { + // given + given(vertx.setPeriodic(anyLong(), any())).willReturn(1L); + given(healthMonitor.calculateHealthIndex()).willReturn(BigDecimal.ONE); + given(currencyConversionService.getLastUpdated()).willReturn(fixedDate); + + // when + registerService.initialize(); + + // then + verify(vertx).setPeriodic(eq(60000L), any()); + verify(vertx, never()).cancelTimer(anyLong()); + + final ArgumentCaptor registerBodyCaptor = ArgumentCaptor.forClass(String.class); + verify(httpClient).post(startsWith(REGISTER_ENDPOINT), any(), registerBodyCaptor.capture(), anyLong()); + assertThat(registerBodyCaptor.getValue()).isEqualTo(mapper.writeValueAsString( + RegisterRequest.of(BigDecimal.ONE, Status.of(CurrencyServiceState.of("2019-07-26T10:00:00.000Z"), null), + PBS_HOST, PBS_REGION, PBS_VENDOR))); + } + + @Test + public void initializeShouldSetDeliveryReportToRegisterRequest() throws JsonProcessingException { + // given + given(vertx.setPeriodic(anyLong(), any())).willReturn(1L); + given(healthMonitor.calculateHealthIndex()).willReturn(BigDecimal.ONE); + given(deliveryProgressService.getOverallDeliveryProgressReport()).willReturn(DeliveryProgressReport.builder() + .reportId("reportId").build()); + + // when + registerService.initialize(); + + // then + final ArgumentCaptor registerBodyCaptor = ArgumentCaptor.forClass(String.class); + verify(httpClient).post(startsWith(REGISTER_ENDPOINT), any(), registerBodyCaptor.capture(), anyLong()); + assertThat(registerBodyCaptor.getValue()).isEqualTo(mapper.writeValueAsString( + RegisterRequest.of(BigDecimal.ONE, + Status.of(null, DeliveryProgressReport.builder().reportId("reportId").build()), + PBS_HOST, PBS_REGION, PBS_VENDOR))); + } + + @Test + public void registerShouldNotCallAdminCentralWhenResponseIsEmpty() { + // given + given(healthMonitor.calculateHealthIndex()).willReturn(BigDecimal.ONE); + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), ""))); + + // when + registerService.register(MultiMap.caseInsensitiveMultiMap()); + + // then + verifyZeroInteractions(adminEventService); + } + + @Test + public void registerShouldCallAdminCentralWhenResponseIsNotEmpty() throws JsonProcessingException { + // given + given(healthMonitor.calculateHealthIndex()).willReturn(BigDecimal.ONE); + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), + mapper.writeValueAsString(AdminCentralResponse.of(null, null, null, null, null, + ServicesCommand.of("stop")))))); + + // when + registerService.register(MultiMap.caseInsensitiveMultiMap()); + + // then + verify(adminEventService).publishAdminCentralEvent(any()); + } + + @Test + public void registerShouldNotCallAdminCentralWhenFutureFailed() { + // given + given(healthMonitor.calculateHealthIndex()).willReturn(BigDecimal.ONE); + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.failedFuture("failed")); + + // when + registerService.register(MultiMap.caseInsensitiveMultiMap()); + + // then + verifyZeroInteractions(adminEventService); + } + + @Test + public void registerShouldCallAlertServiceWhenFutureFailed() { + // given + given(healthMonitor.calculateHealthIndex()).willReturn(BigDecimal.ONE); + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.failedFuture("failed")); + + // when + registerService.register(MultiMap.caseInsensitiveMultiMap()); + + // then + verify(alertHttpService).alertWithPeriod(eq("register"), eq("pbs-register-client-error"), + eq(AlertPriority.MEDIUM), + eq("Error occurred while registering with the Planner:" + + " io.vertx.core.impl.NoStackTraceThrowable: failed")); + } + + @Test + public void registerShouldCallAlertServiceResetWhenRequestWasSuccessful() { + // given + given(healthMonitor.calculateHealthIndex()).willReturn(BigDecimal.ONE); + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), ""))); + + // when + registerService.register(MultiMap.caseInsensitiveMultiMap()); + + // then + verify(alertHttpService).resetAlertCount(eq("pbs-register-client-error")); + } + + @Test + public void registerShouldNotSendAdminEventWhenResponseStatusIsBadRequest() { + // given + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(400, null, null))); + + // when + registerService.register(MultiMap.caseInsensitiveMultiMap()); + + // then + verifyZeroInteractions(adminEventService); + } + + @Test + public void registerShouldThrowPrebidExceptionWhenResponseIsInvalidJson() { + // given + given(httpClient.post(anyString(), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, "{"))); + + // when and then + assertThatThrownBy(() -> registerService.register(MultiMap.caseInsensitiveMultiMap())) + .isInstanceOf(PreBidException.class) + .hasMessage("Cannot parse register response: {"); + } + + @Test + public void suspendShouldStopRegisterTimer() { + // when + registerService.suspend(); + + // then + verify(vertx, times(1)).cancelTimer(anyLong()); + } + + @Test + public void initializeShouldCallHealthMonitor() { + // when + registerService.initialize(); + + // then + verify(healthMonitor).calculateHealthIndex(); + } + + private void givenRegisterHttpResponse(int statusCode) { + final HttpClientResponse httpClientResponse = HttpClientResponse.of(statusCode, null, null); + given(httpClient.post(startsWith(REGISTER_ENDPOINT), any(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(httpClientResponse)); + } +} diff --git a/src/test/java/org/prebid/server/deals/TargetingServiceTest.java b/src/test/java/org/prebid/server/deals/TargetingServiceTest.java new file mode 100644 index 00000000000..5a4611c755e --- /dev/null +++ b/src/test/java/org/prebid/server/deals/TargetingServiceTest.java @@ -0,0 +1,900 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import org.junit.Before; +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.deals.targeting.TargetingDefinition; +import org.prebid.server.deals.targeting.interpret.And; +import org.prebid.server.deals.targeting.interpret.DomainMetricAwareExpression; +import org.prebid.server.deals.targeting.interpret.InIntegers; +import org.prebid.server.deals.targeting.interpret.InStrings; +import org.prebid.server.deals.targeting.interpret.IntersectsIntegers; +import org.prebid.server.deals.targeting.interpret.IntersectsSizes; +import org.prebid.server.deals.targeting.interpret.IntersectsStrings; +import org.prebid.server.deals.targeting.interpret.Matches; +import org.prebid.server.deals.targeting.interpret.Not; +import org.prebid.server.deals.targeting.interpret.Or; +import org.prebid.server.deals.targeting.interpret.Within; +import org.prebid.server.deals.targeting.model.GeoRegion; +import org.prebid.server.deals.targeting.model.Size; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; +import org.prebid.server.deals.targeting.syntax.TargetingCategory.Type; +import org.prebid.server.exception.TargetingSyntaxException; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtGeo; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.io.IOException; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TargetingServiceTest extends VertxTest { + + private TargetingService targetingService; + + @Before + public void setUp() { + targetingService = new TargetingService(jacksonMapper); + } + + @Test + public void parseTargetingDefinitionShouldReturnValidExpression() throws IOException { + // when + final TargetingDefinition definition = targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-valid-targeting-definition.json"), "lineItemId"); + + // then + assertThat(definition).isNotNull().isEqualTo(TargetingDefinition.of( + new And(asList( + new IntersectsSizes(category(Type.size), asList(Size.of(300, 250), Size.of(400, 200))), + new IntersectsStrings(category(Type.mediaType), asList("banner", "video")), + new Or(asList( + new DomainMetricAwareExpression(new Matches(category(Type.domain), "*nba.com*"), + "lineItemId"), + new DomainMetricAwareExpression(new Matches(category(Type.domain), "nba.com*"), + "lineItemId"), + new DomainMetricAwareExpression( + new InStrings(category(Type.domain), asList("nba.com", "cnn.com")), + "lineItemId") + )), + new Or(asList( + new Matches(category(Type.referrer), "*sports*"), + new Matches(category(Type.referrer), "http://nba.com/lalakers*"), + new InStrings(category(Type.referrer), + asList("http://cnn.com/culture", "http://cnn.com/weather")) + )), + new Or(asList( + new Matches(category(Type.appBundle), "*com.google.calendar*"), + new Matches(category(Type.appBundle), "com.google.calendar*"), + new InStrings(category(Type.appBundle), + asList("com.google.calendar", "com.tmz")) + )), + new Or(asList( + new Matches(category(Type.adslot), "*/home/top*"), + new Matches(category(Type.adslot), "/home/top*"), + new InStrings(category(Type.adslot), asList("/home/top", "/home/bottom")) + )), + new InStrings(category(Type.deviceGeoExt, "vendor.attribute"), + asList("device_geo_ext_value1", "device_geo_ext_value2")), + new InStrings(category(Type.deviceGeoExt, "vendor.nested.attribute"), + asList("device_geo_ext_nested_value1", "device_geo_ext_nested_value2")), + new InStrings(category(Type.deviceExt, "vendor.attribute"), + asList("device_ext_value1", "device_ext_value2")), + new InStrings(category(Type.deviceExt, "vendor.nested.attribute"), + asList("device_ext_nested_value1", "device_ext_nested_value2")), + new InIntegers(category(Type.pagePosition), asList(1, 3)), + new Within(category(Type.location), GeoRegion.of(123.456f, 789.123f, 10.0f)), + new Or(asList( + new InIntegers(category(Type.bidderParam, "rubicon.siteId"), asList(123, 321)), + new IntersectsIntegers(category(Type.bidderParam, "rubicon.siteId"), asList(123, 321)) + )), + new Or(asList( + new Matches(category(Type.bidderParam, "appnexus.placementName"), "*somePlacement*"), + new Matches(category(Type.bidderParam, "appnexus.placementName"), "somePlacement*"), + new InStrings(category(Type.bidderParam, "appnexus.placementName"), + asList("somePlacement1", "somePlacement2")), + new IntersectsStrings(category(Type.bidderParam, "appnexus.placementName"), + asList("somePlacement1", "somePlacement2")) + )), + new Or(asList( + new IntersectsStrings( + category(Type.userSegment, "rubicon"), asList("123", "234", "345")), + new IntersectsStrings( + category(Type.userSegment, "bluekai"), asList("123", "234", "345")) + )), + new Or(asList( + new InIntegers(category(Type.userFirstPartyData, "someId"), asList(123, 321)), + new IntersectsIntegers(category(Type.userFirstPartyData, "someId"), asList(123, 321)) + )), + new Or(asList( + new Matches(category(Type.userFirstPartyData, "sport"), "*hockey*"), + new Matches(category(Type.userFirstPartyData, "sport"), "hockey*"), + new InStrings(category(Type.userFirstPartyData, "sport"), asList("hockey", "soccer")), + new IntersectsStrings( + category(Type.userFirstPartyData, "sport"), asList("hockey", "soccer")) + )), + new Or(asList( + new InIntegers(category(Type.siteFirstPartyData, "someId"), asList(123, 321)), + new IntersectsIntegers(category(Type.siteFirstPartyData, "someId"), asList(123, 321)) + )), + new Or(asList( + new Matches(category(Type.siteFirstPartyData, "sport"), "*hockey*"), + new Matches(category(Type.siteFirstPartyData, "sport"), "hockey*"), + new InStrings(category(Type.siteFirstPartyData, "sport"), asList("hockey", "soccer")), + new IntersectsStrings( + category(Type.siteFirstPartyData, "sport"), asList("hockey", "soccer")) + )), + new InIntegers(category(Type.dow), asList(5, 6)), + new InIntegers(category(Type.hour), asList(10, 11, 12, 13, 14)) + )))); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenTopLevelFieldIsNonObject() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-non-object.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected array, got NUMBER"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenTopLevelObjectHasMultipleFields() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-multiple-fields.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected only one element in the object, got 2"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenBooleanOperatorArgumentHasMultipleFields() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-multiple-fields-boolean-args.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected only one element in the object, got 2"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenFieldIsUnknown() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-unknown-field.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected either boolean operator or targeting category, got aaa"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenAndWithNonArray() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-and-with-non-array.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected array, got OBJECT"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenNotWithNonObject() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-not-with-non-object.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected object, got ARRAY"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenCategoryWithNonObject() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-category-non-object.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected object, got NUMBER"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenFunctionHasMultipleFields() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-multiple-fields-function.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected only one element in the object, got 2"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenUnknownFunction() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-unknown-function.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected matching function, got $abc"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenCategoryWithIncompatibleFunction() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-category-incompatible-function.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected $intersects matching function, got $in"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenIntersectsNonArray() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-intersects-non-array.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected array, got OBJECT"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenIntersectsSizesWithNonObjects() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-intersects-sizes-non-objects.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected object, got NUMBER"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenIntersectsSizesWithNonReadableSize() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-intersects-sizes-non-readable-size.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessageStartingWith("Exception occurred while parsing size: " + + "Cannot deserialize instance of `java.lang.Integer`"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenIntersectsSizesWithEmptySize() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-intersects-sizes-empty-size.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Height and width in size definition could not be null or missing"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenIntersectsStringsWithNonString() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-intersects-strings-non-string.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected string, got NUMBER"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenIntersectsStringsWithEmptyString() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-intersects-strings-empty.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("String value could not be empty"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenUnknownStringFunction() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-unknown-string-function.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected matching function, got $abc"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenCategoryWithIncompatibleStringFunction() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-category-incompatible-string-function.json"), + null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected one of $matches, $in matching functions, got $intersects"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenMatchesWithNonString() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-matches-non-string.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected string, got NUMBER"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenMatchesWithEmptyString() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-matches-empty.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("String value could not be empty"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenInIntegersWithNonInteger() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-in-integers-non-integer.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected integer, got STRING"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenCategoryWithIncompatibleGeoFunction() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-category-incompatible-geo-function.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected $within matching function, got $intersects"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenWithinWithNonObject() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-within-non-object.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected object, got ARRAY"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenWithinWithNonReadableGeoRegion() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-within-non-readable-georegion.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessageStartingWith("Exception occurred while parsing geo region: " + + "Cannot deserialize instance of `java.lang.Float`"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenWithinWithEmptyGeoRegion() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-within-empty-georegion.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Lat, lon and radiusMiles in geo region definition could not be null or missing"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenCategoryWithIncompatibleSegmentFunction() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom( + "targeting/test-invalid-targeting-definition-category-incompatible-segment-function.json"), + null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected $intersects matching function, got $in"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenUnknownTypedFunction() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-unknown-typed-function.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected matching function, got $abc"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenCategoryWithIncompatibleTypedFunction() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-category-incompatible-typed-function.json"), + null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected one of $matches, $in, $intersects matching functions, got $within"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenTypedFunctionWithIncompatibleType() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-typed-function-incompatible-type.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected integer or string, got BOOLEAN"); + } + + @Test + public void parseTargetingDefinitionShouldFailWhenTypedFunctionWithMixedTypes() { + assertThatThrownBy(() -> targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-invalid-targeting-definition-typed-function-mixed-types.json"), null)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Expected integer, got STRING"); + } + + @Test + public void matchesTargetingShouldReturnTrue() { + // given + final TargetingDefinition targetingDefinition = TargetingDefinition.of( + new And(asList( + new IntersectsSizes(category(Type.size), asList(Size.of(300, 250), Size.of(400, 200))), + new IntersectsStrings(category(Type.mediaType), asList("banner", "video")), + new DomainMetricAwareExpression(new Matches(category(Type.domain), "*nba.com*"), "lineItemId"), + new InIntegers(category(Type.pagePosition), asList(1, 3)), + new Within(category(Type.location), GeoRegion.of(50.424744f, 30.506435f, 10.0f)), + new InIntegers(category(Type.bidderParam, "rubicon.siteId"), asList(123, 321)), + new IntersectsStrings(category(Type.userSegment, "rubicon"), asList("123", "234", "345")), + new IntersectsIntegers(category(Type.userFirstPartyData, "someId"), asList(123, 321))))); + + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder() + .domain("lakers.nba.com") + .build()) + .device(Device.builder() + .geo(Geo.builder() + .lat(50.432069f) + .lon(30.516455f) + .build()) + .build()) + .user(User.builder() + .data(asList( + Data.builder() + .id("rubicon") + .segment(asList( + Segment.builder().id("234").build(), + Segment.builder().id("567").build())) + .build(), + Data.builder() + .id("bluekai") + .segment(asList( + Segment.builder().id("789").build(), + Segment.builder().id("890").build())) + .build())) + .ext(ExtUser.builder().data(mapper.valueToTree(singletonMap("someId", asList(123, 456)))) + .build()) + .build()) + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .banner(Banner.builder() + .format(asList( + Format.builder().w(300).h(500).build(), + Format.builder().w(300).h(250).build(), + Format.builder().w(400).h(500).build())) + .pos(3) + .build()) + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.valueToTree( + singletonMap("rubicon", singletonMap("siteId", 123)))))) + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isTrue(); + assertThat(txnLog.lineItemsMatchedDomainTargeting()).containsOnly("lineItemId"); + } + + @Test + public void matchesTargetingShouldReturnTrueForNotInIntegers() throws IOException { + // given + final TargetingDefinition targetingDefinition = targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-not-in-integers-definition.json"), "lineItemId"); + + final BidRequest bidRequest = BidRequest.builder() + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.valueToTree( + singletonMap("rubicon", singletonMap("siteId", 123)))))) + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isTrue(); + } + + @Test + public void matchesTargetingShouldReturnTrueForNotInStrings() throws IOException { + // given + final TargetingDefinition targetingDefinition = targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-not-in-strings-definition.json"), "lineItemId"); + + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder() + .domain("lakers.nba.com") + .build()) + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isTrue(); + } + + @Test + public void matchesTargetingShouldReturnTrueForNotIntersectsInteger() throws IOException { + final TargetingDefinition targetingDefinition = targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-not-intersects-integer-definition.json"), "lineItemId"); + + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder() + .ext(ExtUser.builder().data(mapper.valueToTree(singletonMap("someId", asList(123, 456)))) + .build()) + .build()) + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder().build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isTrue(); + } + + @Test + public void matchesTargetingShouldReturnTrueForNotIntersectsSizes() throws IOException { + // given + final TargetingDefinition targetingDefinition = targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-not-intersects-sizes-definition.json"), "lineItemId"); + + final BidRequest bidRequest = BidRequest.builder().build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .banner(Banner.builder() + .format(asList( + Format.builder().w(300).h(500).build(), + Format.builder().w(300).h(250).build(), + Format.builder().w(400).h(500).build())) + .build()) + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isTrue(); + } + + @Test + public void matchesTargetingShouldReturnTrueForNotWithin() throws IOException { + // given + final TargetingDefinition targetingDefinition = targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-not-within-definition.json"), "lineItemId"); + + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder() + .geo(Geo.builder() + .lat(50.432069f) + .lon(30.516455f) + .build()) + .build()) + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isTrue(); + } + + @Test + public void matchesTargetingShouldReturnTrueForNotFalseAnd() throws IOException { + // given + final TargetingDefinition targetingDefinition = targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-not-and-definition.json"), "lineItemId"); + + final BidRequest bidRequest = BidRequest.builder().build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .banner(Banner.builder() + .format(asList( + Format.builder().w(300).h(500).build(), + Format.builder().w(300).h(250).build(), + Format.builder().w(400).h(500).build())) + .build()) + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isTrue(); + } + + @Test + public void matchesTargetingShouldReturnTrueForNotMatches() throws IOException { + // given + final TargetingDefinition targetingDefinition = targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-not-matches-definition.json"), "lineItemId"); + + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder() + .domain("lakers.uefa.com") + .build()) + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isTrue(); + } + + @Test + public void matchesTargetingShouldReturnFalseForNotTrueOr() throws IOException { + // given + final TargetingDefinition targetingDefinition = targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-not-or-definition.json"), "lineItemId"); + + final BidRequest bidRequest = BidRequest.builder().build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .banner(Banner.builder() + .format(asList( + Format.builder().w(300).h(500).build(), + Format.builder().w(300).h(250).build(), + Format.builder().w(400).h(500).build())) + .build()) + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isFalse(); + } + + @Test + public void matchesTargetingShouldReturnTrueForDeviceExt() throws IOException { + // given + final TargetingDefinition targetingDefinition = targetingService.parseTargetingDefinition( + jsonFrom("targeting/test-device-targeting.json"), "lineItemId"); + + final ExtGeo extGeo = ExtGeo.of(); + extGeo.addProperty("netacuity", mapper.createObjectNode().set("country", new TextNode("us"))); + final ExtDevice extDevice = ExtDevice.empty(); + extDevice.addProperty("deviceatlas", mapper.createObjectNode().set("browser", new TextNode("Chrome"))); + final BidRequest bidRequest = BidRequest.builder() + .device(Device + .builder() + .geo(Geo.builder().ext(extGeo) + .build()) + .ext(extDevice) + .build()).build(); + + final TxnLog txnLog = TxnLog.create(); + + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder().build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isTrue(); + } + + @Test + public void matchesTargetingShouldReturnFalseForNotInIntegers() { + // given + final TargetingDefinition targetingDefinition = TargetingDefinition.of( + new Not(new InIntegers(category(Type.bidderParam, "rubicon.siteId"), asList(123, 778)))); + + final BidRequest bidRequest = BidRequest.builder() + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .set("bidder", mapper.valueToTree( + singletonMap("rubicon", singletonMap("siteId", 123)))))) + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isFalse(); + } + + @Test + public void matchesTargetingShouldReturnFalseForNotInStrings() { + // given + final TargetingDefinition targetingDefinition = TargetingDefinition.of( + new Not(new InStrings(category(Type.domain), asList("nba.com", "cnn.com")))); + + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder() + .domain("nba.com") + .build()) + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isFalse(); + } + + @Test + public void matchesTargetingShouldReturnFalseForNotIntersectsInteger() { + final TargetingDefinition targetingDefinition = TargetingDefinition.of( + new Not(new IntersectsIntegers(category(Type.userFirstPartyData, "someId"), asList(123, 321)))); + + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder() + .ext(ExtUser.builder().data(mapper.valueToTree(singletonMap("someId", asList(123, 456)))) + .build()) + .build()) + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder().build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isFalse(); + } + + @Test + public void matchesTargetingShouldReturnFalseForNotIntersectsSizes() { + // given + final TargetingDefinition targetingDefinition = TargetingDefinition.of( + new Not(new IntersectsSizes(category(Type.size), asList(Size.of(300, 250), Size.of(400, 200))))); + + final BidRequest bidRequest = BidRequest.builder().build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .banner(Banner.builder() + .format(asList( + Format.builder().w(300).h(500).build(), + Format.builder().w(300).h(250).build(), + Format.builder().w(400).h(500).build())) + .build()) + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isFalse(); + } + + @Test + public void matchesTargetingShouldReturnFalseForNotWithin() { + // given + final TargetingDefinition targetingDefinition = TargetingDefinition.of( + new Not(new Within(category(Type.location), GeoRegion.of(50.424744f, 30.506435f, 10.0f)))); + + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder() + .geo(Geo.builder() + .lat(50.432069f) + .lon(30.516455f) + .build()) + .build()) + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isFalse(); + } + + @Test + public void matchesTargetingShouldReturnFalseForNotTrueAnd() { + // given + final TargetingDefinition targetingDefinition = TargetingDefinition.of( + new Not(new And(asList( + new IntersectsSizes(category(Type.size), asList(Size.of(300, 250), Size.of(400, 200))), + new IntersectsStrings(category(Type.mediaType), asList("banner", "video")))))); + + final BidRequest bidRequest = BidRequest.builder().build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + .banner(Banner.builder() + .format(asList( + Format.builder().w(300).h(500).build(), + Format.builder().w(300).h(250).build(), + Format.builder().w(400).h(500).build())) + .build()) + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isFalse(); + } + + @Test + public void matchesTargetingShouldReturnFalseForNotMatches() { + // given + final TargetingDefinition targetingDefinition = TargetingDefinition.of( + new Not(new Matches(category(Type.domain), "*nba.com*"))); + + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder() + .domain("lakers.nba.com") + .build()) + .build(); + + final TxnLog txnLog = TxnLog.create(); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(bidRequest) + .txnLog(txnLog) + .build(); + + final Imp imp = Imp.builder() + + .build(); + + // when and then + assertThat(targetingService.matchesTargeting(auctionContext, imp, targetingDefinition)).isFalse(); + } + + private static JsonNode jsonFrom(String file) throws IOException { + return mapper.readTree(TargetingServiceTest.class.getResourceAsStream(file)); + } + + private static TargetingCategory category(Type type) { + return new TargetingCategory(type); + } + + private static TargetingCategory category(Type type, String path) { + return new TargetingCategory(type, path); + } +} diff --git a/src/test/java/org/prebid/server/deals/UserServiceTest.java b/src/test/java/org/prebid/server/deals/UserServiceTest.java new file mode 100644 index 00000000000..9484314d101 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/UserServiceTest.java @@ -0,0 +1,598 @@ +package org.prebid.server.deals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.Future; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.analytics.model.NotificationEvent; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.cache.model.CacheHttpRequest; +import org.prebid.server.cache.model.DebugHttpCall; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.cookie.model.UidWithExpiry; +import org.prebid.server.cookie.proto.Uids; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.model.ExtUser; +import org.prebid.server.deals.model.Segment; +import org.prebid.server.deals.model.User; +import org.prebid.server.deals.model.UserData; +import org.prebid.server.deals.model.UserDetails; +import org.prebid.server.deals.model.UserDetailsProperties; +import org.prebid.server.deals.model.UserDetailsRequest; +import org.prebid.server.deals.model.UserDetailsResponse; +import org.prebid.server.deals.model.UserId; +import org.prebid.server.deals.model.UserIdRule; +import org.prebid.server.deals.model.WinEventNotification; +import org.prebid.server.deals.proto.FrequencyCap; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; + +import java.io.IOException; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeoutException; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class UserServiceTest extends VertxTest { + + private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .toFormatter(); + + private static final String USER_DETAILS_ENDPOINT = "http://user-data.com"; + private static final String WIN_EVENT_ENDPOINT = "http://win-event.com"; + private static final String DATA_CENTER_REGION = "region"; + private static final long CONFIG_TIMEOUT = 300L; + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private LineItemService lineItemService; + @Mock + private HttpClient httpClient; + @Mock + private Metrics metrics; + + private List userIdRules; + private Clock clock; + + private UserService userService; + + private UidsCookie uidsCookie; + private AuctionContext auctionContext; + private Timeout timeout; + private ZonedDateTime now; + + @Before + public void setUp() { + clock = Clock.fixed(Instant.parse("2019-07-26T10:00:00Z"), ZoneOffset.UTC); + now = ZonedDateTime.now(clock); + userIdRules = singletonList(UserIdRule.of("khaos", "uid", "rubicon")); + + userService = new UserService( + UserDetailsProperties.of(USER_DETAILS_ENDPOINT, WIN_EVENT_ENDPOINT, CONFIG_TIMEOUT, userIdRules), + DATA_CENTER_REGION, + lineItemService, + httpClient, + clock, + metrics, + jacksonMapper); + + uidsCookie = new UidsCookie(Uids.builder() + .uids(singletonMap("rubicon", new UidWithExpiry("uid", null))) + .build(), jacksonMapper); + auctionContext = AuctionContext.builder().uidsCookie(uidsCookie).debugHttpCalls(new HashMap<>()).build(); + + timeout = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())).create(500L); + } + + @Test + public void getUserDetailsShouldReturnEmptyUserDetailsWhenUidsAreEmpty() { + // given + final UidsCookie uidsCookie = new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper); + final AuctionContext context = AuctionContext.builder().uidsCookie(uidsCookie) + .debugHttpCalls(new HashMap<>()).build(); + + // when + final UserDetails result = userService.getUserDetails(context, timeout).result(); + + // then + verify(metrics).updateUserDetailsRequestPreparationFailed(); + verifyZeroInteractions(httpClient); + + assertEquals(UserDetails.empty(), result); + } + + @Test + public void getUserDetailsShouldReturnReturnEmptyUserDetailsWhenUidsDoesNotContainRuleLocation() { + // given + final List ruleWithMissingLocation = singletonList( + UserIdRule.of("khaos", "uid", "bad_location")); + + userService = new UserService( + UserDetailsProperties.of( + USER_DETAILS_ENDPOINT, WIN_EVENT_ENDPOINT, CONFIG_TIMEOUT, ruleWithMissingLocation), + DATA_CENTER_REGION, + lineItemService, + httpClient, + clock, + metrics, + jacksonMapper); + + // when + final UserDetails result = userService.getUserDetails(auctionContext, timeout).result(); + + // then + verify(metrics).updateUserDetailsRequestPreparationFailed(); + verifyZeroInteractions(httpClient); + + assertEquals(UserDetails.empty(), result); + } + + @Test + public void getUserDetailsShouldSendPostRequestWithExpectedParameters() throws IOException { + // given + given(httpClient.post(anyString(), anyString(), anyLong())).willReturn(Future.failedFuture("something")); + + // when + userService.getUserDetails(auctionContext, timeout); + + // then + final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(httpClient).post(eq(USER_DETAILS_ENDPOINT), captor.capture(), eq(CONFIG_TIMEOUT)); + + final UserDetailsRequest capturedRequest = mapper.readValue(captor.getValue(), UserDetailsRequest.class); + + assertThat(ZonedDateTime.parse(capturedRequest.getTime())).isEqualTo(UTC_MILLIS_FORMATTER.format(now)); + assertThat(capturedRequest.getIds()).hasSize(1) + .containsOnly(UserId.of("khaos", "uid")); + verify(metrics).updateUserDetailsRequestMetric(eq(false)); + verify(metrics).updateRequestTimeMetric(eq(MetricName.user_details_request_time), anyLong()); + } + + @Test + public void getUserDetailsShouldUseRemainingGlobalTimeoutIfTimeoutFromConfigurationIsGreaterThanRemaining() { + // given + given(httpClient.post(anyString(), anyString(), anyLong())).willReturn(Future.failedFuture("something")); + + userService = new UserService( + UserDetailsProperties.of(USER_DETAILS_ENDPOINT, WIN_EVENT_ENDPOINT, 600L, userIdRules), + DATA_CENTER_REGION, + lineItemService, + httpClient, + clock, + metrics, + jacksonMapper); + + // when + userService.getUserDetails(auctionContext, timeout); + + // then + verify(metrics).updateUserDetailsRequestMetric(eq(false)); + verify(metrics).updateRequestTimeMetric(eq(MetricName.user_details_request_time), anyLong()); + verify(httpClient).post(anyString(), anyString(), eq(500L)); + } + + @Test + public void getUserDetailsShouldReturnFailedFutureWhenResponseStatusIsNotOk() { + // given + given(httpClient.post(anyString(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(400, null, null))); + + // when + final Future result = userService.getUserDetails(auctionContext, timeout); + + // then + verify(metrics).updateRequestTimeMetric(eq(MetricName.user_details_request_time), anyLong()); + verify(metrics).updateUserDetailsRequestMetric(eq(false)); + verify(httpClient).post(eq(USER_DETAILS_ENDPOINT), anyString(), eq(CONFIG_TIMEOUT)); + + assertTrue(result.failed()); + assertThat(result.cause()) + .isInstanceOf(PreBidException.class) + .hasMessage("Bad response status code: 400"); + } + + @Test + public void getUserDetailsShouldReturnFailedFutureWhenErrorOccurs() { + // given + given(httpClient.post(anyString(), anyString(), anyLong())) + .willReturn(Future.failedFuture(new TimeoutException("Timeout has been exceeded"))); + + // when + final Future result = userService.getUserDetails(auctionContext, timeout); + + // then + verify(metrics).updateUserDetailsRequestMetric(eq(false)); + verify(metrics).updateRequestTimeMetric(eq(MetricName.user_details_request_time), anyLong()); + verify(httpClient).post(eq(USER_DETAILS_ENDPOINT), anyString(), eq(CONFIG_TIMEOUT)); + + assertTrue(result.failed()); + assertThat(result.cause()) + .isInstanceOf(TimeoutException.class) + .hasMessage("Timeout has been exceeded"); + } + + @Test + public void getUserDetailsShouldReturnFailedFutureWhenResponseBodyDecodingFails() { + // given + given(httpClient.post(anyString(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, "invalid_body"))); + + // when + final Future result = userService.getUserDetails(auctionContext, timeout); + + // then + verify(metrics).updateUserDetailsRequestMetric(eq(false)); + verify(metrics).updateRequestTimeMetric(eq(MetricName.user_details_request_time), anyLong()); + verify(httpClient).post(eq(USER_DETAILS_ENDPOINT), anyString(), eq(CONFIG_TIMEOUT)); + + assertTrue(result.failed()); + assertThat(result.cause()) + .isInstanceOf(PreBidException.class) + .hasMessage("Cannot parse response: invalid_body"); + } + + @Test + public void getUserDetailsShouldReturnExpectedResult() { + // given + final UserDetailsResponse response = UserDetailsResponse.of(User.of( + asList( + UserData.of("1", "rubicon", asList(Segment.of("2222"), Segment.of("3333"))), + UserData.of("2", "bluekai", asList(Segment.of("5555"), Segment.of("6666")))), + ExtUser.of(asList("L-1111", "O-2222")))); + + given(httpClient.post(anyString(), anyString(), anyLong())).willReturn( + Future.succeededFuture(HttpClientResponse.of(200, null, jacksonMapper.encode(response)))); + + // when + final UserDetails result = userService.getUserDetails(auctionContext, timeout).result(); + + // then + verify(metrics).updateUserDetailsRequestMetric(eq(true)); + verify(metrics).updateRequestTimeMetric(eq(MetricName.user_details_request_time), anyLong()); + verify(httpClient).post(eq(USER_DETAILS_ENDPOINT), anyString(), eq(CONFIG_TIMEOUT)); + + final UserDetails expectedDetails = UserDetails.of( + asList(UserData.of("1", "rubicon", asList(Segment.of("2222"), Segment.of("3333"))), + UserData.of("2", "bluekai", asList(Segment.of("5555"), Segment.of("6666")))), + asList("L-1111", "O-2222")); + + assertEquals(expectedDetails, result); + } + + @Test + public void getUserDetailsShouldReturnFailedFutureWhenUserInResponseIsNull() { + // given + final UserDetailsResponse response = UserDetailsResponse.of(null); + + given(httpClient.post(anyString(), anyString(), anyLong())).willReturn( + Future.succeededFuture(HttpClientResponse.of(200, null, jacksonMapper.encode(response)))); + + // when + final Future result = userService.getUserDetails(auctionContext, timeout); + + // then + verify(metrics).updateUserDetailsRequestMetric(eq(false)); + assertThat(result.failed()).isTrue(); + assertThat(result.cause()).isInstanceOf(PreBidException.class) + .hasMessage("Field 'user' is missing in response: {}"); + } + + @Test + public void getUserDetailsShouldReturnFailedFutureWhenUserDataInResponseIsNull() { + // given + final UserDetailsResponse response = UserDetailsResponse.of(User.of( + null, ExtUser.of(asList("L-1111", "O-2222")))); + + given(httpClient.post(anyString(), anyString(), anyLong())).willReturn( + Future.succeededFuture(HttpClientResponse.of(200, null, jacksonMapper.encode(response)))); + + // when + final Future result = userService.getUserDetails(auctionContext, timeout); + + // then + verify(metrics).updateUserDetailsRequestMetric(eq(false)); + assertThat(result.failed()).isTrue(); + assertThat(result.cause()).isInstanceOf(PreBidException.class) + .hasMessage("Field 'user.data' is missing in response: {\"user\":{\"ext\":{\"fcapIds\":[\"L-1111\"," + + "\"O-2222\"]}}}"); + } + + @Test + public void getUserDetailsShouldReturnFailedFutureWhenExtUserInResponseIsNull() { + // given + final UserDetailsResponse response = UserDetailsResponse.of(User.of( + singletonList(UserData.of("2", "bluekai", singletonList(Segment.of("6666")))), null)); + + given(httpClient.post(anyString(), anyString(), anyLong())).willReturn( + Future.succeededFuture(HttpClientResponse.of(200, null, jacksonMapper.encode(response)))); + + // when + final Future result = userService.getUserDetails(auctionContext, timeout); + + // then + verify(metrics).updateUserDetailsRequestMetric(eq(false)); + assertThat(result.failed()).isTrue(); + assertThat(result.cause()).isInstanceOf(PreBidException.class) + .hasMessage("Field 'user.ext' is missing in response: {\"user\":{\"data\":[{\"id\":\"2\",\"name\":" + + "\"bluekai\",\"segment\":[{\"id\":\"6666\"}]}]}}"); + } + + @Test + public void getUserDetailsShouldAddEmptyCachedHttpCallWhenUidsAreNotDefined() { + // given + final UidsCookie uidsCookie = new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper); + final AuctionContext context = AuctionContext.builder().uidsCookie(uidsCookie) + .debugHttpCalls(new HashMap<>()).build(); + + // when + userService.getUserDetails(context, timeout).result(); + + // then + assertThat(context.getDebugHttpCalls()).hasSize(1) + .containsEntry("userservice", singletonList(DebugHttpCall.empty())); + } + + @Test + public void getUserDetailsShouldAddEmptyCachedHttpCallWhenUserIdsAreNotDefined() { + // given + final List ruleWithMissingLocation = singletonList( + UserIdRule.of("khaos", "uid", "bad_location")); + + userService = new UserService( + UserDetailsProperties.of( + USER_DETAILS_ENDPOINT, WIN_EVENT_ENDPOINT, CONFIG_TIMEOUT, ruleWithMissingLocation), + DATA_CENTER_REGION, + lineItemService, + httpClient, + clock, + metrics, + jacksonMapper); + + // when + userService.getUserDetails(auctionContext, timeout).result(); + + // then + assertThat(auctionContext.getDebugHttpCalls()).hasSize(1) + .containsEntry("userservice", singletonList(DebugHttpCall.empty())); + } + + @Test + public void getUserDetailsShouldAddCachedHttpCallWhenThrowsPrebidException() throws JsonProcessingException { + // given + given(httpClient.post(anyString(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, "invalid_body"))); + + // when + userService.getUserDetails(auctionContext, timeout); + + // then + assertThat(auctionContext.getDebugHttpCalls()).hasSize(1) + .containsEntry("userservice", singletonList(DebugHttpCall.builder() + .requestUri("http://user-data.com") + .requestBody(mapper.writeValueAsString(UserDetailsRequest.of(UTC_MILLIS_FORMATTER.format(now), + singletonList(UserId.of("khaos", "uid"))))) + .responseStatus(200) + .responseBody("invalid_body") + .responseTimeMillis(0) + .build())); + } + + @Test + public void getUserDetailsShouldAddCachedHttpCallWhenCallCompletesSuccessful() throws JsonProcessingException { + // given + final UserDetailsResponse response = UserDetailsResponse.of(User.of( + singletonList( + UserData.of("1", "rubicon", asList(Segment.of("2222"), Segment.of("3333")))), + ExtUser.of(asList("L-1111", "O-2222")))); + + given(httpClient.post(anyString(), anyString(), anyLong())).willReturn( + Future.succeededFuture(HttpClientResponse.of(200, null, jacksonMapper.encode(response)))); + + // when + userService.getUserDetails(auctionContext, timeout).result(); + + CacheHttpRequest.of("http://user-data.com", + mapper.writeValueAsString(UserDetailsRequest.of(UTC_MILLIS_FORMATTER.format(now), + singletonList(UserId.of("khaos", "uid"))))); + + // then + assertThat(auctionContext.getDebugHttpCalls()).hasSize(1) + .containsEntry("userservice", singletonList(DebugHttpCall.builder() + .requestUri("http://user-data.com") + .requestBody(mapper.writeValueAsString(UserDetailsRequest.of(UTC_MILLIS_FORMATTER.format(now), + singletonList(UserId.of("khaos", "uid"))))) + .responseStatus(200) + .responseBody(mapper.writeValueAsString( + UserDetailsResponse.of(User.of(singletonList(UserData.of("1", "rubicon", + asList(Segment.of("2222"), Segment.of("3333")))), + ExtUser.of(asList("L-1111", "O-2222")))))) + .responseTimeMillis(0) + .build())); + } + + @Test + public void getUserDetailsShouldAddCachedHttpCallWhenCallFailed() throws JsonProcessingException { + // given + given(httpClient.post(anyString(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(400, null, null))); + + // when + userService.getUserDetails(auctionContext, timeout); + + // then + assertThat(auctionContext.getDebugHttpCalls()).hasSize(1) + .containsEntry("userservice", singletonList(DebugHttpCall.builder() + .requestUri("http://user-data.com") + .requestBody(mapper.writeValueAsString(UserDetailsRequest.of(UTC_MILLIS_FORMATTER.format(now), + singletonList(UserId.of("khaos", "uid"))))) + .responseTimeMillis(0) + .build())); + } + + @Test + public void processWinEventShouldCallMetricsPreparationFailedMetricWhenHttpClientWhenMetaDataIsMissing() { + // given + given(lineItemService.getLineItemById(any())).willReturn( + null, + LineItem.of(LineItemMetaData.builder().build(), null, null, ZonedDateTime.now(clock))); + + // when + userService.processWinEvent("lineItem1", "bidId", uidsCookie); + + // then + verify(metrics).updateWinRequestPreparationFailed(); + verifyZeroInteractions(httpClient); + } + + @Test + public void processWinEventShouldCallMetricsPreparationFailedMetricWhenHttpClientWhenUserIdsAreMissing() { + // given + final List frequencyCaps = singletonList(FrequencyCap.builder().fcapId("213").build()); + final UidsCookie emptyCookie = new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper); + + given(lineItemService.getLineItemById(any())).willReturn(LineItem.of( + LineItemMetaData.builder() + .source("rubicon") + .updatedTimeStamp(now) + .frequencyCaps(frequencyCaps) + .build(), + null, null, ZonedDateTime.now(clock))); + + // when + userService.processWinEvent("lineItem1", "bidId", emptyCookie); + + // then + verify(metrics).updateWinRequestPreparationFailed(); + verifyZeroInteractions(httpClient); + } + + @Test + public void processWinEventShouldCallMetricsWinRequestWithFalseWhenStatusIsNot200() { + // given + final NotificationEvent event = NotificationEvent.builder() + .bidId("bidId") + .lineItemId("lineItem1") + .build(); + final List frequencyCaps = singletonList(FrequencyCap.builder().fcapId("213").build()); + + given(lineItemService.getLineItemById(any())).willReturn(LineItem.of( + LineItemMetaData.builder() + .source("rubicon") + .updatedTimeStamp(now) + .frequencyCaps(frequencyCaps) + .build(), + null, null, ZonedDateTime.now(clock))); + + given(httpClient.post(anyString(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(400, null, null))); + + // when + userService.processWinEvent("lineItem1", "bidId", uidsCookie); + + // then + verify(metrics).updateWinEventRequestMetric(eq(false)); + verify(metrics).updateWinRequestTime(anyLong()); + } + + @Test + public void processWinEventShouldCallMetricsWinRequestWithFalseWhenFailedFuture() { + // given + final List frequencyCaps = singletonList(FrequencyCap.builder().fcapId("213").build()); + + given(lineItemService.getLineItemById(any())).willReturn(LineItem.of( + LineItemMetaData.builder() + .source("rubicon") + .updatedTimeStamp(now) + .frequencyCaps(frequencyCaps) + .build(), + null, null, ZonedDateTime.now(clock))); + + given(httpClient.post(anyString(), anyString(), anyLong())) + .willReturn(Future.failedFuture(new TimeoutException("timeout"))); + + // when + userService.processWinEvent("lineItem1", "bidId", uidsCookie); + + // then + verify(metrics).updateWinEventRequestMetric(eq(false)); + verify(metrics).updateWinRequestTime(anyLong()); + } + + @Test + public void processWinEventShouldCallExpectedServicesWithExpectedParameters() throws IOException { + // given + final List frequencyCaps = singletonList(FrequencyCap.builder().fcapId("213").build()); + + given(lineItemService.getLineItemById(any())).willReturn(LineItem.of( + LineItemMetaData.builder() + .source("rubicon") + .updatedTimeStamp(now) + .frequencyCaps(frequencyCaps) + .build(), + null, null, ZonedDateTime.now(clock))); + + given(httpClient.post(anyString(), anyString(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, null))); + + // when + userService.processWinEvent("lineItem1", "bidId", uidsCookie); + + // then + verify(lineItemService).getLineItemById(eq("lineItem1")); + verify(metrics).updateWinNotificationMetric(); + verify(metrics).updateWinEventRequestMetric(eq(true)); + verify(metrics).updateWinRequestTime(anyLong()); + final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(httpClient).post(eq(WIN_EVENT_ENDPOINT), captor.capture(), eq(CONFIG_TIMEOUT)); + + final WinEventNotification capturedRequest = mapper.readValue(captor.getValue(), WinEventNotification.class); + assertThat(capturedRequest.getWinEventDateTime()).isEqualTo(UTC_MILLIS_FORMATTER.format(now)); + + final WinEventNotification expectedRequestWithoutWinTime = WinEventNotification.builder() + .bidderCode("rubicon") + .bidId("bidId") + .lineItemId("lineItem1") + .region(DATA_CENTER_REGION) + .userIds(singletonList(UserId.of("khaos", "uid"))) + .lineUpdatedDateTime(now) + .frequencyCaps(frequencyCaps) + .build(); + + assertThat(capturedRequest).isEqualToIgnoringGivenFields(expectedRequestWithoutWinTime, "winEventDateTime"); + } +} diff --git a/src/test/java/org/prebid/server/deals/events/AdminEventServiceTest.java b/src/test/java/org/prebid/server/deals/events/AdminEventServiceTest.java new file mode 100644 index 00000000000..98ef9e34f94 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/events/AdminEventServiceTest.java @@ -0,0 +1,83 @@ +package org.prebid.server.deals.events; + +import io.vertx.core.Context; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.EventBus; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.deals.model.AdminCentralResponse; +import org.prebid.server.vertx.LocalMessageCodec; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(VertxUnitRunner.class) +public class AdminEventServiceTest { + + private Vertx vertx; + private EventBus eventBus; + + @Before + public void setUp(TestContext context) { + vertx = Vertx.vertx(); + vertx.exceptionHandler(context.exceptionHandler()); + + eventBus = vertx.eventBus(); + eventBus.registerCodec(LocalMessageCodec.create()); + } + + @After + public void tearDown(TestContext context) { + vertx.close(context.asyncAssertSuccess()); + } + + @Test + public void publishAuctionEventShouldPassEventToAllRecorders(TestContext testContext) { + // given + final Context initContext = vertx.getOrCreateContext(); + final Context publishContext = vertx.getOrCreateContext(); + + final AdminCentralResponse adminCentralResponse = AdminCentralResponse.of(null, null, null, null, null, null); + + final Handler consumeHandler = event -> { + // then + assertThat(event).isSameAs(adminCentralResponse); + assertThat(Vertx.currentContext()).isSameAs(initContext); + }; + + final AdminEventService adminEventService = new AdminEventService(eventBus); + final EventServiceInitializer eventServiceInitializer = new EventServiceInitializer( + emptyList(), singletonList(new ProcessorStub(consumeHandler)), eventBus); + + // when + final Async initAsync = testContext.async(); + initContext.runOnContext(ignored -> { + eventServiceInitializer.initialize(); + initAsync.complete(); + }); + initAsync.await(); + + publishContext.runOnContext(ignored -> adminEventService.publishAdminCentralEvent(adminCentralResponse)); + } + + private static class ProcessorStub implements AdminEventProcessor { + + private final Handler handler; + + ProcessorStub(Handler handler) { + this.handler = handler; + } + + @Override + public void processAdminCentralEvent(AdminCentralResponse adminCentralResponse) { + handler.handle(adminCentralResponse); + } + } +} diff --git a/src/test/java/org/prebid/server/deals/events/ApplicationEventServiceTest.java b/src/test/java/org/prebid/server/deals/events/ApplicationEventServiceTest.java new file mode 100644 index 00000000000..4aeae4e52d8 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/events/ApplicationEventServiceTest.java @@ -0,0 +1,156 @@ +package org.prebid.server.deals.events; + +import io.vertx.core.Context; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.EventBus; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.vertx.LocalMessageCodec; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(VertxUnitRunner.class) +public class ApplicationEventServiceTest { + + private Vertx vertx; + private EventBus eventBus; + + @Before + public void setUp(TestContext context) { + vertx = Vertx.vertx(); + vertx.exceptionHandler(context.exceptionHandler()); + + eventBus = vertx.eventBus(); + eventBus.registerCodec(LocalMessageCodec.create()); + } + + @After + public void tearDown(TestContext context) { + vertx.close(context.asyncAssertSuccess()); + } + + @Test + public void publishAuctionEventShouldPassEventToAllRecorders(TestContext testContext) { + // given + final Context initContext = vertx.getOrCreateContext(); + final Context publishContext = vertx.getOrCreateContext(); + + final AuctionContext auctionContext = AuctionContext.builder().txnLog(TxnLog.create()).build(); + + final Async async = testContext.async(2); + final Handler consumeHandler = event -> { + // then + assertThat(event).isSameAs(auctionContext); + assertThat(Vertx.currentContext()).isSameAs(initContext); + async.countDown(); + }; + + final ApplicationEventService applicationEventService = new ApplicationEventService(eventBus); + final EventServiceInitializer eventServiceInitializer = new EventServiceInitializer( + asList(new ProcessorStub(consumeHandler), new ProcessorStub(consumeHandler)), emptyList(), eventBus); + + // when + final Async initAsync = testContext.async(); + initContext.runOnContext(ignored -> { + eventServiceInitializer.initialize(); + initAsync.complete(); + }); + initAsync.await(); + + publishContext.runOnContext(ignored -> applicationEventService.publishAuctionEvent(auctionContext)); + } + + @Test + public void publishLineItemWinEventShouldPassEventToAllRecorders(TestContext testContext) { + // given + final Context initContext = vertx.getOrCreateContext(); + final Context publishContext = vertx.getOrCreateContext(); + + final String lineItemId = "lineItemId1"; + + final Async async = testContext.async(2); + final Handler consumeHandler = event -> { + // then + assertThat(event).isSameAs(lineItemId); + assertThat(Vertx.currentContext()).isSameAs(initContext); + async.countDown(); + }; + + final ApplicationEventService applicationEventService = new ApplicationEventService(eventBus); + final EventServiceInitializer eventServiceInitializer = new EventServiceInitializer( + asList(new ProcessorStub(consumeHandler), new ProcessorStub(consumeHandler)), emptyList(), eventBus); + + // when + final Async initAsync = testContext.async(); + initContext.runOnContext(ignored -> { + eventServiceInitializer.initialize(); + initAsync.complete(); + }); + initAsync.await(); + + publishContext.runOnContext(ignored -> applicationEventService.publishLineItemWinEvent(lineItemId)); + } + + @Test + public void publishDeliveryProgressUpdateEventShouldPassEventToAllRecorders(TestContext testContext) { + // given + final Context initContext = vertx.getOrCreateContext(); + final Context publishContext = vertx.getOrCreateContext(); + + final Async async = testContext.async(2); + final Handler consumeHandler = event -> { + // then + assertThat(event).isSameAs(null); + assertThat(Vertx.currentContext()).isSameAs(initContext); + async.countDown(); + }; + + final ApplicationEventService applicationEventService = new ApplicationEventService(eventBus); + final EventServiceInitializer eventServiceInitializer = new EventServiceInitializer( + asList(new ProcessorStub(consumeHandler), new ProcessorStub(consumeHandler)), emptyList(), eventBus); + + // when + final Async initAsync = testContext.async(); + initContext.runOnContext(ignored -> { + eventServiceInitializer.initialize(); + initAsync.complete(); + }); + initAsync.await(); + + publishContext.runOnContext(ignored -> applicationEventService.publishDeliveryUpdateEvent()); + } + + private static class ProcessorStub implements ApplicationEventProcessor { + + private final Handler handler; + + ProcessorStub(Handler handler) { + this.handler = handler; + } + + @Override + public void processAuctionEvent(AuctionContext auctionContext) { + handler.handle(auctionContext); + } + + @Override + public void processLineItemWinEvent(String lineItemId) { + handler.handle(lineItemId); + } + + @Override + public void processDeliveryProgressUpdateEvent() { + handler.handle(null); + } + } +} diff --git a/src/test/java/org/prebid/server/deals/lineitem/DeliveryPlanTest.java b/src/test/java/org/prebid/server/deals/lineitem/DeliveryPlanTest.java new file mode 100644 index 00000000000..1e104f2f8f2 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/lineitem/DeliveryPlanTest.java @@ -0,0 +1,30 @@ +package org.prebid.server.deals.lineitem; + +import org.junit.Test; +import org.prebid.server.deals.proto.DeliverySchedule; +import org.prebid.server.deals.proto.Token; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DeliveryPlanTest { + + @Test + public void getHighestUnspentTokensClassShouldReturnHighestUnspentToken() { + // given + final Set tokens = new HashSet<>(); + tokens.add(Token.of(1, 100)); + tokens.add(Token.of(2, 100)); + tokens.add(Token.of(3, 100)); + + final DeliveryPlan plan = DeliveryPlan.of(DeliverySchedule.builder().tokens(tokens).build()); + + IntStream.range(0, 150).forEach(i -> plan.incSpentToken()); + + // when and then + assertThat(plan.getHighestUnspentTokensClass()).isEqualTo(2); + } +} diff --git a/src/test/java/org/prebid/server/deals/lineitem/DeliveryProgressTest.java b/src/test/java/org/prebid/server/deals/lineitem/DeliveryProgressTest.java new file mode 100644 index 00000000000..bc03de3aa36 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/lineitem/DeliveryProgressTest.java @@ -0,0 +1,365 @@ +package org.prebid.server.deals.lineitem; + +import org.assertj.core.groups.Tuple; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.LineItemService; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.deals.proto.DeliverySchedule; +import org.prebid.server.deals.proto.Token; +import org.prebid.server.deals.proto.report.Event; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +public class DeliveryProgressTest extends VertxTest { + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock + private Clock clock; + + @Mock + private LineItemService lineItemService; + + private ZonedDateTime now; + + private DeliveryProgress deliveryProgress; + + @Before + public void setUp() { + now = ZonedDateTime.now(Clock.fixed(Instant.parse("2019-07-26T10:00:00Z"), ZoneOffset.UTC)); + + given(clock.instant()).willReturn(now.toInstant()); + given(clock.getZone()).willReturn(ZoneOffset.UTC); + deliveryProgress = DeliveryProgress.of(now, lineItemService); + } + + @Test + public void cleanLineItemStatusesShouldRemoveOldestCachedPlans() { + // given + final TxnLog txnLog1 = TxnLog.create(); + txnLog1.lineItemSentToClientAsTopMatch().add("lineItemId1"); + + final TxnLog txnLog2 = TxnLog.create(); + txnLog2.lineItemSentToClientAsTopMatch().add("lineItemId1"); + + final TxnLog txnLog3 = TxnLog.create(); + txnLog3.lineItemSentToClientAsTopMatch().add("lineItemId1"); + + final LineItem lineItem1 = Mockito.mock(LineItem.class); + final DeliveryPlan deliveryPlan1 = Mockito.mock(DeliveryPlan.class); + final DeliveryPlan deliveryPlan2 = Mockito.mock(DeliveryPlan.class); + final DeliveryPlan deliveryPlan3 = Mockito.mock(DeliveryPlan.class); + + given(deliveryPlan1.getPlanId()).willReturn("lineItemId1Plan"); + given(deliveryPlan2.getPlanId()).willReturn("lineItemId2Plan"); + given(deliveryPlan3.getPlanId()).willReturn("lineItemId3Plan"); + + given(deliveryPlan1.withoutSpentTokens()).willReturn(deliveryPlan1); + given(deliveryPlan2.withoutSpentTokens()).willReturn(deliveryPlan2); + given(deliveryPlan3.withoutSpentTokens()).willReturn(deliveryPlan3); + + given(deliveryPlan1.getEndTimeStamp()).willReturn(now.minusMinutes(1)); + given(deliveryPlan2.getEndTimeStamp()).willReturn(now.minusMinutes(2)); + given(deliveryPlan3.getEndTimeStamp()).willReturn(now.minusMinutes(3)); + + given(lineItem1.getEndTimeStamp()).willReturn(now.minusMinutes(1)); + given(lineItem1.getActiveDeliveryPlan()).willReturn(deliveryPlan1, deliveryPlan2, deliveryPlan3); + + given(lineItemService.getLineItemById(eq("lineItemId1"))).willReturn(lineItem1); + + // when and then + deliveryProgress.recordTransactionLog(txnLog1, singletonMap("lineItemId1Plan", 1), "1001"); + deliveryProgress.recordTransactionLog(txnLog2, singletonMap("lineItemId2Plan", 1), "1001"); + deliveryProgress.recordTransactionLog(txnLog3, singletonMap("lineItemId3Plan", 1), "1001"); + + // check that 3 lineItemStatuses + assertThat(deliveryProgress.getLineItemStatuses().get("lineItemId1").getDeliveryPlans()) + .hasSize(3) + .extracting(DeliveryPlan::getPlanId) + .containsOnly("lineItemId1Plan", "lineItemId2Plan", "lineItemId3Plan"); + + deliveryProgress.cleanLineItemStatuses(now, 10000000L, 1); + + assertThat(deliveryProgress.getLineItemStatuses().get("lineItemId1").getDeliveryPlans()) + .hasSize(1) + .extracting(DeliveryPlan::getPlanId) + .containsOnly("lineItemId1Plan"); + } + + @Test + public void cleanLineItemStatusesShouldRemoveExpiredLineItemStatuses() { + // given + final TxnLog txnLog = TxnLog.create(); + txnLog.lineItemsSentToClient().add("lineItemId1"); + txnLog.lineItemsSentToClient().add("lineItemId2"); + final Map planIdToTokenPriority = new HashMap<>(); + planIdToTokenPriority.put("lineItemId1Plan", 1); + planIdToTokenPriority.put("lineItemId2Plan", 1); + + final LineItem lineItem1 = Mockito.mock(LineItem.class); + final LineItem lineItem2 = Mockito.mock(LineItem.class); + + given(lineItem1.getEndTimeStamp()).willReturn(now.minusHours(1)); + given(lineItem2.getEndTimeStamp()).willReturn(now.minusHours(2)); + + given(lineItemService.getLineItemById(eq("lineItemId1"))).willReturn(lineItem1); + given(lineItemService.getLineItemById(eq("lineItemId2"))).willReturn(lineItem2); + + // when and then + deliveryProgress.recordTransactionLog(txnLog, planIdToTokenPriority, "1001"); + + // check that 2 lineItemStatuses + assertThat(deliveryProgress.getLineItemStatuses().keySet()).hasSize(2) + .containsOnly("lineItemId1", "lineItemId2"); + + deliveryProgress.cleanLineItemStatuses(now, 6000000, 5); + + assertThat(deliveryProgress.getLineItemStatuses().keySet()).hasSize(1) + .containsOnly("lineItemId1"); + } + + @Test + public void fromAnotherCopyingPlansShouldReturnDeliveryProgressWithPlansAndStatistics() { + // given + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now, lineItemService); + final TxnLog txnLog = TxnLog.create(); + txnLog.lineItemsMatchedWholeTargeting().add("lineItemId1"); + txnLog.lineItemsMatchedDomainTargeting().add("lineItemId1"); + txnLog.lineItemsReadyToServe().add("lineItemId1"); + txnLog.lineItemsMatchedTargetingFcapped().add("lineItemId1"); + txnLog.lineItemsMatchedTargetingFcapLookupFailed().add("lineItemId1"); + txnLog.lineItemsPacingDeferred().add("lineItemId1"); + txnLog.lineItemsSentToBidder().get("bidder1").add("lineItemId1"); + txnLog.lineItemsSentToBidderAsTopMatch().put("bidder1", singleton("lineItemId1")); + txnLog.lineItemsReceivedFromBidder().get("bidder1").add("lineItemId1"); + txnLog.lineItemsSentToClient().add("lineItemId1"); + txnLog.lineItemSentToClientAsTopMatch().add("lineItemId1"); + txnLog.lostAuctionToLineItems().put("lineItemId1", singleton("lineItemId2")); + txnLog.lostMatchingToLineItems().put("lineItemId1", singleton("lineItemId3")); + + final LineItem lineItem = mock(LineItem.class); + given(lineItemService.getLineItemById("lineItemId1")).willReturn(lineItem); + given(lineItem.getLineItemId()).willReturn("lineItemId1"); + given(lineItem.getActiveDeliveryPlan()).willReturn(DeliveryPlan.of(givenDeliverySchedule(now, "planId1"))); + + deliveryProgress.recordTransactionLog(txnLog, singletonMap("planId1", 1), "1001"); + + // when + final DeliveryProgress copiedDeliveryProgress = deliveryProgress.copyWithOriginalPlans(); + + // then + final LineItemStatus lineItemStatusCopied = copiedDeliveryProgress.getLineItemStatuses().get("lineItemId1"); + final LineItemStatus lineItemStatusOriginal = deliveryProgress.getLineItemStatuses().get("lineItemId1"); + + assertThat(lineItemStatusCopied).isNotSameAs(lineItemStatusOriginal); + + assertThat(lineItemStatusCopied.getDomainMatched().sum()) + .isEqualTo(lineItemStatusOriginal.getDomainMatched().sum()); + + assertThat(lineItemStatusCopied.getTargetMatched().sum()) + .isEqualTo(lineItemStatusOriginal.getTargetMatched().sum()); + + assertThat(lineItemStatusCopied.getTargetMatchedButFcapped().sum()) + .isEqualTo(lineItemStatusOriginal.getTargetMatchedButFcapped().sum()); + + assertThat(lineItemStatusCopied.getTargetMatchedButFcapLookupFailed().sum()) + .isEqualTo(lineItemStatusOriginal.getTargetMatchedButFcapLookupFailed().sum()); + + assertThat(lineItemStatusCopied.getPacingDeferred().sum()) + .isEqualTo(lineItemStatusOriginal.getPacingDeferred().sum()); + + assertThat(lineItemStatusCopied.getSentToBidder().sum()) + .isEqualTo(lineItemStatusOriginal.getSentToBidder().sum()); + + assertThat(lineItemStatusCopied.getSentToBidderAsTopMatch().sum()) + .isEqualTo(lineItemStatusOriginal.getSentToBidderAsTopMatch().sum()); + + assertThat(lineItemStatusCopied.getReceivedFromBidder().sum()) + .isEqualTo(lineItemStatusOriginal.getReceivedFromBidder().sum()); + + assertThat(lineItemStatusCopied.getReceivedFromBidderInvalidated().sum()) + .isEqualTo(lineItemStatusOriginal.getReceivedFromBidderInvalidated().sum()); + + assertThat(lineItemStatusCopied.getSentToClient().sum()) + .isEqualTo(lineItemStatusOriginal.getSentToClient().sum()); + + assertThat(lineItemStatusCopied.getSentToClientAsTopMatch().sum()) + .isEqualTo(lineItemStatusOriginal.getSentToClientAsTopMatch().sum()); + + assertThat(lineItemStatusCopied.getSentToClientAsTopMatch().sum()) + .isEqualTo(lineItemStatusOriginal.getSentToClientAsTopMatch().sum()); + + assertThat(copiedDeliveryProgress.getLineItemIdToLost().get("lineItemId1").entrySet()) + .extracting(Map.Entry::getKey, entry -> entry.getValue().getCount().sum()) + .containsOnly(Tuple.tuple("lineItemId2", 1L), Tuple.tuple("lineItemId3", 1L)); + + final DeliveryPlan originDeliveryPlan = lineItemStatusOriginal.getDeliveryPlans().stream().findFirst().get(); + final DeliveryPlan copiedDeliveryPlan = lineItemStatusCopied.getDeliveryPlans().stream().findFirst().get(); + assertThat(originDeliveryPlan).isSameAs(copiedDeliveryPlan); + } + + @Test + public void recordWinEventShouldRecordEventForAbsentInReportLineItemStatus() { + // given + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now, lineItemService); + + // when + deliveryProgress.recordWinEvent("lineItemId1"); + + // then + final LineItemStatus lineItemStatus = deliveryProgress.getLineItemStatuses().get("lineItemId1"); + assertThat(lineItemStatus.getEvents()) + .extracting(Event::getType, event -> event.getCount().sum()) + .containsOnly(Tuple.tuple("win", 1L)); + } + + @Test + public void upsertPlanReferenceFromLineItemShouldInsertReferenceToNotExistingLineItemStatus() { + // given + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now, lineItemService); + final LineItem lineItem = mock(LineItem.class); + given(lineItem.getActiveDeliveryPlan()).willReturn(DeliveryPlan.of(givenDeliverySchedule(now, "planId1"))); + given(lineItem.getLineItemId()).willReturn("lineItemId1"); + + // when + deliveryProgress.upsertPlanReferenceFromLineItem(lineItem); + + // then + assertThat(deliveryProgress.getLineItemStatuses()).hasSize(1); + final Set deliveryPlans = deliveryProgress.getLineItemStatuses().get("lineItemId1") + .getDeliveryPlans(); + assertThat(deliveryPlans).hasSize(1).extracting(DeliveryPlan::getDeliverySchedule) + .containsOnly(givenDeliverySchedule(now, "planId1")); + } + + @Test + public void upsertPlanReferenceFromLineItemShouldInsertToExistingWhenNotContainsPlanWithSameId() { + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now, lineItemService); + deliveryProgress.getLineItemStatuses().put("lineItemId1", LineItemStatus.of("lineItemId1")); + final LineItem lineItem = mock(LineItem.class); + given(lineItem.getActiveDeliveryPlan()).willReturn(DeliveryPlan.of(givenDeliverySchedule(now, "planId1"))); + given(lineItem.getLineItemId()).willReturn("lineItemId1"); + + // when + deliveryProgress.upsertPlanReferenceFromLineItem(lineItem); + + // then + assertThat(deliveryProgress.getLineItemStatuses()).hasSize(1); + final Set deliveryPlans = deliveryProgress.getLineItemStatuses().get("lineItemId1") + .getDeliveryPlans(); + assertThat(deliveryPlans).hasSize(1).extracting(DeliveryPlan::getDeliverySchedule) + .containsOnly(givenDeliverySchedule(now, "planId1")); + } + + @Test + public void upsertPlanReferenceFromLineItemShouldReplacePlanWhenContainsPlanWithSameId() { + // given + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now, lineItemService); + final LineItemStatus lineItemStatus = LineItemStatus.of("lineItemId1"); + lineItemStatus.getDeliveryPlans().add(DeliveryPlan.of(givenDeliverySchedule(now.minusMinutes(1), "planId1"))); + deliveryProgress.getLineItemStatuses().put("lineItemId1", lineItemStatus); + final LineItem lineItem = mock(LineItem.class); + given(lineItem.getActiveDeliveryPlan()).willReturn(DeliveryPlan.of(givenDeliverySchedule(now, "planId1"))); + given(lineItem.getLineItemId()).willReturn("lineItemId1"); + + // when + deliveryProgress.upsertPlanReferenceFromLineItem(lineItem); + + // then + assertThat(deliveryProgress.getLineItemStatuses()).hasSize(1); + final Set deliveryPlans = deliveryProgress.getLineItemStatuses().get("lineItemId1") + .getDeliveryPlans(); + assertThat(deliveryPlans).hasSize(1).extracting(DeliveryPlan::getDeliverySchedule) + .containsOnly(givenDeliverySchedule(now, "planId1")); + } + + @Test + public void mergePlanFromLineItemShouldMergeCurrentPlanWithNewActiveOne() { + // given + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now, lineItemService); + final LineItemStatus lineItemStatus = LineItemStatus.of("lineItemId1"); + lineItemStatus.getDeliveryPlans().add(DeliveryPlan.of(DeliverySchedule.builder().planId("planId1") + .startTimeStamp(now.minusMinutes(1)).endTimeStamp(now.plusMinutes(1)) + .updatedTimeStamp(now.minusMinutes(2)) + .tokens(singleton(Token.of(2, 50))).build())); + deliveryProgress.getLineItemStatuses().put("lineItemId1", lineItemStatus); + final LineItem lineItem = mock(LineItem.class); + given(lineItem.getActiveDeliveryPlan()).willReturn(DeliveryPlan.of(givenDeliverySchedule(now, "planId1"))); + given(lineItem.getLineItemId()).willReturn("lineItemId1"); + + // when + deliveryProgress.mergePlanFromLineItem(lineItem); + + // then + assertThat(deliveryProgress.getLineItemStatuses()).hasSize(1); + final Set deliveryPlans = deliveryProgress.getLineItemStatuses().get("lineItemId1") + .getDeliveryPlans(); + assertThat(deliveryPlans).hasSize(1) + .flatExtracting(DeliveryPlan::getDeliveryTokens) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, + deliveryToken -> deliveryToken.getSpent().sum()) + .containsOnly(Tuple.tuple(1, 100, 0L), Tuple.tuple(2, 50, 0L)); + + assertThat(deliveryPlans).hasSize(1) + .extracting(DeliveryPlan::getPlanId, DeliveryPlan::getUpdatedTimeStamp) + .containsOnly(Tuple.tuple("planId1", now.minusMinutes(1))); + } + + @Test + public void mergePlanFromLineItemShouldReplaceCurrentPlanWithNewActiveOneWithoutSpentTokens() { + // given + final DeliveryProgress deliveryProgress = DeliveryProgress.of(now, lineItemService); + deliveryProgress.getLineItemStatuses().put("lineItemId1", LineItemStatus.of("lineItemId1")); + final LineItem lineItem = mock(LineItem.class); + given(lineItem.getActiveDeliveryPlan()).willReturn(DeliveryPlan.of(givenDeliverySchedule(now, "planId1"))); + given(lineItem.getLineItemId()).willReturn("lineItemId1"); + + // when + deliveryProgress.mergePlanFromLineItem(lineItem); + + // then + assertThat(deliveryProgress.getLineItemStatuses()).hasSize(1); + final Set deliveryPlans = deliveryProgress.getLineItemStatuses().get("lineItemId1") + .getDeliveryPlans(); + assertThat(deliveryPlans).hasSize(1) + .flatExtracting(DeliveryPlan::getDeliveryTokens) + .extracting(DeliveryToken::getPriorityClass, DeliveryToken::getTotal, + deliveryToken -> deliveryToken.getSpent().sum()) + .containsOnly(Tuple.tuple(1, 100, 0L)); + + assertThat(deliveryPlans).hasSize(1) + .extracting(DeliveryPlan::getPlanId, DeliveryPlan::getUpdatedTimeStamp) + .containsOnly(Tuple.tuple("planId1", now.minusMinutes(1))); + } + + private static DeliverySchedule givenDeliverySchedule(ZonedDateTime now, String planId) { + return DeliverySchedule.builder() + .planId(planId) + .startTimeStamp(now.minusMinutes(1)) + .endTimeStamp(now.plusMinutes(1)) + .updatedTimeStamp(now.minusMinutes(1)) + .tokens(singleton(Token.of(1, 100))) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/deals/model/DeepDebugLogTest.java b/src/test/java/org/prebid/server/deals/model/DeepDebugLogTest.java new file mode 100644 index 00000000000..0e70f86ca56 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/model/DeepDebugLogTest.java @@ -0,0 +1,51 @@ +package org.prebid.server.deals.model; + +import org.junit.Test; +import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal; +import org.prebid.server.proto.openrtb.ext.response.ExtTraceDeal.Category; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.function.Supplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class DeepDebugLogTest { + + private final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + + @SuppressWarnings("unchecked") + @Test + public void addShouldNotRecordMessageWhenDeepDebugIsDisabled() { + // given + final DeepDebugLog deepDebugLog = DeepDebugLog.create(false, clock); + + final Supplier messageSupplier = (Supplier) mock(Supplier.class); + + // when + deepDebugLog.add(null, Category.pacing, messageSupplier); + + // then + verify(messageSupplier, never()).get(); + + assertThat(deepDebugLog.entries()).isEmpty(); + } + + @Test + public void addShouldRecordMessageWhenDeepDebugIsEnabled() { + // given + final DeepDebugLog deepDebugLog = DeepDebugLog.create(true, clock); + + // when + deepDebugLog.add(null, Category.pacing, () -> "debug message 1"); + + // then + assertThat(deepDebugLog.entries()).containsOnly( + ExtTraceDeal.of(null, ZonedDateTime.now(clock), Category.pacing, "debug message 1")); + } +} diff --git a/src/test/java/org/prebid/server/deals/simulation/DealsSimulationAdminHandlerTest.java b/src/test/java/org/prebid/server/deals/simulation/DealsSimulationAdminHandlerTest.java new file mode 100644 index 00000000000..1ac3d1b8660 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/simulation/DealsSimulationAdminHandlerTest.java @@ -0,0 +1,226 @@ +package org.prebid.server.deals.simulation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.exception.PreBidException; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +public class DealsSimulationAdminHandlerTest extends VertxTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private SimulationAwareRegisterService registerService; + + @Mock + private SimulationAwarePlannerService plannerService; + + @Mock + private SimulationAwareDeliveryProgressService deliveryProgressService; + + @Mock + private SimulationAwareDeliveryStatsService deliveryStatsService; + + @Mock + private SimulationAwareHttpBidderRequester httpBidderRequester; + + @Mock + private RoutingContext routingContext; + + @Mock + private HttpServerRequest request; + + @Mock + private HttpServerResponse response; + + private ZonedDateTime now = ZonedDateTime.now(Clock.fixed(Instant.parse("2019-10-10T00:00:00Z"), ZoneOffset.UTC)); + + private DealsSimulationAdminHandler dealsSimulationAdminHandler; + + @Before + public void setUp() { + dealsSimulationAdminHandler = new DealsSimulationAdminHandler(registerService, + plannerService, deliveryProgressService, deliveryStatsService, httpBidderRequester, jacksonMapper, + "endpoint"); + given(routingContext.request()).willReturn(request); + given(routingContext.response()).willReturn(response); + given(request.headers()).willReturn(MultiMap.caseInsensitiveMultiMap() + .add("pg-sim-timestamp", now.toString())); + given(response.setStatusCode(anyInt())).willReturn(response); + } + + @Test + public void handleShouldCallPerformRegistrationHandler() { + // given + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/planner/register"); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(registerService).performRegistration(eq(now)); + } + + @Test + public void handleShouldCallFetchLineItemHandler() { + // given + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/planner/fetchLineItems"); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(plannerService).initiateLineItemsFetching(eq(now)); + } + + @Test + public void handleShouldCallAdvancePlanHandler() { + // given + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/advancePlans"); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(plannerService).advancePlans(eq(now)); + } + + @Test + public void handleShouldCallReportHandler() { + // given + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/dealstats/report"); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(deliveryProgressService).createDeliveryProgressReport(eq(now)); + verify(deliveryStatsService).sendDeliveryProgressReports(eq(now)); + } + + @Test + public void handleShouldCallSetBidRateHandler() throws JsonProcessingException { + // given + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/bidRate"); + given(routingContext.getBody()).willReturn(Buffer.buffer( + mapper.writeValueAsString(Collections.singletonMap("lineItemId", 1.00)))); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(httpBidderRequester).setBidRates(any()); + } + + @Test + public void handleShouldRespondWithErrorWhenBidderRequesterIsNotSet() { + // given + dealsSimulationAdminHandler = new DealsSimulationAdminHandler( + registerService, plannerService, deliveryProgressService, deliveryStatsService, null, jacksonMapper, + "endpoint"); + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/bidRate"); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(response).setStatusCode(400); + verify(response).end(eq("Calling /bidRate is not make sense since " + + "Prebid Server configured to use real bidder exchanges in simulation mode")); + } + + @Test + public void handleShouldRespondWithNotFoundWhenHandlerNotFound() { + // given + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/invalid"); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(response).setStatusCode(404); + verify(response).end(eq("Requested url /invalid was not found")); + } + + @Test + public void handleShouldRespondWithBadRequestWhenRequiredDateHeaderNotFound() { + // given + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/dealstats/startreport"); + given(request.headers()).willReturn(MultiMap.caseInsensitiveMultiMap()); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(response).setStatusCode(400); + verify(response).end(eq("pg-sim-timestamp with simulated current date is required for endpoints:" + + " /planner/register, /planner/fetchLineItems, /advancePlans, /dealstats/report")); + } + + @Test + public void handleShouldRespondWithBadRequestWhenBodyNotFoundForSetBidRatesHandler() { + // given + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/bidRate"); + given(routingContext.getBody()).willReturn(null); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(response).setStatusCode(400); + verify(response).end(eq("Body is required for /bidRate endpoint")); + } + + @Test + public void handleShouldRespondWithBadRequestWhenBodyHasIncorrectFormatForSetBidRatesHandler() { + // given + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/bidRate"); + given(routingContext.getBody()).willReturn(Buffer.buffer("{")); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(response).setStatusCode(400); + verify(response).end(startsWith("Failed to parse bid rates body")); + } + + @Test + public void handleShouldRespondWithInternalServerErrorStatus() { + // given + given(request.uri()).willReturn("/pbs-admin/e2eAdmin/advancePlans"); + doThrow(new PreBidException("Error")).when(plannerService).advancePlans(any()); + + // when + dealsSimulationAdminHandler.handle(routingContext); + + // then + verify(response).setStatusCode(eq(500)); + verify(response).end(eq("Error")); + } +} diff --git a/src/test/java/org/prebid/server/deals/simulation/SimulationAwareHttpBidderRequesterTest.java b/src/test/java/org/prebid/server/deals/simulation/SimulationAwareHttpBidderRequesterTest.java new file mode 100644 index 00000000000..efe2652cd15 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/simulation/SimulationAwareHttpBidderRequesterTest.java @@ -0,0 +1,267 @@ +package org.prebid.server.deals.simulation; + +import com.fasterxml.jackson.databind.node.IntNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Pmp; +import com.iab.openrtb.response.Bid; +import io.vertx.core.Future; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.BidderErrorNotifier; +import org.prebid.server.bidder.BidderRequestCompletionTrackerFactory; +import org.prebid.server.bidder.HttpBidderRequestEnricher; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.deals.LineItemService; +import org.prebid.server.deals.lineitem.LineItem; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.deals.proto.Price; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.proto.openrtb.ext.request.ExtDeal; +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.vertx.http.HttpClient; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; + +public class SimulationAwareHttpBidderRequesterTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + private SimulationAwareHttpBidderRequester bidderRequester; + + @Mock + private HttpClient httpClient; + @Mock + private BidderRequestCompletionTrackerFactory bidderRequestCompletionTrackerFactory; + @Mock + private BidderErrorNotifier bidderErrorNotifier; + @Mock + private HttpBidderRequestEnricher requestEnricher; + @Mock + private CaseInsensitiveMultiMap requestHeaders; + @Mock + private LineItemService lineItemService; + + @Before + public void setUp() { + bidderRequester = new SimulationAwareHttpBidderRequester( + httpClient, bidderRequestCompletionTrackerFactory, bidderErrorNotifier, requestEnricher, + lineItemService, jacksonMapper); + } + + @Test + public void requestBidsShouldReturnBidderSeatBidWithOneBidAndFilterOutOne() { + // given + final Map rates = new HashMap<>(); + rates.put("lineItemId1", 1.00); + rates.put("lineItemId2", 0.00); + bidderRequester.setBidRates(rates); + given(lineItemService.getLineItemById(eq("lineItemId1"))).willReturn(LineItem.of( + LineItemMetaData.builder().price(Price.of(BigDecimal.ONE, "USD")).build(), Price.of(BigDecimal.ONE, + "USD"), null, null)); + given(lineItemService.getLineItemById(eq("lineItemId2"))).willReturn(LineItem.of( + LineItemMetaData.builder().price(Price.of(BigDecimal.TEN, "USD")).build(), Price.of(BigDecimal.ONE, + "USD"), null, null)); + + final BidRequest bidRequest = BidRequest.builder().imp(asList( + Imp.builder().id("impId1").pmp(Pmp.builder().deals(singletonList(Deal.builder() + .id("dealId1") + .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of("lineItemId1", null, + singletonList(Format.builder().w(100).h(100).build()), null)))) + .build())).build()).build(), + Imp.builder().id("impId2").pmp(Pmp.builder().deals(singletonList(Deal.builder() + .id("dealId2") + .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of("lineItemId2", null, + singletonList(Format.builder().w(100).h(100).build()), null)))) + .build())).build()).build())) + .build(); + final BidderRequest bidderRequest = BidderRequest.of("bidder", null, bidRequest); + + // when + final Future result = bidderRequester.requestBids(null, bidderRequest, null, requestHeaders, + false); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isEqualTo(BidderSeatBid.of(singletonList(BidderBid.of( + Bid.builder().id("impId1-lineItemId1").impid("impId1").dealid("dealId1").price(BigDecimal.ONE) + .adm("").crid("crid").w(100).h(100) + .build(), BidType.banner, "USD")), Collections.emptyList(), Collections.emptyList())); + } + + @Test + public void requestBidsShouldFilterBidderSeatBidForWhichImpPmpIsNull() { + // given + + bidderRequester.setBidRates(Collections.singletonMap("lineItemId1", 1.00)); + given(lineItemService.getLineItemById(eq("lineItemId1"))).willReturn(LineItem.of( + LineItemMetaData.builder().price(Price.of(BigDecimal.ONE, "USD")).build(), + Price.of(BigDecimal.ONE, "USD"), null, null)); + + final BidRequest bidRequest = BidRequest.builder().imp(asList( + Imp.builder().id("impId1").pmp(Pmp.builder().deals(singletonList(Deal.builder() + .id("dealId1") + .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of("lineItemId1", null, + singletonList(Format.builder().w(100).h(100).build()), null)))) + .build())).build()).build(), + Imp.builder().id("impId2").build())) + .build(); + final BidderRequest bidderRequest = BidderRequest.of("bidder", null, bidRequest); + + // when + final Future result = bidderRequester.requestBids(null, bidderRequest, null, requestHeaders, + false); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isEqualTo(BidderSeatBid.of(singletonList(BidderBid.of( + Bid.builder().id("impId1-lineItemId1").impid("impId1").dealid("dealId1").price(BigDecimal.ONE) + .adm("").crid("crid").w(100).h(100) + .build(), BidType.banner, "USD")), Collections.emptyList(), Collections.emptyList())); + } + + @Test + public void requestBidsShouldSetSizesAsZeroIfExtDealsLinesDoesNotHaveSizes() { + // given + final Map rates = new HashMap<>(); + rates.put("lineItemId1", 1.00); + bidderRequester.setBidRates(rates); + given(lineItemService.getLineItemById(eq("lineItemId1"))).willReturn(LineItem.of( + LineItemMetaData.builder().price(Price.of(BigDecimal.ONE, "USD")).build(), + Price.of(BigDecimal.ONE, "USD"), null, null)); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList( + Imp.builder().id("impId1").pmp(Pmp.builder().deals(singletonList(Deal.builder() + .id("dealId1") + .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of("lineItemId1", null, null, null)))) + .build())).build()).build())) + .build(); + final BidderRequest bidderRequest = BidderRequest.of("bidder", null, bidRequest); + + // when + final Future result = bidderRequester.requestBids(null, bidderRequest, null, requestHeaders, + false); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isEqualTo(BidderSeatBid.of(singletonList(BidderBid.of( + Bid.builder().id("impId1-lineItemId1").impid("impId1").dealid("dealId1").price(BigDecimal.ONE) + .adm("").crid("crid").w(0).h(0) + .build(), BidType.banner, "USD")), Collections.emptyList(), Collections.emptyList())); + } + + @Test + public void requestBidsShouldThrowPrebidExceptionWhenExtDealsInvalidFormat() { + // given + final Map rates = new HashMap<>(); + rates.put("lineItemId1", 1.00); + bidderRequester.setBidRates(rates); + given(lineItemService.getLineItemById(eq("lineItemId1"))).willReturn(LineItem.of( + LineItemMetaData.builder().price(Price.of(BigDecimal.ONE, "USD")).build(), + Price.of(BigDecimal.ONE, "USD"), null, null)); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList( + Imp.builder().id("impId1").pmp(Pmp.builder().deals(singletonList(Deal.builder() + .id("dealId1") + .ext(mapper.createObjectNode().set("line", new IntNode(5))) + .build())).build()).build())) + .build(); + final BidderRequest bidderRequest = BidderRequest.of("bidder", null, bidRequest); + + // when and then + assertThatThrownBy(() -> bidderRequester.requestBids(null, bidderRequest, null, requestHeaders, false)) + .isInstanceOf(PreBidException.class) + .hasMessageStartingWith("Error decoding bidRequest.imp.pmp.deal.ext:"); + } + + @Test + public void requestBidsShouldReturnBidderSeatBidWithoutBidderBidsAndWithError() { + // given + bidderRequester.setBidRates(Collections.singletonMap("lineItemId1", 1.00)); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList( + Imp.builder().id("impId1").pmp(Pmp.builder().deals(singletonList(Deal.builder() + .id("dealId1").build())).build()).build())) + .build(); + final BidderRequest bidderRequest = BidderRequest.of("bidder", null, bidRequest); + + // when + final Future result = bidderRequester.requestBids(null, bidderRequest, null, requestHeaders, + false); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isEqualTo(BidderSeatBid.of(Collections.emptyList(), Collections.emptyList(), + singletonList(BidderError.failedToRequestBids( + "Matched or ready to serve line items were not found, but required in simulation mode")))); + } + + @Test + public void requestBidsShouldThrowPrebidExceptionWhenBidRatesForLineItemWereNotFound() { + // given + bidderRequester.setBidRates(Collections.singletonMap("lineItemId1", 1.00)); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList( + Imp.builder().id("impId1").pmp(Pmp.builder().deals(singletonList(Deal.builder() + .id("dealId1") + .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of("lineItemId2", null, null, null)))) + .build())).build()).build())) + .build(); + final BidderRequest bidderRequest = BidderRequest.of("bidder", null, bidRequest); + + // when + assertThatThrownBy(() -> bidderRequester.requestBids(null, bidderRequest, null, requestHeaders, false)) + .isInstanceOf(PreBidException.class) + .hasMessage("Bid rate for line item with id lineItemId2 was not found"); + } + + @Test + public void setBidRatesShouldMergeRates() { + // given + final Map initialBidRates = new HashMap<>(); + initialBidRates.put("lineItemId1", 1.00); + initialBidRates.put("lineItemId2", 0.5); + bidderRequester.setBidRates(initialBidRates); + + final Map updateBidRates = new HashMap<>(); + updateBidRates.put("lineItemId1", 0.00); + updateBidRates.put("lineItemId3", 0.75); + + // when + bidderRequester.setBidRates(updateBidRates); + + // then + @SuppressWarnings("unchecked") final Map resultBidRates = + (Map) ReflectionTestUtils.getField(bidderRequester, "bidRates"); + + assertThat(resultBidRates).isNotNull(); + assertThat(resultBidRates.entrySet()).hasSize(3) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly(tuple("lineItemId1", 0.00), tuple("lineItemId2", 0.5), tuple("lineItemId3", 0.75)); + } +} diff --git a/src/test/java/org/prebid/server/deals/simulation/SimulationAwarePlannerServiceTest.java b/src/test/java/org/prebid/server/deals/simulation/SimulationAwarePlannerServiceTest.java new file mode 100644 index 00000000000..844baf28742 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/simulation/SimulationAwarePlannerServiceTest.java @@ -0,0 +1,164 @@ +package org.prebid.server.deals.simulation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.Future; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.AlertHttpService; +import org.prebid.server.deals.DeliveryProgressService; +import org.prebid.server.deals.model.DeploymentProperties; +import org.prebid.server.deals.model.PlannerProperties; +import org.prebid.server.deals.proto.LineItemMetaData; +import org.prebid.server.metric.Metrics; +import org.prebid.server.vertx.http.HttpClient; +import org.prebid.server.vertx.http.model.HttpClientResponse; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class SimulationAwarePlannerServiceTest extends VertxTest { + + private static final String PLAN_ENDPOINT = "plan-endpoint"; + private static final String REGISTER_ENDPOINT = "register-endpoint"; + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + private static final String PBS_HOST = "pbs-host"; + private static final String PBS_REGION = "pbs-region"; + private static final String PBS_VENDOR = "pbs-vendor"; + + @Rule + public final MockitoRule rule = MockitoJUnit.rule(); + + @Mock + private HttpClient httpClient; + @Mock + private SimulationAwareLineItemService lineItemService; + @Mock + private DeliveryProgressService deliveryProgressService; + @Mock + private AlertHttpService alertHttpService; + @Mock + private Metrics metrics; + + private SimulationAwarePlannerService simulationAwarePlannerService; + + private ZonedDateTime now; + + @Before + public void setUp() { + Clock clock = Clock.fixed(Instant.parse("2019-07-26T10:00:00Z"), ZoneOffset.UTC); + now = ZonedDateTime.now(clock); + + simulationAwarePlannerService = new SimulationAwarePlannerService( + PlannerProperties.builder() + .planEndpoint(PLAN_ENDPOINT) + .registerEndpoint(REGISTER_ENDPOINT) + .timeoutMs(100L) + .registerPeriodSeconds(60L) + .username(USERNAME) + .password(PASSWORD) + .build(), + DeploymentProperties.builder().pbsHostId(PBS_HOST).pbsRegion(PBS_REGION).pbsVendor(PBS_VENDOR).build(), + lineItemService, + deliveryProgressService, + alertHttpService, + httpClient, + metrics, + clock, + jacksonMapper); + } + + @Test + public void initiateLineItemFetchingShouldNotCallLineItemServiceUpdateMethod() throws JsonProcessingException { + // given + given(httpClient.get(anyString(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, mapper.writeValueAsString( + Collections.singletonList(LineItemMetaData.builder().lineItemId("id").build()))))); + + // when + simulationAwarePlannerService.initiateLineItemsFetching(now); + + // then + verify(lineItemService, never()).updateLineItems(any(), anyBoolean()); + } + + @Test + public void initiateLineItemFetchingShouldNotRetryWhenCallToPlannerFailed() { + // given + given(httpClient.get(anyString(), any(), anyLong())) + .willReturn(Future.failedFuture(new TimeoutException("time out"))); + + // when + simulationAwarePlannerService.initiateLineItemsFetching(now); + + // then + verify(httpClient).get(anyString(), any(), anyLong()); + } + + @Test + public void initiateLineItemFetchingShouldUpdateMetricsAndLineItemServiceWithResponsiveFlagWhenCallSuccessful() + throws JsonProcessingException { + // given + given(httpClient.get(anyString(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, mapper.writeValueAsString( + Collections.singletonList(LineItemMetaData.builder().lineItemId("id").build()))))); + + // when + simulationAwarePlannerService.initiateLineItemsFetching(now); + + // then + @SuppressWarnings("unchecked") + List lineItemMetaData = (List) ReflectionTestUtils + .getField(simulationAwarePlannerService, "lineItemMetaData"); + assertThat(lineItemMetaData).hasSize(1) + .containsOnly(LineItemMetaData.builder().lineItemId("id").build()); + verify(metrics).updatePlannerRequestMetric(eq(true)); + verify(lineItemService).updateIsPlannerResponsive(eq(true)); + } + + @Test + public void initiateLineItemFetchingShouldUpdateMetricsAndLineItemServiceWithResponsiveFlagWhenCallFailed() { + // given + given(httpClient.get(anyString(), any(), anyLong())) + .willReturn(Future.failedFuture(new TimeoutException("time out"))); + + // when + simulationAwarePlannerService.initiateLineItemsFetching(now); + + // then + verify(metrics).updatePlannerRequestMetric(eq(false)); + verify(lineItemService).updateIsPlannerResponsive(eq(false)); + } + + @Test + public void advancePlansShouldCallUpdateLineItemsAndUpdateProgress() { + // given and when + simulationAwarePlannerService.advancePlans(now); + + // then + verify(lineItemService).updateLineItems(anyList(), anyBoolean(), eq(now)); + verify(lineItemService).advanceToNextPlan(now); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/RequestContextTest.java b/src/test/java/org/prebid/server/deals/targeting/RequestContextTest.java new file mode 100644 index 00000000000..66e826c309c --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/RequestContextTest.java @@ -0,0 +1,1132 @@ +package org.prebid.server.deals.targeting; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iab.openrtb.request.Video; +import org.junit.Before; +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.model.TxnLog; +import org.prebid.server.deals.targeting.model.GeoLocation; +import org.prebid.server.deals.targeting.model.Size; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; +import org.prebid.server.exception.TargetingSyntaxException; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtGeo; +import org.prebid.server.proto.openrtb.ext.request.ExtSite; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.function.Function; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.function.Function.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class RequestContextTest extends VertxTest { + + private TxnLog txnLog; + + @Before + public void setUp() { + txnLog = TxnLog.create(); + } + + @Test + public void lookupStringShouldReturnDomainFromSite() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.domain); + final RequestContext context = + new RequestContext( + request(r -> r.site(site(s -> s + .domain("anotherdomain.com") + .publisher(Publisher.builder().domain("domain.com").build())))), + imp(identity()), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("domain.com"); + } + + @Test + public void lookupStringShouldReturnDomainFromSitePublisher() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.domain); + final RequestContext context = + new RequestContext( + request(r -> r.site(site(s -> s.publisher(Publisher.builder().domain("domain.com").build())))), + imp(identity()), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("domain.com"); + } + + @Test + public void lookupStringShouldReturnNullWhenDomainIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.domain); + final RequestContext context = + new RequestContext(request(r -> r.site(site(identity()))), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnNullWhenSiteIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.domain); + final RequestContext context = + new RequestContext(request(identity()), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnReferrer() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.referrer); + final RequestContext context = + new RequestContext(request(r -> r.site(site(s -> s.page("http://domain.com/index")))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("http://domain.com/index"); + } + + @Test + public void lookupStringShouldReturnAppBundle() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.appBundle); + final RequestContext context = + new RequestContext(request(r -> r.app(app(a -> a.bundle("com.google.calendar")))), imp(identity()), + txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("com.google.calendar"); + } + + @Test + public void lookupStringShouldReturnNullWhenBundleIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.appBundle); + final RequestContext context = + new RequestContext(request(r -> r.app(app(identity()))), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnNullWhenAppIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.appBundle); + final RequestContext context = + new RequestContext(request(identity()), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnAdslotFromContextDataPbadslot() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.adslot); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(mapper.createObjectNode() + .set("context", mapper.createObjectNode() + .set("data", mapper.createObjectNode() + .put("pbadslot", "/123/456") + .set("adserver", obj("adslot", "/234/567")))) + .set("data", mapper.createObjectNode() + .put("pbadslot", "/345/678") + .set("adserver", obj("adslot", "/456/789"))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("/123/456"); + } + + @Test + public void lookupStringShouldReturnAdslotFromContextDataAdserverAdslot() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.adslot); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(mapper.createObjectNode() + .set("context", mapper.createObjectNode() + .set("data", mapper.createObjectNode() + .set("adserver", obj("adslot", "/234/567")))) + .set("data", mapper.createObjectNode() + .put("pbadslot", "/345/678") + .set("adserver", obj("adslot", "/456/789"))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("/234/567"); + } + + @Test + public void lookupStringShouldReturnAdslotFromDataPbadslot() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.adslot); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(mapper.createObjectNode() + .set("data", mapper.createObjectNode() + .put("pbadslot", "/345/678") + .set("adserver", obj("adslot", "/456/789"))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("/345/678"); + } + + @Test + public void lookupStringShouldReturnAdslotFromDataAdserverAdslot() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.adslot); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(mapper.createObjectNode() + .set("data", mapper.createObjectNode() + .set("adserver", obj("adslot", "/456/789"))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("/456/789"); + } + + @Test + public void lookupStringShouldReturnAdslotFromAlternativeAdServerAdSlotPath() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.adslot); + final RequestContext context = + new RequestContext(request(identity()), + imp(i -> i.ext(obj("context", obj("data", obj("adserver", obj("adslot", "/123/456")))))), + txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("/123/456"); + } + + @Test + public void lookupStringShouldReturnNullWhenAdslotIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.adslot); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("context", obj("data", mapper.createObjectNode())))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnCountryFromDeviceGeoExtValue() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.deviceGeoExt, + "vendor.attribute"); + final ExtGeo extGeo = ExtGeo.of(); + extGeo.addProperty("vendor", obj("attribute", "value")); + final RequestContext context = new RequestContext( + request(r -> r.device(device(d -> d.geo(geo(g -> g.ext(extGeo)))))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("value"); + } + + @Test + public void lookupStringShouldReturnRegionFromDeviceGeoExtValue() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.deviceGeoExt, + "vendor.nested.attribute"); + final ExtGeo extGeo = ExtGeo.of(); + extGeo.addProperty("vendor", obj("nested", obj("attribute", "value"))); + final RequestContext context = new RequestContext( + request(r -> r.device(device(d -> d.geo(geo(g -> g.ext(extGeo)))))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("value"); + } + + @Test + public void lookupStringShouldReturnMetroFromDeviceExtValue() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.deviceExt, + "vendor.attribute"); + final ExtDevice extDevice = ExtDevice.of(null, null); + extDevice.addProperty("vendor", obj("attribute", "value")); + + final RequestContext context = new RequestContext( + request(r -> r.device(device(d -> d.ext( + extDevice + )))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("value"); + } + + @Test + public void lookupStringShouldReturnMetroFromDeviceExtNestedValue() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.deviceExt, + "vendor.nested.attribute"); + final ExtDevice extDevice = ExtDevice.of(null, null); + extDevice.addProperty("vendor", obj("nested", obj("attribute", "value"))); + final RequestContext context = new RequestContext( + request(r -> r.device(device(d -> d.ext( + extDevice + )))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("value"); + } + + @Test + public void lookupStringShouldReturnSimpleBidderParam() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", obj("rubicon", obj("siteId", "123")))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("123"); + } + + @Test + public void lookupStringShouldReturnNestedBidderParam() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, + "rubicon.inv.code"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", obj("rubicon", obj("inv", obj("code", "123"))))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("123"); + } + + @Test + public void lookupStringShouldReturnNullWhenBidderParamIsNotString() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("rubicon", obj("siteId", mapper.valueToTree(123))))), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnNullWhenBidderParamIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("rubicon", "phony"))), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnNullWhenImpExtIsMissingForBidderParam() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnSimpleUserFirstPartyDataFromObject() { + // given + final TargetingCategory category = new TargetingCategory( + TargetingCategory.Type.userFirstPartyData, "buyeruid"); + final ExtUser extUser = ExtUser.builder().data(obj("buyeruid", "456")).build(); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u + .buyeruid("123") + .ext(extUser)))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("123"); + } + + @Test + public void lookupStringShouldReturnSimpleUserFirstPartyDataFromExt() { + // given + final TargetingCategory category = new TargetingCategory( + TargetingCategory.Type.userFirstPartyData, "sport"); + final ExtUser extUser = ExtUser.builder().data(obj("sport", "hockey")).build(); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.ext(extUser)))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("hockey"); + } + + @Test + public void lookupStringShouldReturnUserFirstPartyDataFromExtWhenObjectAttributeTypeIsNotString() { + // given + final TargetingCategory category = new TargetingCategory( + TargetingCategory.Type.userFirstPartyData, "yob"); + final ExtUser extUser = ExtUser.builder().data(obj("yob", "1900")).build(); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u + .yob(1800) + .ext(extUser)))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("1900"); + } + + @Test + public void lookupStringShouldReturnNestedUserFirstPartyData() { + // given + final TargetingCategory category = new TargetingCategory( + TargetingCategory.Type.userFirstPartyData, "section.sport"); + final ExtUser extUser = ExtUser.builder().data(obj("section", obj("sport", "hockey"))).build(); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.ext(extUser)))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("hockey"); + } + + @Test + public void lookupStringShouldReturnNullWhenUserFirstPartyDataIsNotString() { + // given + final TargetingCategory category = new TargetingCategory( + TargetingCategory.Type.userFirstPartyData, "sport"); + final ExtUser extUser = ExtUser.builder().data(obj("sport", mapper.valueToTree(123))).build(); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.ext(extUser)))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnNullWhenUserExtIsMissingForUserFirstPartyData() { + // given + final TargetingCategory category = new TargetingCategory( + TargetingCategory.Type.userFirstPartyData, "sport"); + final RequestContext context = new RequestContext( + request(r -> r.user(user(identity()))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnNullWhenUserIsMissingForUserFirstPartyData() { + // given + final TargetingCategory category = new TargetingCategory( + TargetingCategory.Type.userFirstPartyData, "sport"); + final RequestContext context = new RequestContext( + request(identity()), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isNull(); + } + + @Test + public void lookupStringShouldReturnSiteFirstPartyDataFromImpExt() { + // given + final TargetingCategory category = new TargetingCategory( + TargetingCategory.Type.siteFirstPartyData, "section.sport"); + final ExtSite extSite = ExtSite.of(null, obj("section", obj("sport", "basketball"))); + final ExtApp extApp = ExtApp.of(null, obj("section", obj("sport", "baseball"))); + final RequestContext context = new RequestContext( + request(r -> r + .site(site(s -> s.ext(extSite))) + .app(app(a -> a.ext(extApp)))), + imp(i -> i.ext(obj("context", obj("data", obj("section", obj("sport", "hockey")))))), + txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("hockey"); + } + + @Test + public void lookupStringShouldReturnSiteFirstPartyDataFromSiteExt() { + // given + final TargetingCategory category = new TargetingCategory( + TargetingCategory.Type.siteFirstPartyData, "section.sport"); + final ExtSite extSite = ExtSite.of(null, obj("section", obj("sport", "hockey"))); + final ExtApp extApp = ExtApp.of(null, obj("section", obj("sport", "baseball"))); + final RequestContext context = new RequestContext( + request(r -> r + .site(site(s -> s.ext(extSite))) + .app(app(a -> a.ext(extApp)))), + imp(identity()), + txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("hockey"); + } + + @Test + public void lookupStringShouldReturnSiteFirstPartyDataFromAppExt() { + // given + final TargetingCategory category = new TargetingCategory( + TargetingCategory.Type.siteFirstPartyData, "section.sport"); + final ExtApp extApp = ExtApp.of(null, obj("section", obj("sport", "hockey"))); + final RequestContext context = new RequestContext( + request(r -> r.app(app(a -> a.ext(extApp)))), + imp(identity()), + txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupString(category)).isEqualTo("hockey"); + } + + @Test + public void lookupStringShouldThrowExceptionWhenUnexpectedCategory() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.location); + final RequestContext context = new RequestContext(request(identity()), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThatThrownBy(() -> context.lookupString(category)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Unexpected category for fetching string value for: location"); + } + + @Test + public void lookupIntegerShouldReturnDowFromUserExt() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.dow); + final ExtUser extUser = ExtUser.builder().build(); + extUser.addProperty("time", obj("userdow", 5)); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.ext(extUser)))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupInteger(category)).isEqualTo(5); + } + + @Test + public void lookupIntegerShouldReturnHourFromExt() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.hour); + final ExtUser extUser = ExtUser.builder().build(); + extUser.addProperty("time", obj("userhour", 15)); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.ext(extUser)))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupInteger(category)).isEqualTo(15); + } + + @Test + public void lookupIntegerShouldReturnBidderParam() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", obj("rubicon", obj("siteId", mapper.valueToTree(123))))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupInteger(category)).isEqualTo(123); + } + + @Test + public void lookupIntegerShouldReturnNullWhenBidderParamIsNotInteger() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("rubicon", obj("siteId", mapper.valueToTree(123.456d))))), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupInteger(category)).isNull(); + } + + @Test + public void lookupIntegerShouldReturnNullWhenBidderParamIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("rubicon", "phony"))), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupInteger(category)).isNull(); + } + + @Test + public void lookupIntegerShouldReturnUserFirstPartyData() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.userFirstPartyData, "sport"); + final ExtUser extUser = ExtUser.builder().data(obj("sport", mapper.valueToTree(123))).build(); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.ext(extUser)))), + imp(identity()), + txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupInteger(category)).isEqualTo(123); + } + + @Test + public void lookupIntegerShouldReturnSiteFirstPartyData() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.siteFirstPartyData, "sport"); + final ExtSite extSite = ExtSite.of(null, obj("sport", mapper.valueToTree(123))); + final RequestContext context = new RequestContext( + request(r -> r.site(site(s -> s.ext(extSite)))), + imp(identity()), + txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupInteger(category)).isEqualTo(123); + } + + @Test + public void lookupIntegerShouldThrowExceptionWhenUnexpectedCategory() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.domain); + final RequestContext context = new RequestContext(request(identity()), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThatThrownBy(() -> context.lookupInteger(category)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Unexpected category for fetching integer value for: domain"); + } + + @Test + public void lookupStringsShouldReturnMediaTypeBannerAndVideo() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.mediaType); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.banner(banner(identity())).video(Video.builder().build())), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).containsOnly("banner", "video"); + } + + @Test + public void lookupStringsShouldReturnMediaTypeVideoAndNative() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.mediaType); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.video(Video.builder().build()).xNative(Native.builder().build())), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).containsOnly("video", "native"); + } + + @Test + public void lookupStringsShouldReturnBidderParam() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", + obj("rubicon", obj("siteId", mapper.valueToTree(asList("123", "456")))))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).containsOnly("123", "456"); + } + + @Test + public void lookupStringsShouldReturnEmptyListWhenBidderParamIsNotArray() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", obj("rubicon", obj("siteId", "phony")))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).isEmpty(); + } + + @Test + public void lookupStringsShouldReturnOnlyStringsWhenNonStringBidderParamPresent() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", + obj("rubicon", obj("siteId", mapper.valueToTree(asList("123", 456)))))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).containsOnly("123"); + } + + @Test + public void lookupStringsShouldReturnEmptyListWhenBidderParamIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", obj("prebid", obj("bidder", obj("rubicon", "phony"))))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).isEmpty(); + } + + @Test + public void lookupStringsShouldReturnUserFirstPartyData() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.userFirstPartyData, "sport"); + final ExtUser extUser = ExtUser.builder().data(obj("sport", mapper.valueToTree(asList("123", "456")))).build(); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.ext(extUser)))), + imp(identity()), + txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).containsOnly("123", "456"); + } + + @Test + public void lookupStringsShouldReturnSiteFirstPartyData() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.siteFirstPartyData, "sport"); + final ExtSite extSite = ExtSite.of(null, obj("sport", mapper.valueToTree(asList("123", "456")))); + final RequestContext context = new RequestContext( + request(r -> r.site(site(s -> s.ext(extSite)))), + imp(identity()), + txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).containsOnly("123", "456"); + } + + @Test + public void lookupStringsShouldReturnSegmentsWithDesiredSource() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.userSegment, "rubicon"); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.data(asList( + data(d -> d.id("rubicon").segment(asList(segment(s -> s.id("1")), segment(s -> s.id("2"))))), + data(d -> d.id("bluekai").segment( + asList(segment(s -> s.id("3")), segment(s -> s.id("4")))))))))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).containsOnly("1", "2"); + } + + @Test + public void lookupStringsShouldReturnEmptyListWhenDesiredSourceIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.userSegment, "rubicon"); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.data(singletonList( + data(d -> d.id("bluekai").segment( + asList(segment(s -> s.id("3")), segment(s -> s.id("4")))))))))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).isEmpty(); + } + + @Test + public void lookupStringsShouldSkipSegmentsWithoutIds() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.userSegment, "rubicon"); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.data(singletonList( + data(d -> d.id("rubicon").segment(asList(segment(s -> s.id("1")), segment(identity()))))))))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).containsOnly("1"); + } + + @Test + public void lookupStringsShouldReturnEmptyListWhenSegmentsAreMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.userSegment, "rubicon"); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.data(singletonList(data(d -> d.id("rubicon"))))))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).isEmpty(); + } + + @Test + public void lookupStringsShouldTolerateMissingSource() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.userSegment, "rubicon"); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.data(singletonList(data(identity())))))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).isEmpty(); + } + + @Test + public void lookupStringsShouldReturnEmptyListWhenDataIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.userSegment, "rubicon"); + final RequestContext context = new RequestContext( + request(r -> r.user(user(identity()))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).isEmpty(); + } + + @Test + public void lookupStringsShouldReturnEmptyListWhenUserIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.userSegment, "rubicon"); + final RequestContext context = new RequestContext( + request(identity()), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupStrings(category)).isEmpty(); + } + + @Test + public void lookupStringsShouldThrowExceptionWhenUnexpectedCategory() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.domain); + final RequestContext context = new RequestContext(request(identity()), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThatThrownBy(() -> context.lookupStrings(category)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Unexpected category for fetching string values for: domain"); + } + + @Test + public void lookupIntegersShouldReturnBidderParam() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", + obj("rubicon", obj("siteId", mapper.valueToTree(asList(123, 456)))))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupIntegers(category)).containsOnly(123, 456); + } + + @Test + public void lookupIntegersShouldReturnEmptyListWhenBidderParamIsNotArray() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", obj("rubicon", obj("siteId", "phony")))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupIntegers(category)).isEmpty(); + } + + @Test + public void lookupIntegersShouldReturnOnlyIntegersWhenNonIntegerBidderParamPresent() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", + obj("rubicon", obj("siteId", mapper.valueToTree(asList(123, "456")))))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupIntegers(category)).containsOnly(123); + } + + @Test + public void lookupIntegersShouldReturnEmptyListWhenBidderParamIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.bidderParam, "rubicon.siteId"); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.ext(obj("prebid", obj("bidder", obj("rubicon", "phony"))))), + txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupIntegers(category)).isEmpty(); + } + + @Test + public void lookupIntegersShouldReturnUserFirstPartyData() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.userFirstPartyData, "sport"); + final ExtUser extUser = ExtUser.builder().data(obj("sport", mapper.valueToTree(asList(123, 456)))).build(); + final RequestContext context = new RequestContext( + request(r -> r.user(user(u -> u.ext(extUser)))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupIntegers(category)).containsOnly(123, 456); + } + + @Test + public void lookupIntegersShouldReturnSiteFirstPartyData() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.siteFirstPartyData, "sport"); + final ExtSite extSite = ExtSite.of(null, obj("sport", mapper.valueToTree(asList(123, 456)))); + final RequestContext context = new RequestContext( + request(r -> r.site(site(s -> s.ext(extSite)))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupIntegers(category)).containsOnly(123, 456); + } + + @Test + public void lookupIntegersShouldThrowExceptionWhenUnexpectedCategory() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.domain); + final RequestContext context = new RequestContext(request(identity()), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThatThrownBy(() -> context.lookupIntegers(category)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Unexpected category for fetching integer values for: domain"); + } + + @Test + public void lookupSizesShouldReturnSizes() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.size); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.banner(banner(b -> b.format(asList(format(300, 250), format(400, 300)))))), txnLog, + jacksonMapper); + + // when and then + assertThat(context.lookupSizes(category)).containsOnly(Size.of(300, 250), Size.of(400, 300)); + } + + @Test + public void lookupSizesShouldReturnEmptyListWhenFormatIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.size); + final RequestContext context = new RequestContext( + request(identity()), + imp(i -> i.banner(banner(identity()))), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupSizes(category)).isEmpty(); + } + + @Test + public void lookupSizesShouldReturnEmptyListWhenBannerIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.size); + final RequestContext context = new RequestContext( + request(identity()), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupSizes(category)).isEmpty(); + } + + @Test + public void lookupSizesShouldThrowExceptionWhenUnexpectedCategory() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.domain); + final RequestContext context = new RequestContext(request(identity()), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThatThrownBy(() -> context.lookupSizes(category)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Unexpected category for fetching sizes for: domain"); + } + + @Test + public void lookupGeoLocationShouldReturnLocation() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.location); + final RequestContext context = new RequestContext( + request(r -> r.device(device(d -> d.geo(geo(g -> g.lat(50f).lon(60f)))))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupGeoLocation(category)).isEqualTo(GeoLocation.of(50f, 60f)); + } + + @Test + public void lookupGeoLocationShouldReturnNullWhenLonIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.location); + final RequestContext context = new RequestContext( + request(r -> r.device(device(d -> d.geo(geo(g -> g.lat(50f)))))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupGeoLocation(category)).isNull(); + } + + @Test + public void lookupGeoLocationShouldReturnNullWhenLatIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.location); + final RequestContext context = new RequestContext( + request(r -> r.device(device(d -> d.geo(geo(g -> g.lon(60f)))))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupGeoLocation(category)).isNull(); + } + + @Test + public void lookupGeoLocationShouldReturnNullWhenGeoIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.location); + final RequestContext context = new RequestContext( + request(r -> r.device(device(identity()))), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupGeoLocation(category)).isNull(); + } + + @Test + public void lookupGeoLocationShouldReturnNullWhenDeviceIsMissing() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.location); + final RequestContext context = new RequestContext( + request(identity()), + imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThat(context.lookupGeoLocation(category)).isNull(); + } + + @Test + public void lookupGeoLocationShouldThrowExceptionWhenUnexpectedCategory() { + // given + final TargetingCategory category = new TargetingCategory(TargetingCategory.Type.domain); + final RequestContext context = new RequestContext(request(identity()), imp(identity()), txnLog, jacksonMapper); + + // when and then + assertThatThrownBy(() -> context.lookupGeoLocation(category)) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("Unexpected category for fetching geo location for: domain"); + } + + private static BidRequest request(Function customizer) { + return customizer.apply(BidRequest.builder()).build(); + } + + private static Site site(Function customizer) { + return customizer.apply(Site.builder()).build(); + } + + private static App app(Function customizer) { + return customizer.apply(App.builder()).build(); + } + + private static Device device(Function customizer) { + return customizer.apply(Device.builder()).build(); + } + + private static Geo geo(Function customizer) { + return customizer.apply(Geo.builder()).build(); + } + + private static User user(Function customizer) { + return customizer.apply(User.builder()).build(); + } + + private static Data data(Function customizer) { + return customizer.apply(Data.builder()).build(); + } + + private static Segment segment(Function customizer) { + return customizer.apply(Segment.builder()).build(); + } + + private static Imp imp(Function customizer) { + return customizer.apply(Imp.builder()).build(); + } + + private static Banner banner(Function customizer) { + return customizer.apply(Banner.builder()).build(); + } + + private static Format format(int w, int h) { + return Format.builder().w(w).h(h).build(); + } + + private static ObjectNode obj(String field, String value) { + return mapper.createObjectNode().put(field, value); + } + + private static ObjectNode obj(String field, Integer value) { + return mapper.createObjectNode().put(field, value); + } + + private static ObjectNode obj(String field, JsonNode value) { + return mapper.createObjectNode().set(field, value); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/interpret/AndTest.java b/src/test/java/org/prebid/server/deals/targeting/interpret/AndTest.java new file mode 100644 index 00000000000..92b398daafe --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/interpret/AndTest.java @@ -0,0 +1,53 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.deals.targeting.RequestContext; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class AndTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private TerminalExpression trueExpression; + @Mock + private TerminalExpression falseExpression; + @Mock + private RequestContext context; + + @Before + public void setUp() { + given(trueExpression.matches(any())).willReturn(true); + given(falseExpression.matches(any())).willReturn(false); + } + + @Test + public void matchesShouldReturnTrue() { + assertThat(new And(asList(trueExpression, trueExpression, trueExpression)).matches(context)).isTrue(); + } + + @Test + public void matchesShouldReturnFalse() { + assertThat(new And(asList(falseExpression, falseExpression, falseExpression)).matches(context)).isFalse(); + } + + @Test + public void matchesShouldReturnFalseAndNotEvaluateRemainingExpressions() { + assertThat(new And(asList(trueExpression, falseExpression, trueExpression)).matches(context)).isFalse(); + + verify(trueExpression, times(1)).matches(context); + verify(falseExpression, times(1)).matches(context); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/interpret/InIntegersTest.java b/src/test/java/org/prebid/server/deals/targeting/interpret/InIntegersTest.java new file mode 100644 index 00000000000..6c2813b641f --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/interpret/InIntegersTest.java @@ -0,0 +1,64 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +public class InIntegersTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private RequestContext context; + + private Expression expression; + private TargetingCategory category; + + @Before + public void setUp() { + // given + category = new TargetingCategory(TargetingCategory.Type.pagePosition); + expression = new InIntegers(category, asList(1, 2, 3)); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatch() { + // given + willReturn(2).given(context).lookupInteger(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + verify(context).lookupInteger(eq(category)); + } + + @Test + public void matchesShouldReturnFalseWhenThereIsNoMatch() { + // given + willReturn(4).given(context).lookupInteger(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } + + @Test + public void matchesShouldReturnFalseWhenActualValueIsMissing() { + // given + willReturn(null).given(context).lookupInteger(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/interpret/InStringsTest.java b/src/test/java/org/prebid/server/deals/targeting/interpret/InStringsTest.java new file mode 100644 index 00000000000..46ce1ed18a4 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/interpret/InStringsTest.java @@ -0,0 +1,73 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +public class InStringsTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private RequestContext context; + + private Expression expression; + private TargetingCategory category; + + @Before + public void setUp() { + // given + category = new TargetingCategory(TargetingCategory.Type.referrer); + expression = new InStrings(category, asList("Munich", "Berlin", "Stuttgart")); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatch() { + // given + willReturn("Berlin").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + verify(context).lookupString(eq(category)); + } + + @Test + public void matchesShouldReturnFalseWhenThereIsNoMatch() { + // given + willReturn("Ingolstadt").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } + + @Test + public void matchesShouldPerformCaseInsensitiveComparison() { + // given + willReturn("BERLIN").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + } + + @Test + public void matchesShouldReturnFalseWhenActualValueIsMissing() { + // given + willReturn(null).given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/interpret/IntersectsIntegersTest.java b/src/test/java/org/prebid/server/deals/targeting/interpret/IntersectsIntegersTest.java new file mode 100644 index 00000000000..5fff4e9a1c5 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/interpret/IntersectsIntegersTest.java @@ -0,0 +1,65 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +public class IntersectsIntegersTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private RequestContext context; + + private Expression expression; + private TargetingCategory category; + + @Before + public void setUp() { + // given + category = TargetingCategory.fromString("bidp.rubicon.invCodes"); + expression = new IntersectsIntegers(category, asList(1, 2, 3)); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatch() { + // given + willReturn(asList(2, 3, 4)).given(context).lookupIntegers(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + verify(context).lookupIntegers(eq(category)); + } + + @Test + public void matchesShouldReturnFalseWhenThereIsNoMatch() { + // given + willReturn(asList(4, 5)).given(context).lookupIntegers(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } + + @Test + public void matchesShouldReturnFalseWhenActualValueIsMissing() { + // given + willReturn(emptyList()).given(context).lookupIntegers(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/interpret/IntersectsSizesTest.java b/src/test/java/org/prebid/server/deals/targeting/interpret/IntersectsSizesTest.java new file mode 100644 index 00000000000..0afb70cb76d --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/interpret/IntersectsSizesTest.java @@ -0,0 +1,66 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.model.Size; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +public class IntersectsSizesTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private RequestContext context; + + private Expression expression; + private TargetingCategory category; + + @Before + public void setUp() { + // given + category = TargetingCategory.fromString("adunit.size"); + expression = new IntersectsSizes(category, asList(Size.of(250, 300), Size.of(300, 350), Size.of(350, 400))); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatch() { + // given + willReturn(asList(Size.of(300, 350), Size.of(350, 400), Size.of(450, 500))).given(context).lookupSizes(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + verify(context).lookupSizes(eq(category)); + } + + @Test + public void matchesShouldReturnFalseWhenThereIsNoMatch() { + // given + willReturn(asList(Size.of(450, 500), Size.of(500, 550))).given(context).lookupSizes(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } + + @Test + public void matchesShouldReturnFalseWhenActualValueIsMissing() { + // given + willReturn(emptyList()).given(context).lookupSizes(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/interpret/IntersectsStringsTest.java b/src/test/java/org/prebid/server/deals/targeting/interpret/IntersectsStringsTest.java new file mode 100644 index 00000000000..ab25981e19e --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/interpret/IntersectsStringsTest.java @@ -0,0 +1,74 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +public class IntersectsStringsTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private RequestContext context; + + private Expression expression; + private TargetingCategory category; + + @Before + public void setUp() { + // given + category = TargetingCategory.fromString("adunit.mediatype"); + expression = new IntersectsStrings(category, asList("Pop", "Rock", "Alternative")); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatch() { + // given + willReturn(asList("Rock", "Alternative", "Folk")).given(context).lookupStrings(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + verify(context).lookupStrings(eq(category)); + } + + @Test + public void matchesShouldReturnFalseWhenThereIsNoMatch() { + // given + willReturn(asList("Folk", "Trance")).given(context).lookupStrings(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } + + @Test + public void matchesShouldPerformCaseInsensitiveComparison() { + // given + willReturn(asList("ROCK", "ALTERNATIVE", "FOLK")).given(context).lookupStrings(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + } + + @Test + public void matchesShouldReturnFalseWhenActualValueIsMissing() { + // given + willReturn(emptyList()).given(context).lookupStrings(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/interpret/MatchesTest.java b/src/test/java/org/prebid/server/deals/targeting/interpret/MatchesTest.java new file mode 100644 index 00000000000..390c4bf1e29 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/interpret/MatchesTest.java @@ -0,0 +1,167 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +public class MatchesTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private RequestContext context; + + private Expression expression; + private TargetingCategory category; + + @Before + public void setUp() { + // given + category = TargetingCategory.fromString("adunit.adslot"); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatchForEquals() { + // given + expression = new Matches(category, "adunit"); + + willReturn("adunit").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + verify(context).lookupString(eq(category)); + } + + @Test + public void matchesShouldReturnFalseWhenThereIsNoMatchForEquals() { + // given + expression = new Matches(category, "adunit"); + + willReturn("notadunit").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatchForStartsWith() { + // given + expression = new Matches(category, "adunit*"); + + willReturn("adunitOne").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + } + + @Test + public void matchesShouldReturnFalseWhenThereIsNoMatchForStartsWith() { + // given + expression = new Matches(category, "adunit"); + + willReturn("somedunit").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatchForEndsWith() { + // given + expression = new Matches(category, "*adunit"); + + willReturn("someadunit").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + } + + @Test + public void matchesShouldReturnFalseWhenThereIsNoMatchForEndsWith() { + // given + expression = new Matches(category, "*adunit"); + + willReturn("adunitOne").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatchForContainsInTheMiddle() { + // given + expression = new Matches(category, "*adunit*"); + + willReturn("someadunitOne").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatchForContainsInTheBeginning() { + // given + expression = new Matches(category, "*adunit*"); + + willReturn("adunitOne").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatchForContainsInTheEnd() { + // given + expression = new Matches(category, "*adunit*"); + + willReturn("someadunit").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + } + + @Test + public void matchesShouldReturnFalseWhenThereIsNoMatchForContains() { + // given + expression = new Matches(category, "*adunit*"); + + willReturn("One").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } + + @Test + public void matchesShouldPerformCaseInsensitiveComparison() { + // given + expression = new Matches(category, "AdUnIt"); + + willReturn("aDuNiT").given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + } + + @Test + public void matchesShouldReturnFalseWhenActualValueIsMissing() { + // given + expression = new Matches(category, "adunit"); + + willReturn(null).given(context).lookupString(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/interpret/NotTest.java b/src/test/java/org/prebid/server/deals/targeting/interpret/NotTest.java new file mode 100644 index 00000000000..ef1fec7e66f --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/interpret/NotTest.java @@ -0,0 +1,42 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.deals.targeting.RequestContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +public class NotTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private TerminalExpression trueExpression; + @Mock + private TerminalExpression falseExpression; + @Mock + private RequestContext context; + + @Before + public void setUp() { + given(trueExpression.matches(any())).willReturn(true); + given(falseExpression.matches(any())).willReturn(false); + } + + @Test + public void matchesShouldReturnTrue() { + assertThat(new Not(trueExpression).matches(context)).isFalse(); + } + + @Test + public void matchesShouldReturnFalse() { + assertThat(new Not(falseExpression).matches(context)).isTrue(); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/interpret/OrTest.java b/src/test/java/org/prebid/server/deals/targeting/interpret/OrTest.java new file mode 100644 index 00000000000..b3e9685d1a0 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/interpret/OrTest.java @@ -0,0 +1,53 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.deals.targeting.RequestContext; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class OrTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private TerminalExpression trueExpression; + @Mock + private TerminalExpression falseExpression; + @Mock + private RequestContext context; + + @Before + public void setUp() { + given(trueExpression.matches(any())).willReturn(true); + given(falseExpression.matches(any())).willReturn(false); + } + + @Test + public void matchesShouldReturnTrue() { + assertThat(new Or(asList(trueExpression, trueExpression, trueExpression)).matches(context)).isTrue(); + } + + @Test + public void matchesShouldReturnFalse() { + assertThat(new Or(asList(falseExpression, falseExpression, falseExpression)).matches(context)).isFalse(); + } + + @Test + public void matchesShouldReturnTrueAndNotEvaluateRemainingExpressions() { + assertThat(new Or(asList(falseExpression, trueExpression, falseExpression)).matches(context)).isTrue(); + + verify(falseExpression, times(1)).matches(context); + verify(trueExpression, times(1)).matches(context); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/interpret/WithinTest.java b/src/test/java/org/prebid/server/deals/targeting/interpret/WithinTest.java new file mode 100644 index 00000000000..c6dccf1e537 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/interpret/WithinTest.java @@ -0,0 +1,65 @@ +package org.prebid.server.deals.targeting.interpret; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.deals.targeting.RequestContext; +import org.prebid.server.deals.targeting.model.GeoLocation; +import org.prebid.server.deals.targeting.model.GeoRegion; +import org.prebid.server.deals.targeting.syntax.TargetingCategory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.verify; + +public class WithinTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private RequestContext context; + + private Expression expression; + private TargetingCategory category; + + @Before + public void setUp() { + // given + category = new TargetingCategory(TargetingCategory.Type.location); + expression = new Within(category, GeoRegion.of(50.424782f, 30.506423f, 10f)); + } + + @Test + public void matchesShouldReturnTrueWhenThereIsMatch() { + // given + willReturn(GeoLocation.of(50.442406f, 30.521439f)).given(context).lookupGeoLocation(any()); + + // when and then + assertThat(expression.matches(context)).isTrue(); + verify(context).lookupGeoLocation(eq(category)); + } + + @Test + public void matchesShouldReturnFalseWhenThereIsNoMatch() { + // given + willReturn(GeoLocation.of(50.588196f, 30.512357f)).given(context).lookupGeoLocation(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } + + @Test + public void matchesShouldReturnFalseWhenActualValueIsMissing() { + // given + willReturn(null).given(context).lookupGeoLocation(any()); + + // when and then + assertThat(expression.matches(context)).isFalse(); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/syntax/BooleanOperatorTest.java b/src/test/java/org/prebid/server/deals/targeting/syntax/BooleanOperatorTest.java new file mode 100644 index 00000000000..bfc03545845 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/syntax/BooleanOperatorTest.java @@ -0,0 +1,51 @@ +package org.prebid.server.deals.targeting.syntax; + +import org.assertj.core.api.SoftAssertions; +import org.junit.Test; + +import java.util.EnumMap; +import java.util.Map; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class BooleanOperatorTest { + + @Test + public void isBooleanOperatorShouldReturnTrueForKnownFunctions() { + SoftAssertions.assertSoftly(softly -> { + for (final String functionString : asList("$and", "$or", "$not")) { + softly.assertThat(BooleanOperator.isBooleanOperator(functionString)).isTrue(); + } + }); + } + + @Test + public void isBooleanOperatorShouldReturnFalseForUnknownFunctions() { + assertThat(BooleanOperator.isBooleanOperator("unknown")).isFalse(); + } + + @Test + public void fromStringShouldReturnEnumValue() { + // given + final EnumMap enumToString = new EnumMap<>(BooleanOperator.class); + enumToString.put(BooleanOperator.AND, "$and"); + enumToString.put(BooleanOperator.OR, "$or"); + enumToString.put(BooleanOperator.NOT, "$not"); + + // when and then + SoftAssertions.assertSoftly(softly -> { + for (final Map.Entry entry : enumToString.entrySet()) { + softly.assertThat(BooleanOperator.fromString(entry.getValue())).isEqualTo(entry.getKey()); + } + }); + } + + @Test + public void fromStringShouldThrowExceptionWhenUnkownFunction() { + assertThatThrownBy(() -> BooleanOperator.fromString("unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unrecognized boolean operator: unknown"); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/syntax/MatchingFunctionTest.java b/src/test/java/org/prebid/server/deals/targeting/syntax/MatchingFunctionTest.java new file mode 100644 index 00000000000..748ffd3289c --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/syntax/MatchingFunctionTest.java @@ -0,0 +1,52 @@ +package org.prebid.server.deals.targeting.syntax; + +import org.assertj.core.api.SoftAssertions; +import org.junit.Test; + +import java.util.EnumMap; +import java.util.Map; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class MatchingFunctionTest { + + @Test + public void isMatchingFunctionShouldReturnTrueForKnownFunctions() { + SoftAssertions.assertSoftly(softly -> { + for (final String functionString : asList("$matches", "$in", "$intersects", "$within")) { + softly.assertThat(MatchingFunction.isMatchingFunction(functionString)).isTrue(); + } + }); + } + + @Test + public void isMatchingFunctionShouldReturnFalseForUnknownFunctions() { + assertThat(MatchingFunction.isMatchingFunction("unknown")).isFalse(); + } + + @Test + public void fromStringShouldReturnEnumValue() { + // given + final EnumMap enumToString = new EnumMap<>(MatchingFunction.class); + enumToString.put(MatchingFunction.MATCHES, "$matches"); + enumToString.put(MatchingFunction.IN, "$in"); + enumToString.put(MatchingFunction.INTERSECTS, "$intersects"); + enumToString.put(MatchingFunction.WITHIN, "$within"); + + // when and then + SoftAssertions.assertSoftly(softly -> { + for (final Map.Entry entry : enumToString.entrySet()) { + softly.assertThat(MatchingFunction.fromString(entry.getValue())).isEqualTo(entry.getKey()); + } + }); + } + + @Test + public void fromStringShouldThrowExceptionWhenUnkownFunction() { + assertThatThrownBy(() -> MatchingFunction.fromString("unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unrecognized matching function: unknown"); + } +} diff --git a/src/test/java/org/prebid/server/deals/targeting/syntax/TargetingCategoryTest.java b/src/test/java/org/prebid/server/deals/targeting/syntax/TargetingCategoryTest.java new file mode 100644 index 00000000000..27783eea794 --- /dev/null +++ b/src/test/java/org/prebid/server/deals/targeting/syntax/TargetingCategoryTest.java @@ -0,0 +1,119 @@ +package org.prebid.server.deals.targeting.syntax; + +import org.assertj.core.api.AutoCloseableSoftAssertions; +import org.junit.Test; +import org.prebid.server.exception.TargetingSyntaxException; + +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TargetingCategoryTest { + + @Test + public void isTargetingCategoryShouldReturnTrueForKnownCategories() { + final List categories = asList( + "adunit.size", + "adunit.mediatype", + "adunit.adslot", + "site.domain", + "site.referrer", + "app.bundle", + "device.geo.ext.vendor.attribute", + "device.geo.ext.vendor.nested.attribute", + "device.ext.vendor.attribute", + "device.ext.vendor.nested.attribute", + "pos", + "geo.distance", + "segment.bluekai", + "user.ext.time.userdow", + "user.ext.time.userhour", + "bidp.rubicon.siteId", + "ufpd.sport", + "sfpd.sport"); + + try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + categories.forEach(categoryString -> + softly.assertThat(TargetingCategory.isTargetingCategory(categoryString)).isTrue()); + } + } + + @Test + public void isTargetingCategoryShouldReturnFalseForUnknownCategory() { + assertThat(TargetingCategory.isTargetingCategory("phony")).isFalse(); + } + + @Test + public void fromStringShouldReturnCategoryWithoutPathForStaticTypes() { + final List categories = asList( + "adunit.size", + "adunit.mediatype", + "adunit.adslot", + "site.domain", + "site.referrer", + "app.bundle", + "pos", + "geo.distance", + "user.ext.time.userdow", + "user.ext.time.userhour"); + + try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + for (final String categoryString : categories) { + final TargetingCategory category = TargetingCategory.fromString(categoryString); + softly.assertThat(category.type()).isEqualTo(TargetingCategory.Type.fromString(categoryString)); + softly.assertThat(category.path()).isNull(); + } + } + } + + @Test + public void fromStringShouldReturnCategoryWithPathForDynamicTypes() { + // when + final TargetingCategory bidderParamCategory = TargetingCategory.fromString("bidp.rubicon.siteId"); + // then + assertThat(bidderParamCategory.type()).isEqualTo(TargetingCategory.Type.bidderParam); + assertThat(bidderParamCategory.path()).isEqualTo("rubicon.siteId"); + + // when + final TargetingCategory userSegmentCategory = TargetingCategory.fromString("segment.bluekai"); + // then + assertThat(userSegmentCategory.type()).isEqualTo(TargetingCategory.Type.userSegment); + assertThat(userSegmentCategory.path()).isEqualTo("bluekai"); + + // when + final TargetingCategory userFirstPartyDataCategory = TargetingCategory.fromString("ufpd.sport"); + // then + assertThat(userFirstPartyDataCategory.type()).isEqualTo(TargetingCategory.Type.userFirstPartyData); + assertThat(userFirstPartyDataCategory.path()).isEqualTo("sport"); + + // when + final TargetingCategory siteFirstPartyDataCategory = TargetingCategory.fromString("sfpd.sport"); + // then + assertThat(siteFirstPartyDataCategory.type()).isEqualTo(TargetingCategory.Type.siteFirstPartyData); + assertThat(siteFirstPartyDataCategory.path()).isEqualTo("sport"); + } + + @Test + public void fromStringShouldThrowExceptionWhenCategoryIsUnknown() { + assertThatThrownBy(() -> TargetingCategory.fromString("unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unrecognized targeting category: unknown"); + } + + @Test + public void fromStringShouldThrowExceptionWhenBidderParamIsIncorrect() { + assertThatThrownBy(() -> TargetingCategory.fromString("bidp.rubicon")) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("BidderParam path is incorrect: rubicon"); + + assertThatThrownBy(() -> TargetingCategory.fromString("bidp.rubicon.")) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("BidderParam path is incorrect: rubicon."); + + assertThatThrownBy(() -> TargetingCategory.fromString("bidp.rubicon.siteId.")) + .isInstanceOf(TargetingSyntaxException.class) + .hasMessage("BidderParam path is incorrect: rubicon.siteId."); + } +} diff --git a/src/test/java/org/prebid/server/events/EventUtilTest.java b/src/test/java/org/prebid/server/events/EventUtilTest.java index 70ec170a21e..b25430a31e3 100644 --- a/src/test/java/org/prebid/server/events/EventUtilTest.java +++ b/src/test/java/org/prebid/server/events/EventUtilTest.java @@ -217,7 +217,8 @@ public void fromShouldReturnExpectedEventRequest() { .add("b", "bidId") .add("ts", "1000") .add("f", "i") - .add("x", "0")); + .add("x", "0") + .add("l", "lineItemId")); // when final EventRequest result = EventUtil.from(routingContext); @@ -231,6 +232,7 @@ public void fromShouldReturnExpectedEventRequest() { .timestamp(1000L) .format(EventRequest.Format.image) .analytics(EventRequest.Analytics.disabled) + .lineItemId("lineItemId") .build()); } @@ -264,6 +266,7 @@ public void toUrlShouldReturnExpectedUrl() { // given final EventRequest eventRequest = EventRequest.builder() .type(EventRequest.Type.win) + .auctionId("auctionId") .accountId("accountId") .bidder("bidder") .bidId("bidId") @@ -271,6 +274,7 @@ public void toUrlShouldReturnExpectedUrl() { .integration("pbjs") .analytics(EventRequest.Analytics.enabled) .timestamp(1000L) + .lineItemId("lineItemId") .build(); // when @@ -278,14 +282,17 @@ public void toUrlShouldReturnExpectedUrl() { // then assertThat(result).isEqualTo( - "http://external-url/event?t=win&b=bidId&a=accountId&ts=1000&bidder=bidder&f=b&int=pbjs&x=1"); + "http://external-url/event?t=win&b=bidId&a=accountId" + + "&aid=auctionId&ts=1000&bidder=bidder&f=b&int=pbjs&x=1" + + "&l=lineItemId"); } @Test - public void toUrlShouldReturnExpectedUrlWithoutFormatAndAnalytics() { + public void toUrlShouldReturnExpectedUrlWithoutFormatAndAnalyticsAndLineItemId() { // given final EventRequest eventRequest = EventRequest.builder() .type(EventRequest.Type.win) + .auctionId("auctionId") .accountId("accountId") .bidder("bidder") .bidId("bidId") @@ -296,7 +303,8 @@ public void toUrlShouldReturnExpectedUrlWithoutFormatAndAnalytics() { final String result = EventUtil.toUrl("http://external-url", eventRequest); // then - assertThat(result).isEqualTo("http://external-url/event?t=win&b=bidId&a=accountId&ts=1000&bidder=bidder&int="); + assertThat(result).isEqualTo("http://external-url/event?t=win&b=bidId&a=accountId" + + "&aid=auctionId&ts=1000&bidder=bidder&int="); } @Test @@ -304,6 +312,7 @@ public void toUrlShouldReturnExpectedUrlWithoutTimestamp() { // given final EventRequest eventRequest = EventRequest.builder() .type(EventRequest.Type.win) + .auctionId("auctionId") .accountId("accountId") .bidder("bidder") .bidId("bidId") @@ -314,6 +323,7 @@ public void toUrlShouldReturnExpectedUrlWithoutTimestamp() { final String result = EventUtil.toUrl("http://external-url", eventRequest); // then - assertThat(result).isEqualTo("http://external-url/event?t=win&b=bidId&a=accountId&bidder=bidder&int="); + assertThat(result).isEqualTo("http://external-url/event?t=win" + + "&b=bidId&a=accountId&aid=auctionId&bidder=bidder&int="); } } diff --git a/src/test/java/org/prebid/server/events/EventsServiceTest.java b/src/test/java/org/prebid/server/events/EventsServiceTest.java index 32c6b62ba27..6ebab3978e6 100644 --- a/src/test/java/org/prebid/server/events/EventsServiceTest.java +++ b/src/test/java/org/prebid/server/events/EventsServiceTest.java @@ -23,32 +23,97 @@ public void setUp() { @Test public void createEventsShouldReturnExpectedEvent() { + // given + final EventsContext eventsContext = EventsContext.builder().auctionId("auctionId") + .integration("pbjs") + .auctionTimestamp(1000L) + .build(); + + // when + final Events events = eventsService.createEvent("bidId", "bidder", "accountId", "lineItemId", true, + eventsContext); + + // then + assertThat(events).isEqualTo(Events.of( + "http://external-url/event?t=win&b=bidId&a=accountId&aid=auctionId&ts=1000&bidder=bidder&f=i&int=pbjs&l=lineItemId", + "http://external-url/event?t=imp&b=bidId&a=accountId&aid=auctionId&ts=1000&bidder=bidder&f=i&int=pbjs&l=lineItemId")); + } + + @Test + public void createEventsShouldSkipLineItemIdIfMissing() { + // given + final EventsContext eventsContext = EventsContext.builder().auctionId("auctionId").integration( + "pbjs").auctionTimestamp(1000L).build(); + // when - final Events events = eventsService.createEvent("bidId", "bidder", "accountId", 1000L, "pbjs"); + final Events events = eventsService.createEvent("bidId", "bidder", "accountId", null, true, eventsContext); // then assertThat(events).isEqualTo(Events.of( - "http://external-url/event?t=win&b=bidId&a=accountId&ts=1000&bidder=bidder&f=i&int=pbjs", - "http://external-url/event?t=imp&b=bidId&a=accountId&ts=1000&bidder=bidder&f=i&int=pbjs")); + "http://external-url/event?t=win&b=bidId&a=accountId" + + "&aid=auctionId&ts=1000&bidder=bidder&f=i&int=pbjs", + "http://external-url/event?t=imp&b=bidId&a=accountId" + + "&aid=auctionId&ts=1000&bidder=bidder&f=i&int=pbjs")); + } + + @Test + public void createEventsShouldSetAnalyticsDisabled() { + // given + final EventsContext eventsContext = EventsContext.builder().integration("pbjs").auctionTimestamp(1000L).build(); + + // when + final Events events = eventsService.createEvent("bidId", "bidder", "accountId", "lineItemId", false, + eventsContext); + + // then + assertThat(events).isEqualTo(Events.of( + "http://external-url/event?t=win&b=bidId&a=accountId&ts=1000&bidder=bidder&f=i&int=pbjs&x=0" + + "&l=lineItemId", + "http://external-url/event?t=imp&b=bidId&a=accountId&ts=1000&bidder=bidder&f=i&int=pbjs&x=0" + + "&l=lineItemId")); } @Test public void winUrlShouldReturnExpectedUrl() { + // given + final EventsContext eventsContext = EventsContext.builder().integration("pbjs").auctionTimestamp(1000L).build(); + // when - final String winUrl = eventsService.winUrl("bidId", "bidder", "accountId", 1000L, "pbjs"); + final String winUrl = eventsService.winUrl("bidId", "bidder", "accountId", "lineItemId", true, eventsContext); // then assertThat(winUrl).isEqualTo( - "http://external-url/event?t=win&b=bidId&a=accountId&ts=1000&bidder=bidder&f=i&int=pbjs"); + "http://external-url/event?t=win&b=bidId&a=accountId&ts=1000&bidder=bidder&f=i&int=pbjs&l=lineItemId"); + } + + @Test + public void winUrlShouldSEtAnalyticsDisabled() { + // given + final EventsContext eventsContext = EventsContext.builder().integration("pbjs").auctionTimestamp(1000L).build(); + + // when + final String winUrl = eventsService.winUrl("bidId", "bidder", "accountId", "lineItemId", false, eventsContext); + + // then + assertThat(winUrl).isEqualTo( + "http://external-url/event?t=win&b=bidId&a=accountId&ts=1000&bidder=bidder&f=i&int=pbjs&x=0" + + "&l=lineItemId"); } @Test public void vastUrlShouldReturnExpectedUrl() { + // given + final EventsContext eventsContext = EventsContext.builder().auctionId("auctionId") + .integration("pbjs") + .auctionTimestamp(1000L) + .build(); + // when - final String vastUrl = eventsService.vastUrlTracking("bidId", "bidder", "accountId", 1000L, "pbjs"); + final String vastUrl = eventsService.vastUrlTracking("bidId", "bidder", "accountId", "lineItemId", + eventsContext); // then assertThat(vastUrl).isEqualTo( - "http://external-url/event?t=imp&b=bidId&a=accountId&ts=1000&bidder=bidder&f=b&int=pbjs"); + "http://external-url/event?t=imp&b=bidId&a=accountId&aid=auctionId&ts=1000&bidder=bidder&f=b&int=pbjs&l=lineItemId"); } } diff --git a/src/test/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationServiceTest.java b/src/test/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationServiceTest.java index d11d5cd4e17..563816e8955 100644 --- a/src/test/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationServiceTest.java +++ b/src/test/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationServiceTest.java @@ -10,6 +10,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.BDDMockito; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; @@ -20,10 +21,10 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.util.function.BooleanSupplier; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -168,7 +169,7 @@ public void lookupShouldFailsWithOriginalExceptionIfOpeningIntervalExceeds(TestC } @Test - public void lookupShouldReportMetricsOnCircuitOpened(TestContext context) { + public void circuitBreakerGaugeShouldReportOpenedWhenCircuitOpen(TestContext context) { // given givenWrappedGeoLocationReturning(Future.failedFuture(new RuntimeException("exception"))); @@ -176,11 +177,15 @@ public void lookupShouldReportMetricsOnCircuitOpened(TestContext context) { doLookup(context); // then - verify(metrics).updateGeoLocationCircuitBreakerMetric(eq(true)); + final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class); + verify(metrics).createGeoLocationCircuitBreakerGauge(gaugeValueProviderCaptor.capture()); + final BooleanSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue(); + + assertThat(gaugeValueProvider.getAsBoolean()).isTrue(); } @Test - public void lookupShouldReportMetricsOnCircuitClosed(TestContext context) { + public void circuitBreakerGaugeShouldReportClosedWhenCircuitClosed(TestContext context) { // given givenWrappedGeoLocationReturning( Future.failedFuture(new RuntimeException("exception")), @@ -192,7 +197,11 @@ public void lookupShouldReportMetricsOnCircuitClosed(TestContext context) { doLookup(context); // 2 call // then - verify(metrics).updateGeoLocationCircuitBreakerMetric(eq(false)); + final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class); + verify(metrics).createGeoLocationCircuitBreakerGauge(gaugeValueProviderCaptor.capture()); + final BooleanSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue(); + + assertThat(gaugeValueProvider.getAsBoolean()).isFalse(); } @SuppressWarnings("unchecked") diff --git a/src/test/java/org/prebid/server/geolocation/MaxMindGeoLocationServiceTest.java b/src/test/java/org/prebid/server/geolocation/MaxMindGeoLocationServiceTest.java index 3435e53c745..b6c8fdc596b 100644 --- a/src/test/java/org/prebid/server/geolocation/MaxMindGeoLocationServiceTest.java +++ b/src/test/java/org/prebid/server/geolocation/MaxMindGeoLocationServiceTest.java @@ -94,4 +94,21 @@ public void lookupShouldReturnCountryIsoWhenDatabaseReaderWasSet() throws NoSuch .lon(2.3522f) .build()); } + + @Test + public void lookupShouldTolerateMissingGeoInfo() throws IOException, GeoIp2Exception, NoSuchFieldException { + // given + final DatabaseReader databaseReader = Mockito.mock(DatabaseReader.class); + given(databaseReader.city(any())).willReturn(null); + + FieldSetter.setField(maxMindGeoLocationService, + maxMindGeoLocationService.getClass().getDeclaredField("databaseReader"), databaseReader); + + // when + final Future future = maxMindGeoLocationService.lookup(TEST_IP, null); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isEqualTo(GeoInfo.builder().vendor("maxmind").build()); + } } diff --git a/src/test/java/org/prebid/server/handler/AccountCacheInvalidationHandlerTest.java b/src/test/java/org/prebid/server/handler/AccountCacheInvalidationHandlerTest.java index 175075a0e35..ccde1aa16f2 100644 --- a/src/test/java/org/prebid/server/handler/AccountCacheInvalidationHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/AccountCacheInvalidationHandlerTest.java @@ -36,11 +36,13 @@ public class AccountCacheInvalidationHandlerTest extends VertxTest { @Before public void setUp() { - handler = new AccountCacheInvalidationHandler(cachingApplicationSettings); + handler = new AccountCacheInvalidationHandler(cachingApplicationSettings, "/endpoint"); given(routingContext.request()).willReturn(httpRequest); given(routingContext.response()).willReturn(httpResponse); - given(routingContext.response().setStatusCode(anyInt())).willReturn(httpResponse); + + given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + given(httpResponse.closed()).willReturn(false); } @Test @@ -55,11 +57,11 @@ public void shouldReturnBadRequestWhenAccountParamIsMissing() { verify(httpResponse).setStatusCode(eq(400)); verify(httpResponse).end(eq("Account id is not defined")); - verify(httpRequest).getParam("account"); + verify(httpRequest).getParam(eq("account")); } @Test - public void shouldReturnOkAndTriggerInvalidateWhenAccountIdIsPresent() { + public void shouldTriggerInvalidateWhenAccountIdIsPresent() { // given given(httpRequest.getParam(any())).willReturn("123"); @@ -67,10 +69,8 @@ public void shouldReturnOkAndTriggerInvalidateWhenAccountIdIsPresent() { handler.handle(routingContext); // then - verify(httpResponse).setStatusCode(eq(200)); verify(cachingApplicationSettings).invalidateAccountCache("123"); - verify(httpRequest).getParam("account"); + verify(httpRequest).getParam(eq("account")); } } - diff --git a/src/test/java/org/prebid/server/handler/AuctionHandlerTest.java b/src/test/java/org/prebid/server/handler/AuctionHandlerTest.java deleted file mode 100644 index f8e71396e93..00000000000 --- a/src/test/java/org/prebid/server/handler/AuctionHandlerTest.java +++ /dev/null @@ -1,934 +0,0 @@ -package org.prebid.server.handler; - -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Format; -import io.netty.util.AsciiString; -import io.vertx.core.Future; -import io.vertx.core.Handler; -import io.vertx.core.http.CaseInsensitiveHeaders; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.ext.web.RoutingContext; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.prebid.server.VertxTest; -import org.prebid.server.auction.PreBidRequestContextFactory; -import org.prebid.server.auction.PrivacyEnforcementService; -import org.prebid.server.auction.model.AdUnitBid; -import org.prebid.server.auction.model.AdapterRequest; -import org.prebid.server.auction.model.AdapterResponse; -import org.prebid.server.auction.model.PreBidRequestContext; -import org.prebid.server.auction.model.PreBidRequestContext.PreBidRequestContextBuilder; -import org.prebid.server.bidder.Adapter; -import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.bidder.HttpAdapterConnector; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.cache.CacheService; -import org.prebid.server.cache.proto.BidCacheResult; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.metric.MetricName; -import org.prebid.server.metric.Metrics; -import org.prebid.server.privacy.gdpr.TcfDefinerService; -import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; -import org.prebid.server.privacy.gdpr.model.TcfResponse; -import org.prebid.server.privacy.model.PrivacyContext; -import org.prebid.server.proto.request.AdUnit; -import org.prebid.server.proto.request.PreBidRequest; -import org.prebid.server.proto.request.PreBidRequest.PreBidRequestBuilder; -import org.prebid.server.proto.response.Bid; -import org.prebid.server.proto.response.BidderInfo; -import org.prebid.server.proto.response.BidderStatus; -import org.prebid.server.proto.response.BidderStatus.BidderStatusBuilder; -import org.prebid.server.proto.response.MediaType; -import org.prebid.server.proto.response.PreBidResponse; -import org.prebid.server.proto.response.UsersyncInfo; -import org.prebid.server.settings.ApplicationSettings; -import org.prebid.server.settings.model.Account; -import org.prebid.server.util.HttpUtil; - -import java.io.IOException; -import java.math.BigDecimal; -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneId; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; -import static java.util.function.Function.identity; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anySet; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.anyList; -import static org.mockito.Mockito.anyLong; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.same; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; - -public class AuctionHandlerTest extends VertxTest { - - private static final String RUBICON = "rubicon"; - private static final String APPNEXUS = "appnexus"; - - @Rule - public final MockitoRule mockitoRule = MockitoJUnit.rule(); - - @Mock - private ApplicationSettings applicationSettings; - @Mock - private BidderCatalog bidderCatalog; - @Mock - private Adapter rubiconAdapter; - @Mock - private Adapter appnexusAdapter; - @Mock - private PreBidRequestContextFactory preBidRequestContextFactory; - @Mock - private CacheService cacheService; - @Mock - private Metrics metrics; - @Mock - private HttpAdapterConnector httpAdapterConnector; - - private Clock clock; - @Mock - private TcfDefinerService tcfDefinerService; - @Mock - private PrivacyEnforcementService privacyEnforcementService; - - private AuctionHandler auctionHandler; - @Mock - private RoutingContext routingContext; - @Mock - private HttpServerRequest httpRequest; - @Mock - private HttpServerResponse httpResponse; - - @Before - public void setUp() { - given(applicationSettings.getAccountById(any(), any())) - .willReturn(Future.succeededFuture(Account.builder().build())); - - given(bidderCatalog.isValidAdapterName(eq(RUBICON))).willReturn(true); - given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(true); - given(bidderCatalog.isActive(eq(RUBICON))).willReturn(true); - willReturn(rubiconAdapter).given(bidderCatalog).adapterByName(eq(RUBICON)); - given(bidderCatalog.bidderInfoByName(eq(RUBICON))).willReturn(givenBidderInfo(15)); - - given(bidderCatalog.isValidAdapterName(eq(APPNEXUS))).willReturn(true); - given(bidderCatalog.isValidName(eq(APPNEXUS))).willReturn(true); - given(bidderCatalog.isActive(eq(APPNEXUS))).willReturn(true); - willReturn(appnexusAdapter).given(bidderCatalog).adapterByName(eq(APPNEXUS)); - given(bidderCatalog.bidderInfoByName(eq(APPNEXUS))).willReturn(givenBidderInfo(20)); - - given(routingContext.request()).willReturn(httpRequest); - given(routingContext.response()).willReturn(httpResponse); - - given(httpRequest.headers()).willReturn(new CaseInsensitiveHeaders()); - - given(httpResponse.exceptionHandler(any())).willReturn(httpResponse); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - given(httpResponse.putHeader(any(CharSequence.class), any(CharSequence.class))).willReturn(httpResponse); - - clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); - - given(privacyEnforcementService.contextFromLegacyRequest(any(), any())) - .willReturn(Future.succeededFuture(PrivacyContext.of(null, null))); - given(tcfDefinerService.resultForVendorIds(anySet(), any())) - .willReturn(Future.succeededFuture(TcfResponse.of(true, emptyMap(), null))); - - auctionHandler = new AuctionHandler( - applicationSettings, - bidderCatalog, - preBidRequestContextFactory, - cacheService, - metrics, - httpAdapterConnector, - clock, - tcfDefinerService, - privacyEnforcementService, - jacksonMapper, - null); - } - - @Test - public void shouldRespondWithErrorIfRequestIsNotValid() throws IOException { - // given - given(preBidRequestContextFactory.fromRequest(any())) - .willReturn(Future.failedFuture(new PreBidException("Could not create"))); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getStatus()).isEqualTo("Error parsing request: Could not create"); - } - - @Test - public void shouldRespondWithErrorIfRequestBodyHasUnknownAccountId() throws IOException { - // given - givenPreBidRequestContext(identity(), identity()); - - given(applicationSettings.getAccountById(any(), any())) - .willReturn(Future.failedFuture(new PreBidException("Not found"))); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getStatus()).isEqualTo("Unknown account id: Unknown account"); - } - - @Test - public void shouldRespondWithExpectedHeaders() { - // given - givenPreBidRequestContext(identity(), identity()); - - // when - auctionHandler.handle(routingContext); - - // then - verify(httpResponse).putHeader(eq(new AsciiString("Date")), ArgumentMatchers.isNotNull()); - verify(httpResponse) - .putHeader(eq(new AsciiString("Content-Type")), eq(new AsciiString("application/json"))); - } - - @Test - public void shouldRespondWithNoCookieStatusIfNoLiveUidsInCookie() throws IOException { - // given - givenPreBidRequestContext(identity(), builder -> builder.noLiveUids(true)); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getStatus()).isEqualTo("no_cookie"); - } - - @Test - public void shouldRespondWithErrorIfUnexpectedExceptionOccurs() throws IOException { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.failedFuture(new RuntimeException())); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getStatus()).isEqualTo("Unexpected server error"); - } - - @Test - public void shouldInteractWithCacheServiceIfRequestHasBidsAndCacheMarkupFlag() throws IOException { - // given - final Timeout timeout = new TimeoutFactory(clock).create(500L); - - givenPreBidRequestContext( - builder -> builder.cacheMarkup(1), - builder -> builder - .timeout(timeout) - .adapterRequests(singletonList(AdapterRequest.of(RUBICON, singletonList(null))))); - - given(applicationSettings.getAccountById(any(), any())) - .willReturn(Future.succeededFuture(Account.builder().id("accountId").build())); - - givenBidderRespondingWithBids(RUBICON, identity(), "bidId1"); - - given(cacheService.cacheBids(anyList(), any(), anyString())).willReturn(Future.succeededFuture(singletonList( - BidCacheResult.of("0b4f60d1-fb99-4d95-ba6f-30ac90f9a315", "cached_asset_url")))); - given(cacheService.getCachedAssetURLTemplate()).willReturn("cached_asset_url"); - - // when - auctionHandler.handle(routingContext); - - // then - verify(cacheService).cacheBids(anyList(), same(timeout), eq("accountId")); - - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getBids()).extracting(Bid::getAdm).containsNull(); - assertThat(preBidResponse.getBids()).extracting(Bid::getNurl).containsNull(); - assertThat(preBidResponse.getBids()).extracting(Bid::getCacheId) - .containsOnly("0b4f60d1-fb99-4d95-ba6f-30ac90f9a315"); - assertThat(preBidResponse.getBids()).extracting(Bid::getCacheUrl).containsOnly("cached_asset_url"); - } - - @Test - public void shouldNotInteractWithCacheServiceIfRequestHasBidsAndNoCacheMarkupFlag() throws IOException { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - givenBidderRespondingWithBids(RUBICON, identity(), "bidId1"); - - // when - auctionHandler.handle(routingContext); - - // then - verifyZeroInteractions(cacheService); - - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getBids()).extracting(Bid::getCacheId).containsNull(); - assertThat(preBidResponse.getBids()).extracting(Bid::getCacheUrl).containsNull(); - } - - @Test - public void shouldNotInteractWithCacheServiceIfRequestHasNoBidsButCacheMarkupFlag() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(builder -> builder.cacheMarkup(1)); - - givenBidderRespondingWithBids(RUBICON, identity()); - - // when - auctionHandler.handle(routingContext); - - // then - verifyZeroInteractions(cacheService); - } - - @Test - public void shouldRespondWithErrorIfCacheServiceFails() throws IOException { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(builder -> builder.cacheMarkup(1)); - - givenBidderRespondingWithBids(RUBICON, identity(), "bidId1"); - - given(cacheService.cacheBids(anyList(), any(), any())).willReturn(Future.failedFuture("http exception")); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getStatus()).isEqualTo("Prebid cache failed: http exception"); - } - - @Test - public void shouldRespondWithMultipleBidderStatusesAndBidsWhenMultipleAdUnitsAndBidsInPreBidRequest() - throws IOException { - // given - givenPreBidRequestContextWith2AdUnitsAnd2BidsEach(builder -> builder.noLiveUids(false)); - - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(RUBICON).responseTimeMs(100).build(), - Arrays.stream(new String[]{"bidId1", "bidId2"}) - .map(id -> org.prebid.server.proto.response.Bid.builder() - .bidId(id) - .price(new BigDecimal("5.67")) - .build()) - .collect(Collectors.toList()), - null))) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(APPNEXUS).responseTimeMs(100).build(), - Arrays.stream(new String[]{"bidId3", "bidId4"}) - .map(id -> org.prebid.server.proto.response.Bid.builder() - .bidId(id) - .price(new BigDecimal("5.67")) - .build()) - .collect(Collectors.toList()), - null))); - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getStatus()).isEqualTo("OK"); - assertThat(preBidResponse.getTid()).isEqualTo("tid"); - assertThat(preBidResponse.getBidderStatus()).extracting(BidderStatus::getBidder) - .containsOnly(RUBICON, APPNEXUS); - assertThat(preBidResponse.getBids()).extracting(Bid::getBidId) - .containsOnly("bidId1", "bidId2", "bidId3", "bidId4"); - } - - @Test - public void shouldRespondWithBidsWithTargetingKeywordsWhenSortBidsFlagIsSetInPreBidRequest() throws IOException { - // given - final List adUnitBids = asList(null, null); - final List adapterRequests = asList(AdapterRequest.of(RUBICON, adUnitBids), - AdapterRequest.of(APPNEXUS, adUnitBids)); - givenPreBidRequestContext(builder -> builder.sortBids(1), builder -> builder.adapterRequests(adapterRequests)); - - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(RUBICON).responseTimeMs(100).build(), - asList( - org.prebid.server.proto.response.Bid.builder() - .bidder(RUBICON).code("adUnitCode1").bidId("bidId1") - .price(new BigDecimal("5.67")) - .responseTimeMs(60).adServerTargeting( - singletonMap("rpfl_1001", "2_tier0100")).build(), - org.prebid.server.proto.response.Bid.builder() - .bidder(RUBICON).code("adUnitCode2").bidId("bidId2") - .price(new BigDecimal("6.35")) - .responseTimeMs(80).build()), - null))) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(APPNEXUS).responseTimeMs(100).build(), - asList( - org.prebid.server.proto.response.Bid.builder() - .bidder(APPNEXUS).code("adUnitCode1").bidId("bidId3") - .price(new BigDecimal("5.67")) - .responseTimeMs(50).build(), - org.prebid.server.proto.response.Bid.builder() - .bidder(APPNEXUS).code("adUnitCode2").bidId("bidId4") - .price(new BigDecimal("7.15")) - .responseTimeMs(100).build()), - null))); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getBids()).extracting(Bid::getAdServerTargeting).doesNotContainNull(); - // verify that ad server targeting has been preserved - assertThat(preBidResponse.getBids()).extracting(Bid::getBidId, b -> b.getAdServerTargeting().get("rpfl_1001")) - .contains(tuple("bidId1", "2_tier0100")); - // weird way to verify that sorting has happened before bids grouped by ad unit code are enriched with targeting - // keywords - assertThat(preBidResponse.getBids()).extracting(Bid::getBidId, b -> b.getAdServerTargeting().get("hb_bidder")) - .containsOnly( - tuple("bidId1", null), - tuple("bidId2", null), - tuple("bidId3", APPNEXUS), - tuple("bidId4", APPNEXUS)); - } - - @Test - public void shouldRespondWithValidBannerBidIfSizeIsMissedButRecoveredFromAdUnit() throws IOException { - // given - final List adUnitBids = singletonList(AdUnitBid.builder() - .adUnitCode("adUnitCode1") - .bidId("bidId1") - .sizes(singletonList(Format.builder().w(100).h(200).build())) - .build()); - final List adapterRequests = singletonList(AdapterRequest.of(RUBICON, adUnitBids)); - - givenPreBidRequestContext(identity(), builder -> builder.adapterRequests(adapterRequests)); - - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(RUBICON).responseTimeMs(100).numBids(1).build(), - singletonList( - org.prebid.server.proto.response.Bid.builder().mediaType(MediaType.banner) - .bidder(RUBICON).code("adUnitCode1").bidId("bidId1") - .price(new BigDecimal("5.67")).build()), - null))); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getBids()).hasSize(1); - assertThat(preBidResponse.getBidderStatus().get(0).getNumBids()).isEqualTo(1); - } - - @Test - public void shouldRespondWithValidVideoBidEvenIfSizeIsMissed() throws IOException { - // given - final List adUnitBids = singletonList(null); - final List adapterRequests = singletonList(AdapterRequest.of(RUBICON, adUnitBids)); - - givenPreBidRequestContext(identity(), builder -> builder.adapterRequests(adapterRequests)); - - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(RUBICON).responseTimeMs(100).numBids(1).build(), - singletonList( - org.prebid.server.proto.response.Bid.builder().mediaType(MediaType.video) - .bidId("bidId1").price(new BigDecimal("5.67")).build()), - null))); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getBids()).hasSize(1); - assertThat(preBidResponse.getBidderStatus().get(0).getNumBids()).isEqualTo(1); - } - - @Test - public void shouldSupportOfBidderAlias() { - // given - given(bidderCatalog.isAlias("rubiconAlias")).willReturn(true); - given(bidderCatalog.nameByAlias("rubiconAlias")).willReturn(RUBICON); - - final List adapterRequests = - singletonList(AdapterRequest.of("rubiconAlias", singletonList(null))); - givenPreBidRequestContext(identity(), builder -> builder.adapterRequests(adapterRequests)); - - // when - auctionHandler.handle(routingContext); - - // then - verify(httpAdapterConnector).call(eq(rubiconAdapter), any(), eq(adapterRequests.get(0)), any()); - } - - @Test - public void shouldTolerateUnsupportedBidderInPreBidRequest() throws IOException { - // given - final List adapterRequests = asList( - AdapterRequest.of("unsupported", singletonList(null)), - AdapterRequest.of(RUBICON, singletonList(null))); - givenPreBidRequestContext(identity(), builder -> builder.adapterRequests(adapterRequests)); - - givenBidderRespondingWithBids(RUBICON, identity(), "bidId1"); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getBidderStatus()) - .extracting(BidderStatus::getBidder, BidderStatus::getError).containsOnly( - tuple("unsupported", "Unsupported bidder"), - tuple(RUBICON, null)); - assertThat(preBidResponse.getBids()).hasSize(1); - } - - @Test - public void shouldTolerateErrorResultFromAdapter() throws IOException { - // given - givenPreBidRequestContextWith2AdUnitsAnd2BidsEach(identity()); - - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(RUBICON).responseTimeMs(500).error("rubicon error").build(), - emptyList(), BidderError.badInput("rubicon error")))) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(APPNEXUS).responseTimeMs(100).build(), - singletonList(org.prebid.server.proto.response.Bid.builder() - .bidId("bidId1") - .price(new BigDecimal("5.67")) - .build()), - null))); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getBidderStatus()).extracting(BidderStatus::getBidder, BidderStatus::getError) - .containsOnly( - tuple(RUBICON, "rubicon error"), - tuple(APPNEXUS, null)); - assertThat(preBidResponse.getBids()).hasSize(1); - } - - @SuppressWarnings("unchecked") - @Test - public void shouldIncrementCommonMetrics() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(builder -> builder - .adUnits(singletonList(AdUnit.builder() - .mediaTypes(singletonList("banner")) - .build())) - .app(App.builder().build())); - - // simulate calling end handler that is supposed to update request_time timer value - given(httpResponse.endHandler(any())).willAnswer(inv -> { - ((Handler) inv.getArgument(0)).handle(null); - return null; - }); - - givenBidderRespondingWithBids(RUBICON, builder -> builder.noCookie(true).numBids(1), "bidId1"); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateRequestTypeMetric(eq(MetricName.legacy), eq(MetricName.ok)); - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyBoolean(), eq(1)); - verify(metrics).updateImpTypesMetrics(singletonMap("banner", 1L)); - verify(metrics).updateAccountRequestMetrics(eq("accountId"), eq(MetricName.legacy)); - verify(metrics).updateRequestTimeMetric(anyLong()); - verify(metrics).updateAdapterRequestGotbidsMetrics(eq(RUBICON), eq("accountId")); - verify(metrics).updateAdapterRequestTypeAndNoCookieMetrics(eq(RUBICON), eq(MetricName.legacy), eq(true)); - verify(metrics).updateAdapterResponseTime(eq(RUBICON), eq("accountId"), eq(100)); - verify(metrics).updateAdapterBidMetrics(eq(RUBICON), eq("accountId"), eq(5670L), eq(false), eq("banner")); - } - - @Test - public void shouldIncrementNoBidMetrics() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - givenBidderRespondingWithBids(RUBICON, builder -> builder.noBid(true)); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateAdapterRequestNobidMetrics(eq(RUBICON), eq("accountId")); - } - - @Test - public void shouldIncrementSafariAndNoCookieMetrics() { - // given - givenPreBidRequestContext(identity(), builder -> builder.noLiveUids(true)); - - httpRequest.headers().add(HttpUtil.USER_AGENT_HEADER, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) " - + "AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7"); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), eq(true), anyInt()); - } - - @Test - public void shouldIncrementErrorMetricIfRequestBodyHasUnknownAccountId() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - given(applicationSettings.getAccountById(any(), any())) - .willReturn(Future.failedFuture(new PreBidException("Not found"))); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateRequestTypeMetric(eq(MetricName.legacy), eq(MetricName.badinput)); - } - - @Test - public void shouldIncrementErrorMetricIfRequestIsNotValid() { - // given - given(preBidRequestContextFactory.fromRequest(any())) - .willReturn(Future.failedFuture(new PreBidException("Could not create"))); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateRequestTypeMetric(eq(MetricName.legacy), eq(MetricName.badinput)); - } - - @Test - public void shouldIncrementErrorMetricIfAdapterReturnsBadInputError() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - givenBidderRespondingWithError(RUBICON, BidderError.badInput("rubicon error")); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateAdapterRequestErrorMetric(eq(RUBICON), eq(MetricName.badinput)); - } - - @Test - public void shouldIncrementErrorMetricIfAdapterReturnsBadServerResponseError() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - givenBidderRespondingWithError(RUBICON, BidderError.badServerResponse("rubicon error")); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateAdapterRequestErrorMetric(eq(RUBICON), eq(MetricName.badserverresponse)); - } - - @Test - public void shouldIncrementErrorMetricIfAdapterReturnsGenericError() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - givenBidderRespondingWithError(RUBICON, BidderError.generic("rubicon error")); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateAdapterRequestErrorMetric(eq(RUBICON), eq(MetricName.unknown_error)); - } - - @Test - public void shouldIncrementErrorMetricIfAdapterReturnsTimeoutError() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - givenBidderRespondingWithError(RUBICON, BidderError.timeout("time out")); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateAdapterRequestErrorMetric(eq(RUBICON), eq(MetricName.timeout)); - } - - @Test - public void shouldIncrementErrorMetricIfCacheServiceFails() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(builder -> builder.cacheMarkup(1)); - - givenBidderRespondingWithBids(RUBICON, identity(), "bidId1"); - - given(cacheService.cacheBids(anyList(), any(), anyString())).willReturn(Future.failedFuture("http exception")); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateRequestTypeMetric(eq(MetricName.legacy), eq(MetricName.err)); - } - - @SuppressWarnings("unchecked") - @Test - public void shouldUpdateNetworkErrorMetric() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - givenBidderRespondingWithBids(RUBICON, identity(), "bidId1"); - - // simulate calling exception handler that is supposed to update networkerr timer value - given(httpResponse.exceptionHandler(any())).willAnswer(inv -> { - ((Handler) inv.getArgument(0)).handle(new RuntimeException()); - return httpResponse; - }); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateRequestTypeMetric(eq(MetricName.legacy), eq(MetricName.networkerr)); - } - - @Test - public void shouldNotUpdateNetworkErrorMetricIfResponseSucceeded() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - givenBidderRespondingWithBids(RUBICON, identity(), "bidId1"); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics, never()).updateRequestTypeMetric(eq(MetricName.legacy), eq(MetricName.networkerr)); - } - - @Test - public void shouldUpdateNetworkErrorMetricIfClientClosedConnection() { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - givenBidderRespondingWithBids(RUBICON, identity(), "bidId1"); - - given(routingContext.response().closed()).willReturn(true); - - // when - auctionHandler.handle(routingContext); - - // then - verify(metrics).updateRequestTypeMetric(eq(MetricName.legacy), eq(MetricName.networkerr)); - } - - @Test - public void shouldRespondWithNoUsersyncInfoForAllBiddersIfHostVendorDeniesGdpr() throws IOException { - // given - givenPreBidRequestContextWith2AdUnitsAnd2BidsEach(identity()); - - given(tcfDefinerService.resultForVendorIds(anySet(), any())) - .willReturn(Future.succeededFuture( - TcfResponse.of(true, singletonMap(1, actionWithUserSync(true)), null))); - - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(RUBICON).responseTimeMs(100) - .usersync(UsersyncInfo.of("url1", "type1", null)) - .build(), emptyList(), null))) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(APPNEXUS).responseTimeMs(100) - .usersync(UsersyncInfo.of("url2", "type2", null)) - .build(), emptyList(), null))); - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getBidderStatus()).extracting(BidderStatus::getUsersync) - .containsOnly(null, null); - } - - @Test - public void shouldRespondWithNoUsersyncInfoForBidderRestrictedByGdpr() throws IOException { - // given - givenPreBidRequestContextWith2AdUnitsAnd2BidsEach(identity()); - - final Map vendorToAction = new HashMap<>(); - vendorToAction.put(1, actionWithUserSync(false)); // host vendor id from app config - vendorToAction.put(15, actionWithUserSync(false)); // Rubicon bidder - vendorToAction.put(20, actionWithUserSync(true)); // Appnexus bidder - given(tcfDefinerService.resultForVendorIds(anySet(), any())) - .willReturn(Future.succeededFuture(TcfResponse.of(true, vendorToAction, null))); - - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(RUBICON).responseTimeMs(100) - .usersync(UsersyncInfo.of("url1", "type1", null)) - .build(), emptyList(), null))) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(APPNEXUS).responseTimeMs(100) - .usersync(UsersyncInfo.of("url2", "type2", null)) - .build(), emptyList(), null))); - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getBidderStatus()).extracting(BidderStatus::getUsersync) - .containsOnly(UsersyncInfo.of("url1", "type1", null), null); - } - - @Test - public void shouldRespondWithUsersyncInfoForBiddersButNotForHostVendor() throws IOException { - // given - givenPreBidRequestContextWith1AdUnitAndOneBid(identity()); - - final Map vendorToAction = new HashMap<>(); - vendorToAction.put(1, actionWithUserSync(false)); // host vendor id from app config - vendorToAction.put(15, actionWithUserSync(false)); // Rubicon bidder - given(tcfDefinerService.resultForVendorIds(anySet(), any())) - .willReturn(Future.succeededFuture(TcfResponse.of(true, vendorToAction, null))); - - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(RUBICON).responseTimeMs(100) - .usersync(UsersyncInfo.of("url1", "type1", null)) - .build(), emptyList(), null))); - - auctionHandler = new AuctionHandler( - applicationSettings, - bidderCatalog, - preBidRequestContextFactory, - cacheService, - metrics, - httpAdapterConnector, - clock, - tcfDefinerService, - privacyEnforcementService, - jacksonMapper, - 1); - - // when - auctionHandler.handle(routingContext); - - // then - final PreBidResponse preBidResponse = capturePreBidResponse(); - assertThat(preBidResponse.getBidderStatus()).extracting(BidderStatus::getUsersync) - .containsOnly(UsersyncInfo.of("url1", "type1", null)); - } - - private void givenPreBidRequestContextWith1AdUnitAndOneBid( - Function preBidRequestBuilderCustomizer) { - - final List adapterRequests = singletonList(AdapterRequest.of(RUBICON, singletonList(null))); - givenPreBidRequestContext(preBidRequestBuilderCustomizer, builder -> builder.adapterRequests(adapterRequests)); - } - - private void givenPreBidRequestContextWith2AdUnitsAnd2BidsEach( - Function preBidRequestContextBuilderCustomizer) { - final List adUnitBids = asList(null, null); - final List adapterRequests = asList( - AdapterRequest.of(RUBICON, adUnitBids), - AdapterRequest.of(APPNEXUS, adUnitBids)); - givenPreBidRequestContext(identity(), - preBidRequestContextBuilderCustomizer.compose(builder -> builder.adapterRequests(adapterRequests))); - } - - private void givenPreBidRequestContext( - Function preBidRequestBuilderCustomizer, - Function preBidRequestContextBuilderCustomizer) { - - final PreBidRequest preBidRequest = preBidRequestBuilderCustomizer.apply( - PreBidRequest.builder() - .tid("tid") - .accountId("accountId") - .adUnits(emptyList())) - .build(); - final PreBidRequestContext preBidRequestContext = preBidRequestContextBuilderCustomizer.apply( - PreBidRequestContext.builder() - .adapterRequests(emptyList()) - .preBidRequest(preBidRequest)) - .build(); - given(preBidRequestContextFactory.fromRequest(any())).willReturn(Future.succeededFuture(preBidRequestContext)); - } - - @SuppressWarnings("SameParameterValue") - private void givenBidderRespondingWithBids(String bidder, Function - bidderStatusBuilderCustomizer, String... bidIds) { - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.succeededFuture(AdapterResponse.of( - bidderStatusBuilderCustomizer.apply(BidderStatus.builder() - .bidder(bidder) - .responseTimeMs(100)) - .build(), - Arrays.stream(bidIds) - .map(id -> org.prebid.server.proto.response.Bid.builder() - .bidId(id) - .price(new BigDecimal("5.67")) - .mediaType(MediaType.banner) - .build()) - .collect(Collectors.toList()), - null))); - } - - @SuppressWarnings("SameParameterValue") - private void givenBidderRespondingWithError(String bidder, BidderError error) { - given(httpAdapterConnector.call(any(), any(), any(), any())) - .willReturn(Future.succeededFuture(AdapterResponse.of( - BidderStatus.builder().bidder(bidder).responseTimeMs(500).error(error.getMessage()).build(), - emptyList(), error))); - } - - private PreBidResponse capturePreBidResponse() throws IOException { - final ArgumentCaptor preBidResponseCaptor = ArgumentCaptor.forClass(String.class); - verify(httpResponse).end(preBidResponseCaptor.capture()); - return mapper.readValue(preBidResponseCaptor.getValue(), PreBidResponse.class); - } - - private static BidderInfo givenBidderInfo(int gdprVendorId) { - return new BidderInfo(true, null, null, null, - new BidderInfo.GdprInfo(gdprVendorId, true), true, false); - } - - private static PrivacyEnforcementAction actionWithUserSync(boolean blockPixelSync) { - return PrivacyEnforcementAction.builder().blockPixelSync(blockPixelSync).build(); - } -} diff --git a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java index 17911e2959f..d0bcd6bb14e 100644 --- a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java @@ -1,6 +1,7 @@ package org.prebid.server.handler; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.TextNode; import io.netty.util.AsciiString; import io.vertx.core.Future; import io.vertx.core.buffer.Buffer; @@ -14,7 +15,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.CookieSyncEvent; import org.prebid.server.auction.PrivacyEnforcementService; import org.prebid.server.bidder.BidderCatalog; @@ -39,7 +40,9 @@ import org.prebid.server.proto.response.UsersyncInfo; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountCookieSyncConfig; import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; import org.prebid.server.settings.model.EnabledForRequestType; import java.io.IOException; @@ -99,7 +102,7 @@ public class CookieSyncHandlerTest extends VertxTest { @Mock private PrivacyEnforcementService privacyEnforcementService; @Mock - private AnalyticsReporter analyticsReporter; + private AnalyticsReporterDelegator analyticsReporterDelegator; @Mock private Metrics metrics; @@ -115,11 +118,12 @@ public class CookieSyncHandlerTest extends VertxTest { @Before public void setUp() { - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(routingContext.response()).willReturn(httpResponse); - given(httpResponse.putHeader(any(CharSequence.class), any(CharSequence.class))).willReturn(httpResponse); + given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + given(httpResponse.putHeader(any(CharSequence.class), any(AsciiString.class))).willReturn(httpResponse); given(privacyEnforcementService.contextFromCookieSyncRequest(any(), any(), any(), any())) .willReturn(Future.succeededFuture(PrivacyContext.of( @@ -137,66 +141,72 @@ public void setUp() { 1, false, emptyList(), - analyticsReporter, + analyticsReporterDelegator, metrics, timeoutFactory, jacksonMapper); } @Test - public void shouldRespondWithErrorIfOptedOut() { + public void shouldRespondWithErrorAndSendToAnalyticsWithoutTcfWhenRequestBodyIsMissing() { // given - given(uidsCookieService.parseFromRequest(any())) - .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).optout(true).build(), jacksonMapper)); - - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - given(httpResponse.setStatusMessage(anyString())).willReturn(httpResponse); + given(routingContext.getBody()).willReturn(null); // when cookieSyncHandler.handle(routingContext); // then - verify(httpResponse).setStatusCode(eq(401)); - verify(httpResponse).setStatusMessage(eq("User has opted out")); - verify(httpResponse).end(); - verify(routingContext, never()).getBody(); + verify(httpResponse).closed(); + verify(httpResponse).setStatusCode(400); + verify(httpResponse).end("Invalid request format: Request has no body"); + verify(metrics).updateUserSyncBadRequestMetric(); verifyNoMoreInteractions(httpResponse, tcfDefinerService); + + final CookieSyncEvent cookieSyncEvent = captureCookieSyncEvent(); + assertThat(cookieSyncEvent) + .isEqualTo(CookieSyncEvent.error(400, "Invalid request format: Request has no body")); } @Test - public void shouldRespondWithErrorIfRequestBodyIsMissing() { + public void shouldRespondWithErrorAndSendToAnalyticsWithoutTcfWhenRequestBodyCouldNotBeParsed() { // given - given(routingContext.getBody()).willReturn(null); - - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - given(httpResponse.setStatusMessage(anyString())).willReturn(httpResponse); + given(routingContext.getBody()).willReturn(Buffer.buffer("{")); // when cookieSyncHandler.handle(routingContext); // then - verify(httpResponse).setStatusCode(eq(400)); - verify(httpResponse).setStatusMessage(eq("Request has no body")); - verify(httpResponse).end(); + verify(httpResponse).closed(); + verify(httpResponse).setStatusCode(400); + verify(httpResponse).end("Invalid request format: Request body cannot be parsed"); + verify(metrics).updateUserSyncBadRequestMetric(); verifyNoMoreInteractions(httpResponse, tcfDefinerService); + + final CookieSyncEvent cookieSyncEvent = captureCookieSyncEvent(); + assertThat(cookieSyncEvent) + .isEqualTo(CookieSyncEvent.error(400, "Invalid request format: Request body cannot be parsed")); } @Test - public void shouldRespondWithErrorIfRequestBodyCouldNotBeParsed() { + public void shouldRespondWithErrorAndSendToAnalyticsWithTcfWhenOptedOut() { // given - given(routingContext.getBody()).willReturn(Buffer.buffer("{")); - - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - given(httpResponse.setStatusMessage(anyString())).willReturn(httpResponse); + given(routingContext.getBody()) + .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(emptyList()).gdpr(1).build())); + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) + .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).optout(true).build(), jacksonMapper)); // when cookieSyncHandler.handle(routingContext); // then - verify(httpResponse).setStatusCode(eq(400)); - verify(httpResponse).setStatusMessage(eq("Request body cannot be parsed")); - verify(httpResponse).end(); + verify(httpResponse).closed(); + verify(httpResponse).setStatusCode(401); + verify(httpResponse).end("Unauthorized: Sync is not allowed for this uids"); verifyNoMoreInteractions(httpResponse, tcfDefinerService); + + final CookieSyncEvent cookieSyncEvent = captureCookieSyncTcfEvent(); + assertThat(cookieSyncEvent) + .isEqualTo(CookieSyncEvent.error(401, "Unauthorized: Sync is not allowed for this uids")); } @Test @@ -205,17 +215,42 @@ public void shouldRespondWithErrorIfGdprConsentIsMissing() { given(routingContext.getBody()) .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(emptyList()).gdpr(1).build())); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - given(httpResponse.setStatusMessage(anyString())).willReturn(httpResponse); + // when + cookieSyncHandler.handle(routingContext); + + // then + verify(httpResponse).closed(); + verify(httpResponse).setStatusCode(400); + verify(httpResponse).end("Invalid request format: gdpr_consent is required if gdpr is 1"); + verify(metrics).updateUserSyncBadRequestMetric(); + verifyNoMoreInteractions(httpResponse, tcfDefinerService); + + final CookieSyncEvent cookieSyncEvent = captureCookieSyncTcfEvent(); + assertThat(cookieSyncEvent) + .isEqualTo(CookieSyncEvent.error(400, "Invalid request format: gdpr_consent is required if gdpr is 1")); + } + + @Test + public void shouldRespondWithBadRequestStatusIfGdprConsentIsInvalid() { + // given + given(routingContext.getBody()) + .willReturn(givenRequestBody(CookieSyncRequest.builder() + .bidders(singletonList(RUBICON)) + .gdpr(1) + .gdprConsent("invalid") + .build())); + + given(privacyEnforcementService.contextFromCookieSyncRequest(any(), any(), any(), any())) + .willReturn(Future.succeededFuture(PrivacyContext.of(null, + TcfContext.builder().gdpr("1").isConsentValid(false).build()))); // when cookieSyncHandler.handle(routingContext); // then + verify(metrics).updateUserSyncTcfInvalidMetric(); verify(httpResponse).setStatusCode(eq(400)); - verify(httpResponse).setStatusMessage(eq("gdpr_consent is required if gdpr is 1")); - verify(httpResponse).end(); - verifyNoMoreInteractions(httpResponse, tcfDefinerService); + verify(httpResponse).end(eq("Invalid request format: Consent string is invalid")); } @Test @@ -248,7 +283,7 @@ public void shouldRespondWithExpectedHeaders() { // then verify(httpResponse) - .putHeader(eq(new AsciiString("Content-Type")), eq(new AsciiString("application/json"))); + .putHeader(new AsciiString("Content-Type"), new AsciiString("application/json")); } @Test @@ -259,10 +294,14 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsFound() { final AccountGdprConfig accountGdprConfig = AccountGdprConfig.builder() .enabledForRequestType(EnabledForRequestType.of(true, true, true, true)).build(); - final Account account = Account.builder().gdpr(accountGdprConfig).build(); + final Account account = Account.builder() + .privacy(AccountPrivacyConfig.of(null, accountGdprConfig, null)) + .build(); given(applicationSettings.getAccountById(any(), any())).willReturn(Future.succeededFuture(account)); givenTcfServiceReturningVendorIdResult(singleton(1)); + given(privacyEnforcementService.contextFromCookieSyncRequest(any(), any(), any(), any())) + .willReturn(Future.failedFuture("fail")); // when cookieSyncHandler.handle(routingContext); @@ -282,6 +321,8 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsNotFound() given(applicationSettings.getAccountById(any(), any())).willReturn(Future.failedFuture("bad")); givenTcfServiceReturningVendorIdResult(singleton(1)); + given(privacyEnforcementService.contextFromCookieSyncRequest(any(), any(), any(), any())) + .willReturn(Future.failedFuture("fail")); // when cookieSyncHandler.handle(routingContext); @@ -296,7 +337,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsNotFound() @Test public void shouldRespondWithSomeBidderStatusesIfSomeUidsMissingInCookies() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(singletonMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"))).build(), jacksonMapper)); @@ -305,8 +346,8 @@ public void shouldRespondWithSomeBidderStatusesIfSomeUidsMissingInCookies() thro given(bidderCatalog.isActive(anyString())).willReturn(true); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", null, null, "redirect", false); - rubiconUsersyncer = new Usersyncer(RUBICON, "url", null, null, "redirect", false); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", "redirect"); + rubiconUsersyncer = createUsersyncer(RUBICON, "url", "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -330,19 +371,18 @@ public void shouldRespondWithAllActiveBiddersWhenRequestCoopSyncTrueAndNoPriorit // given final String disabledBidder = "disabled_bidder"; given(bidderCatalog.names()).willReturn(new HashSet<>(asList(APPNEXUS, RUBICON, disabledBidder))); - given(bidderCatalog.isActive(anyString())).willReturn(true); given(bidderCatalog.isActive(disabledBidder)).willReturn(false); cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, bidderCatalog, tcfDefinerService, privacyEnforcementService, 1, false, emptyList(), - analyticsReporter, metrics, timeoutFactory, jacksonMapper); + analyticsReporterDelegator, metrics, timeoutFactory, jacksonMapper); given(routingContext.getBody()).willReturn(givenRequestBody( CookieSyncRequest.builder().bidders(singletonList(APPNEXUS)).coopSync(true).build())); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", null, null, "redirect", false); - rubiconUsersyncer = new Usersyncer(RUBICON, "http://rubiconexample.com", null, null, "redirect", false); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", "redirect"); + rubiconUsersyncer = createUsersyncer(RUBICON, "http://rubiconexample.com", "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -372,13 +412,13 @@ public void shouldRespondWithCoopBiddersWhenRequestCoopSyncTrue() throws IOExcep cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, bidderCatalog, tcfDefinerService, privacyEnforcementService, 1, false, coopBidders, - analyticsReporter, metrics, timeoutFactory, jacksonMapper); + analyticsReporterDelegator, metrics, timeoutFactory, jacksonMapper); given(routingContext.getBody()).willReturn(givenRequestBody( CookieSyncRequest.builder().bidders(singletonList(APPNEXUS)).coopSync(true).build())); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", null, null, "redirect", false); - rubiconUsersyncer = new Usersyncer(RUBICON, "http://rubiconexample.com", null, null, "redirect", false); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", "redirect"); + rubiconUsersyncer = createUsersyncer(RUBICON, "http://rubiconexample.com", "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -397,6 +437,49 @@ public void shouldRespondWithCoopBiddersWhenRequestCoopSyncTrue() throws IOExcep UsersyncInfo.of("http://rubiconexample.com", "redirect", false)).build()))); } + @Test + public void shouldRespondWithCoopBiddersWhenAccountCoopSyncTrue() throws IOException { + // given + given(bidderCatalog.isActive(anyString())).willReturn(true); + + final List> coopBidders = singletonList(singletonList(RUBICON)); + + cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, + bidderCatalog, tcfDefinerService, privacyEnforcementService, 1, false, coopBidders, + analyticsReporterDelegator, metrics, timeoutFactory, jacksonMapper); + + given(routingContext.getBody()).willReturn(givenRequestBody( + CookieSyncRequest.builder() + .bidders(singletonList(APPNEXUS)) + .account("account") + .build())); + + given(applicationSettings.getAccountById(anyString(), any())).willReturn(Future.succeededFuture( + Account.builder() + .cookieSync(AccountCookieSyncConfig.of(null, null, true)) + .build())); + + appnexusUsersyncer = Usersyncer.of(APPNEXUS_COOKIE, + Usersyncer.UsersyncMethod.of("redirect", "http://adnxsexample.com", null, false), null); + rubiconUsersyncer = Usersyncer.of(RUBICON, + Usersyncer.UsersyncMethod.of("redirect", "http://rubiconexample.com", null, false), null); + givenUsersyncersReturningFamilyName(); + + givenTcfServiceReturningVendorIdResult(singleton(1)); + givenTcfServiceReturningBidderNamesResult(set(RUBICON, APPNEXUS)); + + // when + cookieSyncHandler.handle(routingContext); + + // then + final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); + assertThat(cookieSyncResponse).isEqualTo(CookieSyncResponse.of("no_cookie", asList( + BidderUsersyncStatus.builder().bidder(APPNEXUS).noCookie(true).usersync( + UsersyncInfo.of("http://adnxsexample.com", "redirect", false)).build(), + BidderUsersyncStatus.builder().bidder(RUBICON).noCookie(true).usersync( + UsersyncInfo.of("http://rubiconexample.com", "redirect", false)).build()))); + } + @Test public void shouldRespondWithPrioritisedCoopBidderWhenRequestCoopDefaultTrueAndLimitIsLessThanCoopSize() throws IOException { @@ -408,13 +491,13 @@ public void shouldRespondWithPrioritisedCoopBidderWhenRequestCoopDefaultTrueAndL cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, bidderCatalog, tcfDefinerService, privacyEnforcementService, 1, true, priorityBidders, - analyticsReporter, metrics, timeoutFactory, jacksonMapper); + analyticsReporterDelegator, metrics, timeoutFactory, jacksonMapper); given(routingContext.getBody()).willReturn(givenRequestBody( CookieSyncRequest.builder().bidders(singletonList(APPNEXUS)).limit(2).build())); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", null, null, "redirect", false); - rubiconUsersyncer = new Usersyncer(RUBICON, "http://rubiconexample.com", null, null, "redirect", false); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", "redirect"); + rubiconUsersyncer = createUsersyncer(RUBICON, "http://rubiconexample.com", "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -447,10 +530,10 @@ public void shouldRespondWithBidderStatusForAllBiddersIfBiddersListOmittedInRequ cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, bidderCatalog, tcfDefinerService, privacyEnforcementService, 1, false, emptyList(), - analyticsReporter, metrics, timeoutFactory, jacksonMapper); + analyticsReporterDelegator, metrics, timeoutFactory, jacksonMapper); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", null, null, "redirect", false); - rubiconUsersyncer = new Usersyncer(RUBICON, "http://rubiconexample.com", null, null, "redirect", false); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", "redirect"); + rubiconUsersyncer = createUsersyncer(RUBICON, "http://rubiconexample.com", "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -474,7 +557,7 @@ public void shouldRespondWithNoBidderStatusesIfAllUidsPresentInCookies() throws final Map uids = new HashMap<>(); uids.put(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT")); uids.put(APPNEXUS_COOKIE, UidWithExpiry.live("12345")); - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(uids).build(), jacksonMapper)); given(bidderCatalog.isActive(anyString())).willReturn(true); @@ -482,8 +565,8 @@ public void shouldRespondWithNoBidderStatusesIfAllUidsPresentInCookies() throws given(routingContext.getBody()) .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).build())); - rubiconUsersyncer = new Usersyncer(RUBICON, "", null, null, null, false); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "", null, null, null, false); + rubiconUsersyncer = createUsersyncer(RUBICON, "", null); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "", null); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -500,14 +583,14 @@ public void shouldRespondWithNoBidderStatusesIfAllUidsPresentInCookies() throws @Test public void shouldTolerateUnsupportedBidder() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(singletonMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"))).build(), jacksonMapper)); given(routingContext.getBody()).willReturn(givenRequestBody( CookieSyncRequest.builder().bidders(asList(RUBICON, "unsupported")).build())); - rubiconUsersyncer = new Usersyncer(RUBICON, "", null, null, null, false); + rubiconUsersyncer = createUsersyncer(RUBICON, "", null); givenUsersyncersReturningFamilyName(); given(bidderCatalog.isActive(RUBICON)).willReturn(true); @@ -530,14 +613,14 @@ public void shouldTolerateUnsupportedBidder() throws IOException { @Test public void shouldTolerateDisabledBidder() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(singletonMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"))).build(), jacksonMapper)); given(routingContext.getBody()) .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(asList(RUBICON, "disabled")).build())); - rubiconUsersyncer = new Usersyncer(RUBICON, "", null, null, null, false); + rubiconUsersyncer = createUsersyncer(RUBICON, "", null); givenUsersyncersReturningFamilyName(); given(bidderCatalog.isValidName(RUBICON)).willReturn(true); @@ -566,23 +649,22 @@ public void shouldTolerateDisabledBidder() throws IOException { @Test public void shouldTolerateRejectedBidderByTcf() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(singletonMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"))).build(), jacksonMapper)); given(routingContext.getBody()) .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).build())); - rubiconUsersyncer = new Usersyncer(RUBICON, "", null, null, null, false); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "", null, null, null, false); + rubiconUsersyncer = createUsersyncer(RUBICON, "", null); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "", null); givenUsersyncersReturningFamilyName(); given(bidderCatalog.isActive(RUBICON)).willReturn(true); given(bidderCatalog.isActive(APPNEXUS)).willReturn(true); given(bidderCatalog.bidderInfoByName(APPNEXUS)) - .willReturn(BidderInfo.create(true, null, null, - null, null, 2, true, true, false)); + .willReturn(BidderInfo.create(true, null, null, null, null, null, null, 2, true, true, false)); givenTcfServiceReturningVendorIdResult(singleton(1)); givenTcfServiceReturningBidderNamesResult(singleton(RUBICON)); @@ -601,7 +683,7 @@ public void shouldTolerateRejectedBidderByTcf() throws IOException { @Test public void shouldTolerateBiddersWithoutUsersyncUrl() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(singletonMap("notRelevantBidder", UidWithExpiry.live("J5VLCWQP-26-CWFT"))).build(), jacksonMapper)); @@ -610,8 +692,8 @@ public void shouldTolerateBiddersWithoutUsersyncUrl() throws IOException { given(routingContext.getBody()) .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).build())); - rubiconUsersyncer = new Usersyncer(RUBICON, "", null, null, null, false); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "", null, null, null, false); + rubiconUsersyncer = createUsersyncer(RUBICON, "", null); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "", null); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -625,22 +707,60 @@ public void shouldTolerateBiddersWithoutUsersyncUrl() throws IOException { assertThat(cookieSyncResponse).isEqualTo(CookieSyncResponse.of("ok", emptyList())); } + @Test + public void shouldSkipVendorHostCheckAndContinueWithBiddersCheckWhenHostVendorIdIsMissing() throws IOException { + // given + cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, + bidderCatalog, tcfDefinerService, privacyEnforcementService, null, false, emptyList(), + analyticsReporterDelegator, metrics, timeoutFactory, jacksonMapper); + + final Uids uids = Uids.builder().uids(singletonMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"))).build(); + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) + .willReturn(new UidsCookie(uids, jacksonMapper)); + + given(routingContext.getBody()) + .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).build())); + + rubiconUsersyncer = createUsersyncer(RUBICON, "", null); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "", null); + givenUsersyncersReturningFamilyName(); + + given(bidderCatalog.isActive(RUBICON)).willReturn(true); + given(bidderCatalog.isActive(APPNEXUS)).willReturn(true); + + given(bidderCatalog.bidderInfoByName(APPNEXUS)) + .willReturn(BidderInfo.create(true, null, null, null, + null, null, null, 2, true, true, false)); + + givenTcfServiceReturningBidderNamesResult(singleton(RUBICON)); + + // when + cookieSyncHandler.handle(routingContext); + + // then + verify(tcfDefinerService, never()).resultForVendorIds(anySet(), any()); + final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); + assertThat(cookieSyncResponse.getStatus()).isEqualTo("ok"); + assertThat(cookieSyncResponse.getBidderStatus()).hasSize(1) + .extracting(BidderUsersyncStatus::getBidder, BidderUsersyncStatus::getError) + .containsOnly(tuple(APPNEXUS, "Rejected by TCF")); + } + @Test public void shouldUpdateCookieSyncSetAndRejectByTcfMetricForEachRejectedAndSyncedBidder() { // given given(routingContext.getBody()) .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).build())); - rubiconUsersyncer = new Usersyncer(RUBICON, "", null, null, null, false); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "", null, null, null, false); + rubiconUsersyncer = createUsersyncer(RUBICON, "", null); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "", null); givenUsersyncersReturningFamilyName(); given(bidderCatalog.isActive(RUBICON)).willReturn(true); given(bidderCatalog.isActive(APPNEXUS)).willReturn(true); given(bidderCatalog.bidderInfoByName(APPNEXUS)) - .willReturn(BidderInfo.create(true, null, null, - null, null, 2, true, true, false)); + .willReturn(BidderInfo.create(true, null, null, null, null, null, null, 2, true, true, false)); givenTcfServiceReturningVendorIdResult(singleton(1)); givenTcfServiceReturningBidderNamesResult(singleton(RUBICON)); @@ -656,15 +776,15 @@ public void shouldUpdateCookieSyncSetAndRejectByTcfMetricForEachRejectedAndSynce @Test public void shouldUpdateCookieSyncMatchesMetricForEachAlreadySyncedBidder() { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(singletonMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"))).build(), jacksonMapper)); given(routingContext.getBody()) .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).build())); - rubiconUsersyncer = new Usersyncer(RUBICON, "url", null, null, null, false); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "url", null, null, null, false); + rubiconUsersyncer = createUsersyncer(RUBICON, "url", null); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "url", null); givenUsersyncersReturningFamilyName(); given(bidderCatalog.isActive(RUBICON)).willReturn(true); @@ -685,23 +805,23 @@ public void shouldUpdateCookieSyncMatchesMetricForEachAlreadySyncedBidder() { public void shouldRespondWithNoCookieStatusIfHostVendorRejectedByTcf() throws IOException { // given cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, - bidderCatalog, tcfDefinerService, privacyEnforcementService, null, false, emptyList(), - analyticsReporter, metrics, timeoutFactory, jacksonMapper); + bidderCatalog, tcfDefinerService, privacyEnforcementService, 1, false, emptyList(), + analyticsReporterDelegator, metrics, timeoutFactory, jacksonMapper); - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(routingContext.getBody()) .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).build())); - rubiconUsersyncer = new Usersyncer(RUBICON, "", null, null, null, false); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "", null, null, null, false); + rubiconUsersyncer = createUsersyncer(RUBICON, "", null); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "", null); givenUsersyncersReturningFamilyName(); given(bidderCatalog.isActive(RUBICON)).willReturn(true); given(bidderCatalog.isActive(APPNEXUS)).willReturn(true); - givenTcfServiceReturningVendorIdResult(singleton(1)); + givenTcfServiceReturningBlockedVendorIdResult(set(1)); givenTcfServiceReturningBidderNamesResult(set(RUBICON, APPNEXUS)); // when @@ -720,7 +840,7 @@ bidderCatalog, tcfDefinerService, privacyEnforcementService, null, false, emptyL @Test public void shouldRespondWithNoCookieStatusIfNoLiveUids() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(singletonMap(APPNEXUS_COOKIE, UidWithExpiry.expired("12345"))).build(), jacksonMapper)); @@ -729,7 +849,7 @@ public void shouldRespondWithNoCookieStatusIfNoLiveUids() throws IOException { given(bidderCatalog.isActive(anyString())).willReturn(true); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", null, null, "redirect", false); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -765,9 +885,10 @@ public void shouldRespondWithExpectedUsersyncInfo() throws IOException { given(bidderCatalog.isActive(anyString())).willReturn(true); - appnexusUsersyncer = new Usersyncer( - APPNEXUS_COOKIE, "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", null, null, - "redirect", false); + appnexusUsersyncer = createUsersyncer( + APPNEXUS_COOKIE, + "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", + "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -783,6 +904,43 @@ public void shouldRespondWithExpectedUsersyncInfo() throws IOException { .containsOnly("http://adnxsexample.com/sync?gdpr=1&gdpr_consent=gdpr_consent1"); } + @Test + public void shouldRespondWithUsersyncMethodAllowedByRequest() throws IOException { + // given + given(routingContext.getBody()) + .willReturn(givenRequestBody(CookieSyncRequest.builder() + .bidders(singletonList(RUBICON)) + .filterSettings(CookieSyncRequest.FilterSettings.of( + CookieSyncRequest.MethodFilter.of( + new TextNode("*"), + CookieSyncRequest.FilterType.exclude), + null)) + .build())); + + given(bidderCatalog.isActive(anyString())).willReturn(true); + + rubiconUsersyncer = Usersyncer.of( + RUBICON, + Usersyncer.UsersyncMethod.of("iframe", "iframe-url", null, false), + Usersyncer.UsersyncMethod.of("redirect", "redirect-url", null, false)); + givenUsersyncersReturningFamilyName(); + + givenTcfServiceReturningVendorIdResult(singleton(1)); + givenTcfServiceReturningBidderNamesResult(set(RUBICON)); + + // when + cookieSyncHandler.handle(routingContext); + + // then + final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); + assertThat(cookieSyncResponse).isEqualTo(CookieSyncResponse.of("no_cookie", + singletonList(BidderUsersyncStatus.builder() + .bidder(RUBICON) + .noCookie(true) + .usersync(UsersyncInfo.of("redirect-url", "redirect", false)) + .build()))); + } + @Test public void shouldRespondWithUpdatedUsersyncInfoIfHostCookieAndUidsDiffers() throws IOException { // given @@ -795,7 +953,7 @@ public void shouldRespondWithUpdatedUsersyncInfoIfHostCookieAndUidsDiffers() thr TcfContext.empty()))); given(bidderCatalog.isActive(RUBICON)).willReturn(true); - rubiconUsersyncer = new Usersyncer(RUBICON, "http://rubiconexample.com", null, null, "redirect", false); + rubiconUsersyncer = createUsersyncer(RUBICON, "http://rubiconexample.com", "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -815,7 +973,7 @@ public void shouldRespondWithUpdatedUsersyncInfoIfHostCookieAndUidsDiffers() thr .extracting(BidderUsersyncStatus::getUsersync) .containsOnly( UsersyncInfo.of("http://external-url/setuid?bidder=rubicon&gdpr=&gdpr_consent=&us_privacy=" - + "&uid=host%2Fcookie%2Fvalue", "redirect", false)); + + "&f=i&uid=host%2Fcookie%2Fvalue", "redirect", false)); } @Test @@ -835,9 +993,10 @@ public void shouldRespondWithOriginalUsersyncInfoIfNoHostCookieFamilyInBiddersCo TcfContext.empty()))); given(bidderCatalog.isActive(APPNEXUS)).willReturn(true); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, + appnexusUsersyncer = createUsersyncer( + APPNEXUS_COOKIE, "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}", - null, null, "redirect", false); + "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -865,7 +1024,7 @@ public void shouldRespondWithOriginalUsersyncInfoIfNoHostCookieInRequest() throw given(bidderCatalog.isActive(RUBICON)).willReturn(true); - rubiconUsersyncer = new Usersyncer(RUBICON, "http://rubiconexample.com", null, null, "redirect", false); + rubiconUsersyncer = createUsersyncer(RUBICON, "http://rubiconexample.com", "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -893,7 +1052,7 @@ public void shouldRespondWithOriginalUsersyncInfoIfHostCookieAndUidsAreEqual() t given(bidderCatalog.isActive(RUBICON)).willReturn(true); - rubiconUsersyncer = new Usersyncer(RUBICON, "http://rubiconexample.com", null, null, "redirect", false); + rubiconUsersyncer = createUsersyncer(RUBICON, "http://rubiconexample.com", "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -914,32 +1073,6 @@ public void shouldRespondWithOriginalUsersyncInfoIfHostCookieAndUidsAreEqual() t .containsOnly(UsersyncInfo.of("http://rubiconexample.com", "redirect", false)); } - @Test - public void shouldRespondWithExpectedUsersyncInfoForBidderAlias() throws IOException { - // given - given(routingContext.getBody()).willReturn(givenRequestBody( - CookieSyncRequest.builder().bidders(singletonList("rubiconAlias")).gdpr(0).build())); - - given(bidderCatalog.isActive(RUBICON)).willReturn(true); - given(bidderCatalog.isAlias("rubiconAlias")).willReturn(true); - given(bidderCatalog.nameByAlias("rubiconAlias")).willReturn(RUBICON); - - rubiconUsersyncer = new Usersyncer(RUBICON, "http://rubiconexample.com", null, null, "redirect", false); - givenUsersyncersReturningFamilyName(); - - givenTcfServiceReturningVendorIdResult(singleton(1)); - givenTcfServiceReturningBidderNamesResult(emptySet()); - - // when - cookieSyncHandler.handle(routingContext); - - // then - final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); - assertThat(cookieSyncResponse.getBidderStatus()) - .extracting(bidderStatus -> bidderStatus.getUsersync().getUrl()) - .containsOnly("http://rubiconexample.com"); - } - @Test public void shouldTolerateMissingTcfParamsInRequestForUsersyncInfo() throws IOException { // given @@ -954,9 +1087,10 @@ public void shouldTolerateMissingTcfParamsInRequestForUsersyncInfo() throws IOEx given(bidderCatalog.isActive(anyString())).willReturn(true); given(bidderCatalog.names()).willReturn(singleton(APPNEXUS)); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, - "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", null, null, "redirect", - false); + appnexusUsersyncer = createUsersyncer( + APPNEXUS_COOKIE, + "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", + "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -982,11 +1116,18 @@ public void shouldLimitBidderStatuses() throws IOException { given(bidderCatalog.isActive(anyString())).willReturn(true); - rubiconUsersyncer = new Usersyncer(RUBICON, - "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", - null, null, "redirect", false); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://rubiconexample.com", null, null, "redirect", - false); + given(applicationSettings.getAccountById(anyString(), any())).willReturn(Future.succeededFuture( + Account.builder() + .cookieSync(AccountCookieSyncConfig.of(5, 5, null)) + .build())); + + rubiconUsersyncer = Usersyncer.of(RUBICON, + Usersyncer.UsersyncMethod.of("redirect", + "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", + null, false), null); + appnexusUsersyncer = Usersyncer.of(APPNEXUS_COOKIE, + Usersyncer.UsersyncMethod.of("redirect", + "http://rubiconexample.com", null, false), null); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -1001,49 +1142,60 @@ public void shouldLimitBidderStatuses() throws IOException { } @Test - public void shouldLimitBidderStatusesWithLiveUids() throws IOException { + public void shouldLimitBidderStatusesWithAccountDefaultLimit() throws IOException { // given - Map liveUids = doubleMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"), - APPNEXUS_COOKIE, UidWithExpiry.live("1234567890")); - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( - Uids.builder().uids(liveUids).build(), jacksonMapper)); - - given(routingContext.getBody()).willReturn(givenRequestBody( - CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).gdpr(0).limit(1).build())); + given(routingContext.getBody()).willReturn(givenRequestBody(CookieSyncRequest.builder() + .bidders(asList(RUBICON, APPNEXUS)) + .gdpr(0) + .account("account") + .build())); given(bidderCatalog.isActive(anyString())).willReturn(true); - rubiconUsersyncer = new Usersyncer(RUBICON, - "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", - null, null, "redirect", false); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://rubiconexample.com", null, null, "redirect", - false); + given(applicationSettings.getAccountById(anyString(), any())).willReturn(Future.succeededFuture( + Account.builder() + .cookieSync(AccountCookieSyncConfig.of(1, null, null)) + .build())); + + rubiconUsersyncer = Usersyncer.of(RUBICON, + Usersyncer.UsersyncMethod.of("redirect", + "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", null, false), null); + appnexusUsersyncer = Usersyncer.of(APPNEXUS_COOKIE, + Usersyncer.UsersyncMethod.of("redirect", "http://rubiconexample.com", null, false), null); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); - givenTcfServiceReturningBidderNamesResult(set(RUBICON, APPNEXUS)); + givenTcfServiceReturningBidderNamesResult(singleton(APPNEXUS)); // when cookieSyncHandler.handle(routingContext); // then final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); - assertThat(cookieSyncResponse.getBidderStatus()).isEmpty(); + assertThat(cookieSyncResponse.getBidderStatus()).hasSize(1); } @Test - public void shouldNotLimitBidderStatusesIfLimitIsBiggerThanBiddersList() throws IOException { + public void shouldLimitBidderStatusesWithAccountMaxLimit() throws IOException { // given - given(routingContext.getBody()).willReturn(givenRequestBody( - CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).gdpr(0).limit(3).build())); + given(routingContext.getBody()).willReturn(givenRequestBody(CookieSyncRequest.builder() + .bidders(asList(RUBICON, APPNEXUS)) + .gdpr(0) + .account("account") + .build())); given(bidderCatalog.isActive(anyString())).willReturn(true); - rubiconUsersyncer = new Usersyncer(RUBICON, + given(applicationSettings.getAccountById(anyString(), any())).willReturn(Future.succeededFuture( + Account.builder() + .cookieSync(AccountCookieSyncConfig.of(5, 1, null)) + .build())); + + rubiconUsersyncer = createUsersyncer( + RUBICON, "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", - null, null, "redirect", false); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://rubiconexample.com", null, null, "redirect", - false); + "redirect"); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "http://rubiconexample.com", "redirect"); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -1054,76 +1206,85 @@ public void shouldNotLimitBidderStatusesIfLimitIsBiggerThanBiddersList() throws // then final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); - assertThat(cookieSyncResponse.getBidderStatus()).hasSize(2); + assertThat(cookieSyncResponse.getBidderStatus()).hasSize(1); } @Test - public void shouldIncrementMetrics() { + public void shouldLimitBidderStatusesWithLiveUids() throws IOException { // given - given(routingContext.getBody()).willReturn( - givenRequestBody(CookieSyncRequest.builder().bidders(emptyList()).build())); - - givenTcfServiceReturningVendorIdResult(emptySet()); + Map liveUids = doubleMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"), + APPNEXUS_COOKIE, UidWithExpiry.live("1234567890")); + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( + Uids.builder().uids(liveUids).build(), jacksonMapper)); - // when - cookieSyncHandler.handle(routingContext); + given(routingContext.getBody()).willReturn(givenRequestBody( + CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).gdpr(0).limit(1).build())); - // then - verify(metrics).updateCookieSyncRequestMetric(); - } + given(bidderCatalog.isActive(anyString())).willReturn(true); - @Test - public void shouldPassUnauthorizedEventToAnalyticsReporterIfOptedOut() { - given(uidsCookieService.parseFromRequest(any())) - .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).optout(true).build(), jacksonMapper)); + rubiconUsersyncer = createUsersyncer( + RUBICON, + "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", + "redirect"); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "http://rubiconexample.com", "redirect"); + givenUsersyncersReturningFamilyName(); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - given(httpResponse.setStatusMessage(anyString())).willReturn(httpResponse); + givenTcfServiceReturningVendorIdResult(singleton(1)); + givenTcfServiceReturningBidderNamesResult(set(RUBICON, APPNEXUS)); // when cookieSyncHandler.handle(routingContext); // then - final CookieSyncEvent cookieSyncEvent = captureCookieSyncEvent(); - assertThat(cookieSyncEvent).isEqualTo(CookieSyncEvent.error(401, "User has opted out")); + final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); + assertThat(cookieSyncResponse.getBidderStatus()).isEmpty(); } @Test - public void shouldPassBadRequestEventToAnalyticsReporterIfRequestBodyIsMissing() { + public void shouldNotLimitBidderStatusesIfLimitIsBiggerThanBiddersList() throws IOException { // given - given(routingContext.getBody()).willReturn(null); + given(routingContext.getBody()).willReturn(givenRequestBody( + CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).gdpr(0).limit(3).build())); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - given(httpResponse.setStatusMessage(anyString())).willReturn(httpResponse); + given(bidderCatalog.isActive(anyString())).willReturn(true); + + rubiconUsersyncer = createUsersyncer( + RUBICON, + "http://adnxsexample.com/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}", + "redirect"); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "http://rubiconexample.com", "redirect"); + givenUsersyncersReturningFamilyName(); + + givenTcfServiceReturningVendorIdResult(singleton(1)); + givenTcfServiceReturningBidderNamesResult(singleton(APPNEXUS)); // when cookieSyncHandler.handle(routingContext); // then - final CookieSyncEvent cookieSyncEvent = captureCookieSyncEvent(); - assertThat(cookieSyncEvent).isEqualTo(CookieSyncEvent.error(400, "Request has no body")); + final CookieSyncResponse cookieSyncResponse = captureCookieSyncResponse(); + assertThat(cookieSyncResponse.getBidderStatus()).hasSize(2); } @Test - public void shouldPassBadRequestEventToAnalyticsReporterIfRequestBodyCouldNotBeParsed() { + public void shouldIncrementMetrics() { // given - given(routingContext.getBody()).willReturn(Buffer.buffer("{")); + given(routingContext.getBody()).willReturn( + givenRequestBody(CookieSyncRequest.builder().bidders(emptyList()).build())); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - given(httpResponse.setStatusMessage(anyString())).willReturn(httpResponse); + givenTcfServiceReturningVendorIdResult(emptySet()); // when cookieSyncHandler.handle(routingContext); // then - final CookieSyncEvent cookieSyncEvent = captureCookieSyncEvent(); - assertThat(cookieSyncEvent).isEqualTo(CookieSyncEvent.error(400, "Request body cannot be parsed")); + verify(metrics).updateCookieSyncRequestMetric(); } @Test public void shouldPassSuccessfulEventToAnalyticsReporter() { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(singletonMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"))).build(), jacksonMapper)); @@ -1132,8 +1293,8 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { given(bidderCatalog.isActive(anyString())).willReturn(true); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", null, null, "redirect", false); - rubiconUsersyncer = new Usersyncer(RUBICON, "", null, null, null, false); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "http://adnxsexample.com", "redirect"); + rubiconUsersyncer = createUsersyncer(RUBICON, "", null); givenUsersyncersReturningFamilyName(); givenTcfServiceReturningVendorIdResult(singleton(1)); @@ -1143,7 +1304,7 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { cookieSyncHandler.handle(routingContext); // then - final CookieSyncEvent cookieSyncEvent = captureCookieSyncEvent(); + final CookieSyncEvent cookieSyncEvent = captureCookieSyncTcfEvent(); assertThat(cookieSyncEvent).isEqualTo(CookieSyncEvent.builder() .status(200) .bidderStatus(singletonList(BidderUsersyncStatus.builder() @@ -1155,25 +1316,25 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { } @Test - public void handleShouldRespondWithNoCookieWhenBothCcpaAndGdprRejectBidders() throws IOException { + public void shouldRespondWithNoCookieWhenBothCcpaAndGdprRejectBidders() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(routingContext.getBody()) .willReturn(givenRequestBody(CookieSyncRequest.builder().bidders(asList(RUBICON, APPNEXUS)).build())); - rubiconUsersyncer = new Usersyncer(RUBICON, "", null, null, null, false); - appnexusUsersyncer = new Usersyncer(APPNEXUS_COOKIE, "", null, null, null, false); + rubiconUsersyncer = createUsersyncer(RUBICON, "", null); + appnexusUsersyncer = createUsersyncer(APPNEXUS_COOKIE, "", null); givenUsersyncersReturningFamilyName(); given(bidderCatalog.isActive(RUBICON)).willReturn(true); given(bidderCatalog.isActive(APPNEXUS)).willReturn(true); - given(bidderCatalog.bidderInfoByName(RUBICON)).willReturn(BidderInfo.create(true, null, null, - null, null, 2, true, true, false)); - given(bidderCatalog.bidderInfoByName(APPNEXUS)).willReturn(BidderInfo.create(true, null, null, - null, null, 2, true, false, false)); + given(bidderCatalog.bidderInfoByName(RUBICON)).willReturn( + BidderInfo.create(true, null, null, null, null, null, null, 2, true, true, false)); + given(bidderCatalog.bidderInfoByName(APPNEXUS)).willReturn( + BidderInfo.create(true, null, null, null, null, null, null, 2, true, false, false)); given(privacyEnforcementService.isCcpaEnforced(any(), any())).willReturn(true); @@ -1193,19 +1354,24 @@ public void handleShouldRespondWithNoCookieWhenBothCcpaAndGdprRejectBidders() th private void givenTcfServiceReturningVendorIdResult(Set vendorIds) { given(tcfDefinerService.resultForVendorIds(anySet(), any())) - .willReturn(Future.succeededFuture(TcfResponse.of(true, actions(vendorIds), null))); + .willReturn(Future.succeededFuture(TcfResponse.of(true, actions(vendorIds, false), null))); + } + + private void givenTcfServiceReturningBlockedVendorIdResult(Set vendorIds) { + given(tcfDefinerService.resultForVendorIds(anySet(), any())) + .willReturn(Future.succeededFuture(TcfResponse.of(true, actions(vendorIds, true), null))); } private void givenTcfServiceReturningBidderNamesResult(Set bidderNames) { given(tcfDefinerService.resultForBidderNames(anySet(), any(), any())) - .willReturn(Future.succeededFuture(TcfResponse.of(true, actions(bidderNames), null))); + .willReturn(Future.succeededFuture(TcfResponse.of(true, actions(bidderNames, false), null))); } - private static Map actions(Set keys) { + private static Map actions(Set keys, boolean blockPixelSync) { return keys.stream() .collect(Collectors.toMap( identity(), - vendorId -> PrivacyEnforcementAction.builder().blockPixelSync(false).build())); + vendorId -> PrivacyEnforcementAction.builder().blockPixelSync(blockPixelSync).build())); } private static Buffer givenRequestBody(CookieSyncRequest request) { @@ -1224,15 +1390,31 @@ private void givenUsersyncersReturningFamilyName() { given(bidderCatalog.usersyncerByName(APPNEXUS)).willReturn(appnexusUsersyncer); } + private static Usersyncer createUsersyncer(String cookieFamilyName, + String usersyncUrl, + String type) { + + return Usersyncer.of( + cookieFamilyName, + Usersyncer.UsersyncMethod.of(type, usersyncUrl, null, false), + null); + } + private CookieSyncResponse captureCookieSyncResponse() throws IOException { final ArgumentCaptor cookieSyncResponseCaptor = ArgumentCaptor.forClass(String.class); verify(httpResponse).end(cookieSyncResponseCaptor.capture()); return mapper.readValue(cookieSyncResponseCaptor.getValue(), CookieSyncResponse.class); } + private CookieSyncEvent captureCookieSyncTcfEvent() { + final ArgumentCaptor cookieSyncEventCaptor = ArgumentCaptor.forClass(CookieSyncEvent.class); + verify(analyticsReporterDelegator).processEvent(cookieSyncEventCaptor.capture(), any()); + return cookieSyncEventCaptor.getValue(); + } + private CookieSyncEvent captureCookieSyncEvent() { final ArgumentCaptor cookieSyncEventCaptor = ArgumentCaptor.forClass(CookieSyncEvent.class); - verify(analyticsReporter).processEvent(cookieSyncEventCaptor.capture()); + verify(analyticsReporterDelegator).processEvent(cookieSyncEventCaptor.capture()); return cookieSyncEventCaptor.getValue(); } diff --git a/src/test/java/org/prebid/server/handler/CurrencyRatesHandlerTest.java b/src/test/java/org/prebid/server/handler/CurrencyRatesHandlerTest.java index da07e5bfdfc..bce27ba876a 100644 --- a/src/test/java/org/prebid/server/handler/CurrencyRatesHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/CurrencyRatesHandlerTest.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; -import io.vertx.core.http.CaseInsensitiveHeaders; +import io.netty.util.AsciiString; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; import org.junit.Before; @@ -20,6 +20,8 @@ import java.util.Map; import static java.util.Collections.singletonMap; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; @@ -40,10 +42,11 @@ public class CurrencyRatesHandlerTest extends VertxTest { @Before public void setUp() { - currencyRatesHandler = new CurrencyRatesHandler(currencyConversionService, jacksonMapper); + currencyRatesHandler = new CurrencyRatesHandler(currencyConversionService, "/endpoint", jacksonMapper); given(routingContext.response()).willReturn(httpResponse); - given(httpResponse.headers()).willReturn(new CaseInsensitiveHeaders()); + given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + given(httpResponse.putHeader(any(), any(AsciiString.class))).willReturn(httpResponse); } @Test diff --git a/src/test/java/org/prebid/server/handler/DealsStatusHandlerTest.java b/src/test/java/org/prebid/server/handler/DealsStatusHandlerTest.java new file mode 100644 index 00000000000..68e4a2fed66 --- /dev/null +++ b/src/test/java/org/prebid/server/handler/DealsStatusHandlerTest.java @@ -0,0 +1,86 @@ +package org.prebid.server.handler; + +import io.netty.handler.codec.http.HttpHeaderValues; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.DeliveryProgressService; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; +import org.prebid.server.util.HttpUtil; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class DealsStatusHandlerTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + private DealsStatusHandler dealsStatusHandler; + + @Mock + private DeliveryProgressService deliveryProgressService; + + @Mock + private RoutingContext routingContext; + + @Mock + private HttpServerResponse httpServerResponse; + + @Before + public void setUp() { + dealsStatusHandler = new DealsStatusHandler(deliveryProgressService, jacksonMapper); + given(routingContext.response()).willReturn(httpServerResponse); + given(httpServerResponse.putHeader(any(CharSequence.class), any(CharSequence.class))) + .willReturn(httpServerResponse); + given(httpServerResponse.closed()).willReturn(false); + given(httpServerResponse.exceptionHandler(any())).willReturn(httpServerResponse); + } + + @Test + public void handleShouldNotSendDeliveryProgressReportWhenClientHasGone() { + // given + given(httpServerResponse.closed()).willReturn(true); + + // when + dealsStatusHandler.handle(routingContext); + + // then + verify(httpServerResponse, never()).end(); + } + + @Test + public void handleShouldReturnDeliveryProgressReport() throws IOException { + // given + given(deliveryProgressService.getOverallDeliveryProgressReport()) + .willReturn(DeliveryProgressReport.builder().reportId("reportId").build()); + + // when + dealsStatusHandler.handle(routingContext); + + // then + final String responseBody = getResponseBody(); + verify(httpServerResponse).putHeader(eq(HttpUtil.CONTENT_TYPE_HEADER), eq(HttpHeaderValues.APPLICATION_JSON)); + assertThat(mapper.readValue(responseBody, DeliveryProgressReport.class)) + .isEqualTo(DeliveryProgressReport.builder().reportId("reportId").build()); + } + + private String getResponseBody() { + final ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(String.class); + verify(httpServerResponse).end(bodyCaptor.capture()); + return bodyCaptor.getValue(); + } +} diff --git a/src/test/java/org/prebid/server/handler/GetuidsHandlerTest.java b/src/test/java/org/prebid/server/handler/GetuidsHandlerTest.java index 9c34321dc99..98d5b2060cb 100644 --- a/src/test/java/org/prebid/server/handler/GetuidsHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/GetuidsHandlerTest.java @@ -15,6 +15,7 @@ import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.cookie.model.UidWithExpiry; import org.prebid.server.cookie.proto.Uids; +import org.prebid.server.util.HttpUtil; import java.time.ZonedDateTime; import java.util.Collections; @@ -48,6 +49,9 @@ public void setUp() { given(routingContext.request()).willReturn(httpServerRequest); given(routingContext.response()).willReturn(httpServerResponse); + given(httpServerResponse.putHeader(any(CharSequence.class), any(CharSequence.class))) + .willReturn(httpServerResponse); + getuidsHandler = new GetuidsHandler(uidsCookieService, jacksonMapper); } @@ -65,7 +69,7 @@ public void shouldReturnBuyerUidsJsonWithoutExpirationDate() { uids.put("adnxs", new UidWithExpiry("Appnexus-uid", ZonedDateTime.parse("2019-04-01T12:30:40.123456789Z"))); - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(uids).bday(ZonedDateTime.parse("2019-04-01T13:28:40.123456789Z")).build(), jacksonMapper)); @@ -73,6 +77,8 @@ public void shouldReturnBuyerUidsJsonWithoutExpirationDate() { getuidsHandler.handle(routingContext); // then + verify(httpServerResponse).putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpUtil.APPLICATION_JSON_CONTENT_TYPE); + final String responseBody = getResponseBody(); assertThat(responseBody).isNotBlank() @@ -82,7 +88,7 @@ public void shouldReturnBuyerUidsJsonWithoutExpirationDate() { @Test public void shouldReturnEmptyBuyerUids() { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(Collections.emptyMap()).build(), jacksonMapper)); @@ -90,6 +96,8 @@ public void shouldReturnEmptyBuyerUids() { getuidsHandler.handle(routingContext); // then + verify(httpServerResponse).putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpUtil.APPLICATION_JSON_CONTENT_TYPE); + final String responseBody = getResponseBody(); assertThat(responseBody).isNotBlank().isEqualTo("{}"); diff --git a/src/test/java/org/prebid/server/handler/HttpInteractionLogHandlerTest.java b/src/test/java/org/prebid/server/handler/HttpInteractionLogHandlerTest.java index 703ad70b5c4..0720b9e0b2e 100644 --- a/src/test/java/org/prebid/server/handler/HttpInteractionLogHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/HttpInteractionLogHandlerTest.java @@ -42,7 +42,7 @@ public void setUp() { given(routingContext.response()).willReturn(httpResponse); given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - handler = new HttpInteractionLogHandler(5, httpInteractionLogger); + handler = new HttpInteractionLogHandler(5, httpInteractionLogger, "/endpoint"); } @Test diff --git a/src/test/java/org/prebid/server/handler/LineItemStatusHandlerTest.java b/src/test/java/org/prebid/server/handler/LineItemStatusHandlerTest.java new file mode 100644 index 00000000000..e4b286342ce --- /dev/null +++ b/src/test/java/org/prebid/server/handler/LineItemStatusHandlerTest.java @@ -0,0 +1,104 @@ +package org.prebid.server.handler; + +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.DeliveryProgressService; +import org.prebid.server.deals.proto.report.LineItemStatusReport; +import org.prebid.server.exception.PreBidException; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +public class LineItemStatusHandlerTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private DeliveryProgressService deliveryProgressService; + + private LineItemStatusHandler handler; + @Mock + private RoutingContext routingContext; + @Mock + private HttpServerRequest httpRequest; + @Mock + private HttpServerResponse httpResponse; + + @Before + public void setUp() { + given(routingContext.request()).willReturn(httpRequest); + given(routingContext.response()).willReturn(httpResponse); + + given(httpRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap()); + given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + + given(routingContext.request().getParam(any())).willReturn("lineItemId"); + + handler = new LineItemStatusHandler(deliveryProgressService, jacksonMapper, "endpoint"); + } + + @Test + public void handleShouldRespondWithErrorIfNoLineItemIdSpecified() { + // given + given(routingContext.request().getParam(any())).willReturn(null); + + // when + handler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(400); + verify(httpResponse).end("id parameter is required"); + } + + @Test + public void handleShouldRespondWithErrorIfProcessingFailed() { + // given + given(deliveryProgressService.getLineItemStatusReport(any(), any())).willThrow(new PreBidException("error")); + + // when + handler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(400); + verify(httpResponse).end("error"); + } + + @Test + public void handleShouldRespondWithErrorIfUnexpectedExceptionOccurred() { + // given + given(deliveryProgressService.getLineItemStatusReport(any(), any())).willThrow(new RuntimeException("error")); + + // when + handler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(500); + verify(httpResponse).end("error"); + } + + @Test + public void handleShouldRespondWithExpectedResult() { + // given + given(deliveryProgressService.getLineItemStatusReport(any(), any())) + .willReturn(LineItemStatusReport.builder().lineItemId("lineItemId").build()); + + // when + handler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(200); + verify(httpResponse).end("{\"lineItemId\":\"lineItemId\"}"); + } +} diff --git a/src/test/java/org/prebid/server/handler/LoggerControlKnobHandlerTest.java b/src/test/java/org/prebid/server/handler/LoggerControlKnobHandlerTest.java index 9c9aab90d76..ed89808f92c 100644 --- a/src/test/java/org/prebid/server/handler/LoggerControlKnobHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/LoggerControlKnobHandlerTest.java @@ -43,7 +43,7 @@ public void setUp() { given(routingContext.response()).willReturn(httpResponse); given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - handler = new LoggerControlKnobHandler(10000L, loggerControlKnob); + handler = new LoggerControlKnobHandler(10000L, loggerControlKnob, "endpoint"); } @Test diff --git a/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java b/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java index 46dcd15722d..88af2a7ab8f 100644 --- a/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java @@ -16,14 +16,19 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.HttpContext; import org.prebid.server.analytics.model.NotificationEvent; import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.cookie.UidsCookieService; +import org.prebid.server.deals.UserService; +import org.prebid.server.deals.events.ApplicationEventService; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.util.ResourceUtil; import java.io.IOException; @@ -35,9 +40,10 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; public class NotificationEventHandlerTest extends VertxTest { @@ -46,11 +52,20 @@ public class NotificationEventHandlerTest extends VertxTest { public final MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock - private AnalyticsReporter analyticsReporter; + private UidsCookieService uidsCookieService; + @Mock + private ApplicationEventService applicationEventService; + @Mock + private UserService userService; + @Mock + private AnalyticsReporterDelegator analyticsReporterDelegator; @Mock private TimeoutFactory timeoutFactory; @Mock private ApplicationSettings applicationSettings; + + private NotificationEventHandler notificationHandler; + @Mock private RoutingContext routingContext; @Mock @@ -58,8 +73,6 @@ public class NotificationEventHandlerTest extends VertxTest { @Mock private HttpServerResponse httpResponse; - private NotificationEventHandler notificationHandler; - @Before public void setUp() { given(routingContext.request()).willReturn(httpRequest); @@ -71,19 +84,23 @@ public void setUp() { given(httpResponse.putHeader(any(CharSequence.class), any(CharSequence.class))).willReturn(httpResponse); given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - notificationHandler = new NotificationEventHandler(analyticsReporter, timeoutFactory, applicationSettings); + notificationHandler = new NotificationEventHandler( + uidsCookieService, + applicationEventService, + userService, + analyticsReporterDelegator, + timeoutFactory, + applicationSettings, + true); } @Test public void shouldReturnBadRequestWhenTypeIsMissing() { - // given - given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap()); - // when notificationHandler.handle(routingContext); // then - verifyZeroInteractions(analyticsReporter); + verifyZeroInteractions(analyticsReporterDelegator); assertThat(captureResponseStatusCode()).isEqualTo(400); } @@ -91,14 +108,15 @@ public void shouldReturnBadRequestWhenTypeIsMissing() { @Test public void shouldReturnBadRequestWhenTypeIsInvalid() { // given - given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap() - .add("t", "invalid")); + given(httpRequest.params()) + .willReturn(MultiMap.caseInsensitiveMultiMap() + .add("t", "invalid")); // when notificationHandler.handle(routingContext); // then - verifyZeroInteractions(analyticsReporter); + verifyZeroInteractions(analyticsReporterDelegator); assertThat(captureResponseStatusCode()).isEqualTo(400); } @@ -106,14 +124,15 @@ public void shouldReturnBadRequestWhenTypeIsInvalid() { @Test public void shouldReturnBadRequestWhenBidIdIsMissing() { // given - given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap() - .add("t", "win")); + given(httpRequest.params()) + .willReturn(MultiMap.caseInsensitiveMultiMap() + .add("t", "win")); // when notificationHandler.handle(routingContext); // then - verifyZeroInteractions(analyticsReporter); + verifyZeroInteractions(analyticsReporterDelegator); assertThat(captureResponseStatusCode()).isEqualTo(400); } @@ -131,7 +150,7 @@ public void shouldReturnBadRequestWhenTimestampIsInvalid() { notificationHandler.handle(routingContext); // then - verifyZeroInteractions(analyticsReporter); + verifyZeroInteractions(analyticsReporterDelegator); assertThat(captureResponseStatusCode()).isEqualTo(400); } @@ -139,15 +158,16 @@ public void shouldReturnBadRequestWhenTimestampIsInvalid() { @Test public void shouldReturnUnauthorizedWhenAccountIsMissing() { // given - given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap() - .add("t", "win") - .add("b", "bidId")); + given(httpRequest.params()) + .willReturn(MultiMap.caseInsensitiveMultiMap() + .add("t", "win") + .add("b", "bidId")); // when notificationHandler.handle(routingContext); // then - verifyZeroInteractions(analyticsReporter); + verifyZeroInteractions(analyticsReporterDelegator); assertThat(captureResponseStatusCode()).isEqualTo(401); } @@ -165,7 +185,7 @@ public void shouldReturnBadRequestWhenFormatValueIsInvalid() { notificationHandler.handle(routingContext); // then - verifyZeroInteractions(analyticsReporter); + verifyZeroInteractions(analyticsReporterDelegator); assertThat(captureResponseStatusCode()).isEqualTo(400); } @@ -184,7 +204,7 @@ public void shouldReturnBadRequestWhenAnalyticsValueIsInvalid() { notificationHandler.handle(routingContext); // then - verifyZeroInteractions(analyticsReporter); + verifyZeroInteractions(analyticsReporterDelegator); assertThat(captureResponseStatusCode()).isEqualTo(400); } @@ -204,7 +224,7 @@ public void shouldReturnBadRequestWhenIntegrationValueIsInvalid() { notificationHandler.handle(routingContext); // then - verifyZeroInteractions(analyticsReporter); + verifyZeroInteractions(analyticsReporterDelegator); assertThat(captureResponseStatusCode()).isEqualTo(400); } @@ -224,7 +244,7 @@ public void shouldNotPassEventToAnalyticsReporterWhenAccountNotFound() { notificationHandler.handle(routingContext); // then - verifyZeroInteractions(analyticsReporter); + verifyZeroInteractions(analyticsReporterDelegator); assertThat(captureResponseStatusCode()).isEqualTo(401); assertThat(captureResponseBody()).isEqualTo("Account 'accountId' doesn't support events"); @@ -239,13 +259,18 @@ public void shouldNotPassEventToAnalyticsReporterWhenAccountEventNotEnabled() { .add("a", "accountId")); given(applicationSettings.getAccountById(anyString(), any())) - .willReturn(Future.succeededFuture(Account.builder().id("accountId").eventsEnabled(false).build())); + .willReturn(Future.succeededFuture(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(false)) + .build()) + .build())); // when notificationHandler.handle(routingContext); // then - verifyZeroInteractions(analyticsReporter); + verifyZeroInteractions(analyticsReporterDelegator); assertThat(captureResponseStatusCode()).isEqualTo(401); assertThat(captureResponseBody()).isEqualTo("Account 'accountId' doesn't support events"); @@ -259,7 +284,11 @@ public void shouldPassEventToAnalyticsReporterWhenAccountEventEnabled() { .add("b", "bidId") .add("a", "accountId")); - final Account account = Account.builder().eventsEnabled(true).build(); + final Account account = Account.builder() + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build(); given(applicationSettings.getAccountById(anyString(), any())) .willReturn(Future.succeededFuture(account)); @@ -285,6 +314,115 @@ public void shouldPassEventToAnalyticsReporterWhenAccountEventEnabled() { .build()); } + @Test + public void shouldUpdateEventForLineItemForEventTypeWinAndAccountEventsEnabled() { + // given + given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap() + .add("t", "win") + .add("b", "bidId") + .add("l", "lineItemId") + .add("a", "accountId")); + + final Account account = Account.builder() + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build(); + given(applicationSettings.getAccountById(anyString(), any())) + .willReturn(Future.succeededFuture(account)); + + // when + notificationHandler.handle(routingContext); + + // then + verify(applicationEventService).publishLineItemWinEvent(eq("lineItemId")); + } + + @Test + public void shouldProcessLineItemEventWhenRequestAnalyticsFlagDisabled() { + // given + given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap() + .add("t", "win") + .add("b", "bidId") + .add("l", "lineItemId") + .add("a", "accountId") + .add("x", "0")); + + final Account account = Account.builder() + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build(); + given(applicationSettings.getAccountById(anyString(), any())) + .willReturn(Future.succeededFuture(account)); + + // when + notificationHandler.handle(routingContext); + + // then + verify(applicationEventService).publishLineItemWinEvent(eq("lineItemId")); + verify(userService).processWinEvent(eq("lineItemId"), eq("bidId"), any()); + } + + @Test + public void shouldProcessLineItemEventWhenAccountEventsDisabled() { + // given + given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap() + .add("t", "win") + .add("b", "bidId") + .add("l", "lineItemId") + .add("a", "accountId")); + + final Account account = Account.builder() + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(false)) + .build()) + .build(); + given(applicationSettings.getAccountById(anyString(), any())) + .willReturn(Future.succeededFuture(account)); + + // when + notificationHandler.handle(routingContext); + + // then + verify(applicationEventService).publishLineItemWinEvent(eq("lineItemId")); + verify(userService).processWinEvent(eq("lineItemId"), eq("bidId"), any()); + } + + @Test + public void shouldNotProcessLineItemEventWhenDealsDisabled() { + // given + notificationHandler = new NotificationEventHandler( + uidsCookieService, + applicationEventService, + userService, + analyticsReporterDelegator, + timeoutFactory, + applicationSettings, + false); + + given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap() + .add("t", "win") + .add("b", "bidId") + .add("l", "lineItemId") + .add("a", "accountId")); + + final Account account = Account.builder() + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build(); + given(applicationSettings.getAccountById(anyString(), any())) + .willReturn(Future.succeededFuture(account)); + + // when + notificationHandler.handle(routingContext); + + // then + verifyZeroInteractions(applicationEventService); + verifyZeroInteractions(userService); + } + @Test public void shouldNotPassEventToAnalyticsReporterWhenAnalyticsValueIsZero() { // given @@ -294,12 +432,41 @@ public void shouldNotPassEventToAnalyticsReporterWhenAnalyticsValueIsZero() { .add("a", "accountId") .add("x", "0")); + given(applicationSettings.getAccountById(anyString(), any())) + .willReturn(Future.succeededFuture(Account.builder() + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build())); + + // when + notificationHandler.handle(routingContext); + + // then + verifyZeroInteractions(analyticsReporterDelegator); + } + + @Test + public void shouldRespondWhenAnalyticsValueIsZeroAndDoNotSetStatusManually() { + // given + given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap() + .add("t", "win") + .add("b", "bidId") + .add("a", "accountId") + .add("x", "0")); + + given(applicationSettings.getAccountById(anyString(), any())) + .willReturn(Future.succeededFuture(Account.builder() + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build())); + // when notificationHandler.handle(routingContext); // then - verifyZeroInteractions(applicationSettings); - verifyZeroInteractions(analyticsReporter); + verify(httpResponse).end(); } @Test @@ -312,7 +479,11 @@ public void shouldRespondWithPixelAndContentTypeWhenRequestFormatIsImp() throws .add("f", "i")); given(applicationSettings.getAccountById(anyString(), any())) - .willReturn(Future.succeededFuture(Account.builder().eventsEnabled(true).build())); + .willReturn(Future.succeededFuture(Account.builder() + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build())); // when notificationHandler.handle(routingContext); @@ -334,14 +505,17 @@ public void shouldRespondWithNoContentWhenRequestFormatIsNotDefined() { .add("a", "accountId")); given(applicationSettings.getAccountById(anyString(), any())) - .willReturn(Future.succeededFuture(Account.builder().eventsEnabled(true).build())); + .willReturn(Future.succeededFuture(Account.builder() + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build())); // when notificationHandler.handle(routingContext); // then verify(httpResponse).end(); - verifyNoMoreInteractions(httpResponse); } @Test @@ -355,7 +529,11 @@ public void shouldPassExpectedEventToAnalyticsReporter() { .add("ts", "1000") .add("int", "pbjs")); - final Account account = Account.builder().eventsEnabled(true).build(); + final Account account = Account.builder() + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build(); given(applicationSettings.getAccountById(anyString(), any())) .willReturn(Future.succeededFuture(account)); @@ -387,6 +565,57 @@ public void shouldPassExpectedEventToAnalyticsReporter() { .build()); } + @Test + public void shouldPassEventObjectToUserServiceWhenLineItemIdParameterIsPresent() { + // given + given(httpRequest.params()) + .willReturn(MultiMap.caseInsensitiveMultiMap() + .add("t", "win") + .add("b", "bidId") + .add("a", "accountId") + .add("l", "lineItemId")); + + given(applicationSettings.getAccountById(anyString(), any())) + .willReturn(Future.succeededFuture(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build())); + + // when + notificationHandler.handle(routingContext); + + // then + verify(uidsCookieService).parseFromRequest(eq(routingContext)); + + final Map queryParams = new HashMap<>(); + queryParams.put("t", "win"); + queryParams.put("b", "bidId"); + queryParams.put("a", "accountId"); + queryParams.put("l", "lineItemId"); + final HttpContext expectedHttpContext = HttpContext.builder() + .queryParams(queryParams) + .headers(Collections.emptyMap()) + .cookies(Collections.emptyMap()) + .build(); + final NotificationEvent expectedEvent = NotificationEvent.builder() + .type(NotificationEvent.Type.win) + .bidId("bidId") + .account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build()) + .httpContext(expectedHttpContext) + .lineItemId("lineItemId") + .build(); + + verify(userService).processWinEvent(eq("lineItemId"), eq("bidId"), isNull()); + verify(analyticsReporterDelegator).processEvent(eq(expectedEvent)); + } + private Integer captureResponseStatusCode() { final ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class); verify(httpResponse).setStatusCode(captor.capture()); @@ -414,7 +643,7 @@ private Tuple2 captureHeader() { private NotificationEvent captureAnalyticEvent() { final ArgumentCaptor captor = ArgumentCaptor.forClass(NotificationEvent.class); - verify(analyticsReporter).processEvent(captor.capture()); + verify(analyticsReporterDelegator).processEvent(captor.capture()); return captor.getValue(); } } diff --git a/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java b/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java index 30b88e086b9..f30f34793a7 100644 --- a/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/OptoutHandlerTest.java @@ -1,8 +1,8 @@ package org.prebid.server.handler; -import io.netty.util.AsciiString; import io.vertx.core.Future; import io.vertx.core.http.Cookie; +import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; @@ -60,7 +60,7 @@ public void setUp() { given(googleRecaptchaVerifier.verify(anyString())).willReturn(Future.succeededFuture()); given(uidsCookieService.toCookie(any())).willReturn(Cookie.cookie("cookie", "value")); - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); optoutHandler = new OptoutHandler(googleRecaptchaVerifier, uidsCookieService, @@ -179,8 +179,8 @@ private Integer captureResponseStatusCode() { } private String captureResponseLocationHeader() { - final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(httpResponse).putHeader(eq(new AsciiString("Location")), captor.capture()); - return captor.getValue(); + final ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); + verify(httpResponse).putHeader(eq(HttpHeaders.createOptimized("Location")), captor.capture()); + return captor.getValue().toString(); } } diff --git a/src/test/java/org/prebid/server/handler/SettingsCacheNotificationHandlerTest.java b/src/test/java/org/prebid/server/handler/SettingsCacheNotificationHandlerTest.java index 2987ba6dfb4..deaffcef38d 100644 --- a/src/test/java/org/prebid/server/handler/SettingsCacheNotificationHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SettingsCacheNotificationHandlerTest.java @@ -43,7 +43,7 @@ public class SettingsCacheNotificationHandlerTest extends VertxTest { @Before public void setUp() { - handler = new SettingsCacheNotificationHandler(cacheNotificationListener, jacksonMapper); + handler = new SettingsCacheNotificationHandler(cacheNotificationListener, jacksonMapper, "endpoint"); given(routingContext.request()).willReturn(httpRequest); given(routingContext.response()).willReturn(httpResponse); diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index 9aff7e95951..054e66df774 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -3,6 +3,7 @@ import io.vertx.core.Future; import io.vertx.core.http.CaseInsensitiveHeaders; import io.vertx.core.http.Cookie; +import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; @@ -14,7 +15,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.SetuidEvent; import org.prebid.server.auction.PrivacyEnforcementService; import org.prebid.server.bidder.BidderCatalog; @@ -28,11 +29,13 @@ import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; +import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.gdpr.model.TcfResponse; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; import org.prebid.server.settings.model.EnabledForRequestType; import java.io.IOException; @@ -46,9 +49,11 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyMap; +import static java.util.Collections.singleton; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.any; @@ -76,7 +81,7 @@ public class SetuidHandlerTest extends VertxTest { @Mock private TcfDefinerService tcfDefinerService; @Mock - private AnalyticsReporter analyticsReporter; + private AnalyticsReporterDelegator analyticsReporterDelegator; @Mock private Metrics metrics; @@ -88,26 +93,36 @@ public class SetuidHandlerTest extends VertxTest { @Mock private HttpServerResponse httpResponse; + private TcfContext tcfContext; + @Before public void setUp() { - final Map vendorIdToGdpr = singletonMap(null, + final Map vendorIdToGdpr = singletonMap(1, PrivacyEnforcementAction.allowAll()); + tcfContext = TcfContext.builder().gdpr("GDPR").build(); given(privacyEnforcementService.contextFromSetuidRequest(any(), any(), any())) - .willReturn(Future.succeededFuture(PrivacyContext.of(null, null))); + .willReturn(Future.succeededFuture(PrivacyContext.of(null, tcfContext))); given(tcfDefinerService.resultForVendorIds(anySet(), any())) .willReturn(Future.succeededFuture(TcfResponse.of(true, vendorIdToGdpr, null))); given(routingContext.request()).willReturn(httpRequest); given(routingContext.response()).willReturn(httpResponse); + given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); given(httpResponse.headers()).willReturn(new CaseInsensitiveHeaders()); + given(httpResponse.putHeader(any(CharSequence.class), any(CharSequence.class))).willReturn(httpResponse); + given(httpResponse.closed()).willReturn(false); given(uidsCookieService.toCookie(any())).willReturn(Cookie.cookie("test", "test")); + given(bidderCatalog.names()).willReturn(new HashSet<>(asList("rubicon", "audienceNetwork"))); given(bidderCatalog.isActive(any())).willReturn(true); - given(bidderCatalog.usersyncerByName(any())).willReturn( - new Usersyncer(RUBICON, null, null, null, false)); + + given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn( + Usersyncer.of(RUBICON, Usersyncer.UsersyncMethod.of("redirect", null, null, false), null)); + given(bidderCatalog.usersyncerByName(eq(FACEBOOK))).willReturn( + Usersyncer.of(FACEBOOK, Usersyncer.UsersyncMethod.of("redirect", null, null, false), null)); final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); final TimeoutFactory timeoutFactory = new TimeoutFactory(clock); @@ -118,58 +133,105 @@ public void setUp() { bidderCatalog, privacyEnforcementService, tcfDefinerService, - null, - analyticsReporter, + 1, + analyticsReporterDelegator, metrics, timeoutFactory); } @Test - public void shouldRespondWithErrorIfOptedOut() { + public void shouldRespondWithErrorAndTriggerMetricsAndAnalyticsWhenOptedOut() { // given - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).optout(true).build(), jacksonMapper)); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + given(httpRequest.getParam("bidder")).willReturn(RUBICON); // when setuidHandler.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(401)); - verify(httpResponse).end(); + verify(httpResponse).end("Unauthorized: Sync is not allowed for this uids"); + verify(metrics).updateUserSyncOptoutMetric(); + + final SetuidEvent setuidEvent = captureSetuidEvent(); + assertThat(setuidEvent).isEqualTo(SetuidEvent.error(401)); } @Test public void shouldRespondWithErrorIfBidderParamIsMissing() { // given - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - // when setuidHandler.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(400)); - verify(httpResponse).end(eq("\"bidder\" query param is required")); + verify(httpResponse).end(eq("Invalid request format: \"bidder\" query param is required")); + verify(metrics).updateUserSyncBadRequestMetric(); + + final SetuidEvent setuidEvent = captureSetuidEvent(); + assertThat(setuidEvent).isEqualTo(SetuidEvent.error(400)); } @Test public void shouldRespondWithErrorIfBidderParamIsInvalid() { // given - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); - given(httpRequest.getParam(any())).willReturn("invalid_or_disabled"); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + + given(httpRequest.getParam(eq("bidder"))).willReturn("invalid_or_disabled"); // when setuidHandler.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(400)); - verify(httpResponse).end(eq("\"bidder\" query param is invalid")); + verify(httpResponse).end(eq("Invalid request format: \"bidder\" query param is invalid")); + verify(metrics).updateUserSyncBadRequestMetric(); + } + + @Test + public void shouldRespondWithBadRequestStatusIfGdprConsentIsInvalid() { + // given + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) + .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); + + tcfContext = TcfContext.builder().gdpr("1").isConsentValid(false).build(); + given(privacyEnforcementService.contextFromSetuidRequest(any(), any(), any())) + .willReturn(Future.succeededFuture(PrivacyContext.of(null, tcfContext))); + + // when + setuidHandler.handle(routingContext); + + // then + verify(metrics).updateUserSyncTcfInvalidMetric(RUBICON); + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("Invalid request format: Consent string is invalid")); + } + + @Test + public void shouldPassUnsuccessfulEventToAnalyticsReporterIfUidMissingInRequest() { + // given + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) + .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); + + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + + // when + setuidHandler.handle(routingContext); + + // then + final SetuidEvent setuidEvent = captureSetuidEvent(); + assertThat(setuidEvent).isEqualTo(SetuidEvent.builder() + .status(200) + .bidder(RUBICON) + .success(false) + .build()); } @Test @@ -180,20 +242,23 @@ public void shouldRespondWithoutCookieIfGdprProcessingPreventsCookieSetting() { .willReturn(Future.succeededFuture( TcfResponse.of(true, singletonMap(null, privacyEnforcementAction), null))); - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); - - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + given(httpResponse.setStatusMessage(anyString())).willReturn(httpResponse); // when setuidHandler.handle(routingContext); // then verify(routingContext, never()).addCookie(any(Cookie.class)); - verify(httpResponse).setStatusCode(eq(200)); + verify(httpResponse).setStatusCode(eq(451)); verify(httpResponse).end(eq("The gdpr_consent param prevents cookies from being saved")); + verify(metrics).updateUserSyncTcfBlockedMetric(RUBICON); + + final SetuidEvent setuidEvent = captureSetuidEvent(); + assertThat(setuidEvent).isEqualTo(SetuidEvent.builder().status(451).build()); } @Test @@ -202,35 +267,34 @@ public void shouldRespondWithBadRequestStatusIfGdprProcessingFailsWithInvalidReq given(tcfDefinerService.resultForVendorIds(anySet(), any())) .willReturn(Future.failedFuture(new InvalidRequestException("gdpr exception"))); - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - // when setuidHandler.handle(routingContext); // then verify(routingContext, never()).addCookie(any(Cookie.class)); verify(httpResponse).setStatusCode(eq(400)); - verify(httpResponse).end(eq("GDPR processing failed with error: gdpr exception")); + verify(httpResponse).end(eq("Invalid request format: gdpr exception")); + + final SetuidEvent setuidEvent = captureSetuidEvent(); + assertThat(setuidEvent).isEqualTo(SetuidEvent.error(400)); } @Test public void shouldRespondWithInternalServerErrorStatusIfGdprProcessingFailsWithUnexpectedException() { // given given(tcfDefinerService.resultForVendorIds(anySet(), any())) - .willReturn(Future.failedFuture("unexpected error")); + .willReturn(Future.failedFuture("unexpected error TCF")); - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - // when setuidHandler.handle(routingContext); @@ -238,13 +302,13 @@ public void shouldRespondWithInternalServerErrorStatusIfGdprProcessingFailsWithU verify(httpResponse, never()).sendFile(any()); verify(routingContext, never()).addCookie(any(Cookie.class)); verify(httpResponse).setStatusCode(eq(500)); - verify(httpResponse).end(eq("Unexpected GDPR processing error")); + verify(httpResponse).end(eq("Unexpected setuid processing error: unexpected error TCF")); } @Test public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsFound() { // given - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -253,12 +317,12 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsFound() { final AccountGdprConfig accountGdprConfig = AccountGdprConfig.builder() .enabledForRequestType(EnabledForRequestType.of(true, true, true, true)) .build(); - final Account account = Account.builder().gdpr(accountGdprConfig).build(); + final Account account = Account.builder() + .privacy(AccountPrivacyConfig.of(null, accountGdprConfig, null)) + .build(); final Future accountFuture = Future.succeededFuture(account); given(applicationSettings.getAccountById(any(), any())).willReturn(accountFuture); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - // when setuidHandler.handle(routingContext); @@ -270,7 +334,7 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsFound() { @Test public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsNotFound() { // given - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); @@ -278,8 +342,6 @@ public void shouldPassAccountToPrivacyEnforcementServiceWhenAccountIsNotFound() given(applicationSettings.getAccountById(any(), any())).willReturn(Future.failedFuture("bad req")); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - // when setuidHandler.handle(routingContext); @@ -294,11 +356,11 @@ public void shouldRemoveUidFromCookieIfMissingInRequest() throws IOException { final Map uids = new HashMap<>(); uids.put(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT")); uids.put(ADNXS, UidWithExpiry.live("12345")); - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(uids).build(), jacksonMapper)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); - given(httpRequest.getParam("format")).willReturn("img"); + given(httpRequest.getParam("f")).willReturn("i"); // this uids cookie stands for {"tempUIDs":{"adnxs":{"uid":"12345"}}} given(uidsCookieService.toCookie(any())).willReturn(Cookie @@ -320,7 +382,7 @@ public void shouldRemoveUidFromCookieIfMissingInRequest() throws IOException { @Test public void shouldIgnoreFacebookSentinel() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(singletonMap(FACEBOOK, UidWithExpiry.live("facebookUid"))).build(), jacksonMapper)); given(httpRequest.getParam("bidder")).willReturn(FACEBOOK); @@ -329,9 +391,9 @@ public void shouldIgnoreFacebookSentinel() throws IOException { // this uids cookie value stands for {"tempUIDs":{"audienceNetwork":{"uid":"facebookUid"}}} given(uidsCookieService.toCookie(any())).willReturn(Cookie .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJhdWRpZW5jZU5ldHdvcmsiOnsidWlkIjoiZmFjZWJvb2tVaWQifX19")); - + given(bidderCatalog.names()).willReturn(singleton(FACEBOOK)); given(bidderCatalog.usersyncerByName(any())).willReturn( - new Usersyncer(FACEBOOK, null, null, null, false)); + Usersyncer.of(FACEBOOK, Usersyncer.UsersyncMethod.of("iframe", null, null, false), null)); final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); final TimeoutFactory timeoutFactory = new TimeoutFactory(clock); @@ -343,7 +405,7 @@ public void shouldIgnoreFacebookSentinel() throws IOException { privacyEnforcementService, tcfDefinerService, null, - analyticsReporter, + analyticsReporterDelegator, metrics, timeoutFactory); @@ -364,7 +426,7 @@ public void shouldIgnoreFacebookSentinel() throws IOException { @Test public void shouldRespondWithCookieFromRequestParam() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} @@ -372,18 +434,13 @@ public void shouldRespondWithCookieFromRequestParam() throws IOException { .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); - given(httpRequest.getParam("format")).willReturn("img"); given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - // when setuidHandler.handle(routingContext); // then verify(routingContext, never()).addCookie(any(Cookie.class)); - verify(httpResponse).sendFile(any()); - final String uidsCookie = getUidsCookie(); final Uids decodedUids = decodeUids(uidsCookie); assertThat(decodedUids.getUids()).hasSize(1); @@ -391,43 +448,30 @@ public void shouldRespondWithCookieFromRequestParam() throws IOException { } @Test - public void shouldUpdateUidInCookieWithRequestValue() throws IOException { + public void shouldSendPixelWhenFParamIsEqualToIWhenTypeIsIframe() { // given - final Map uids = new HashMap<>(); - uids.put(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT")); - uids.put(ADNXS, UidWithExpiry.live("12345")); - given(uidsCookieService.parseFromRequest(any())) - .willReturn(new UidsCookie(Uids.builder().uids(uids).build(), jacksonMapper)); - - given(httpRequest.getParam("bidder")).willReturn(RUBICON); - given(httpRequest.getParam("uid")).willReturn("updatedUid"); + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) + .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); - // {"tempUIDs":{"adnxs":{"uid":"12345"}, "rubicon":{"uid":"updatedUid"}}} + // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} given(uidsCookieService.toCookie(any())).willReturn(Cookie - .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9LCAicnViaWNvbiI6eyJ1aWQiOiJ1cGRhdGVkVW" - + "lkIn19fQ==")); + .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(httpRequest.getParam("f")).willReturn("i"); + given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); // when setuidHandler.handle(routingContext); // then - verify(httpResponse).end(); verify(routingContext, never()).addCookie(any(Cookie.class)); - - final String uidsCookie = getUidsCookie(); - final Uids decodedUids = decodeUids(uidsCookie); - assertThat(decodedUids.getUids()).hasSize(2); - assertThat(decodedUids.getUids().get(RUBICON).getUid()).isEqualTo("updatedUid"); - assertThat(decodedUids.getUids().get(ADNXS).getUid()).isEqualTo("12345"); + verify(httpResponse).sendFile(any()); } @Test - public void shouldRespondWithCookieIfUserIsNotInGdprScope() throws IOException { + public void shouldSendEmptyResponseWhenFParamIsEqualToBWhenTypeIsRedirect() { // given - given(tcfDefinerService.resultForVendorIds(anySet(), any())) - .willReturn(Future.succeededFuture(TcfResponse.of(false, emptyMap(), null))); - - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} @@ -435,151 +479,246 @@ public void shouldRespondWithCookieIfUserIsNotInGdprScope() throws IOException { .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(httpRequest.getParam("f")).willReturn("b"); given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); + given(bidderCatalog.names()).willReturn(singleton(RUBICON)); + given(bidderCatalog.usersyncerByName(any())) + .willReturn(Usersyncer.of(RUBICON, Usersyncer.UsersyncMethod.of("redirect", null, null, false), null)); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + setuidHandler = new SetuidHandler( + 2000, + uidsCookieService, + applicationSettings, + bidderCatalog, + privacyEnforcementService, + tcfDefinerService, + null, + analyticsReporterDelegator, + metrics, + new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault()))); // when setuidHandler.handle(routingContext); // then verify(routingContext, never()).addCookie(any(Cookie.class)); - verify(httpResponse).end(); - - final String uidsCookie = getUidsCookie(); - final Uids decodedUids = decodeUids(uidsCookie); - assertThat(decodedUids.getUids()).hasSize(1); - assertThat(decodedUids.getUids().get(RUBICON).getUid()).isEqualTo("J5VLCWQP-26-CWFT"); + verify(httpResponse, never()).sendFile(any()); + verify(httpResponse).putHeader(eq(HttpHeaders.CONTENT_LENGTH), eq("0")); + verify(httpResponse).putHeader(eq(HttpHeaders.CONTENT_TYPE), eq(HttpHeaders.TEXT_HTML)); } @Test - public void shouldUpdateOptOutsMetricIfOptedOut() { + public void shouldSendEmptyResponseWhenFParamNotDefinedAndTypeIsIframe() { // given - // this uids cookie value stands for {"optout": true} - given(uidsCookieService.parseFromRequest(any())) - .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).optout(true).build(), jacksonMapper)); + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) + .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} + given(uidsCookieService.toCookie(any())).willReturn(Cookie + .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + + given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn( + Usersyncer.of(RUBICON, Usersyncer.UsersyncMethod.of("iframe", null, null, false), null)); + + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); + + setuidHandler = new SetuidHandler( + 2000, + uidsCookieService, + applicationSettings, + bidderCatalog, + privacyEnforcementService, + tcfDefinerService, + null, + analyticsReporterDelegator, + metrics, + new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault()))); // when setuidHandler.handle(routingContext); // then - verify(metrics).updateUserSyncOptoutMetric(); + verify(routingContext, never()).addCookie(any(Cookie.class)); + verify(httpResponse, never()).sendFile(any()); + verify(httpResponse).putHeader(eq(HttpHeaders.CONTENT_LENGTH), eq("0")); + verify(httpResponse).putHeader(eq(HttpHeaders.CONTENT_TYPE), eq(HttpHeaders.TEXT_HTML)); } @Test - public void shouldUpdateBadRequestsMetricIfBidderParamIsMissing() { + public void shouldSendPixelWhenFParamNotDefinedAndTypeIsRedirect() { // given - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} + given(uidsCookieService.toCookie(any())).willReturn(Cookie + .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(bidderCatalog.names()).willReturn(singleton(RUBICON)); + given(bidderCatalog.usersyncerByName(any())) + .willReturn(Usersyncer.of(RUBICON, Usersyncer.UsersyncMethod.of("redirect", null, null, false), null)); + given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); + + setuidHandler = new SetuidHandler( + 2000, + uidsCookieService, + applicationSettings, + bidderCatalog, + privacyEnforcementService, + tcfDefinerService, + null, + analyticsReporterDelegator, + metrics, + new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault()))); // when setuidHandler.handle(routingContext); // then - verify(metrics).updateUserSyncBadRequestMetric(); + verify(routingContext, never()).addCookie(any(Cookie.class)); + verify(httpResponse).sendFile(any()); } @Test - public void shouldNotSendResponseIfClientClosedConnection() { + public void shouldUpdateUidInCookieWithRequestValue() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())) - .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); + final Map uids = new HashMap<>(); + uids.put(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT")); + uids.put(ADNXS, UidWithExpiry.live("12345")); + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) + .willReturn(new UidsCookie(Uids.builder().uids(uids).build(), jacksonMapper)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); - given(httpRequest.getParam("uid")).willReturn("uid"); + given(httpRequest.getParam("uid")).willReturn("updatedUid"); - given(routingContext.response().closed()).willReturn(true); + // {"tempUIDs":{"adnxs":{"uid":"12345"}, "rubicon":{"uid":"updatedUid"}}} + given(uidsCookieService.toCookie(any())) + .willReturn(Cookie.cookie("uids", + "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiIxMjM0NSJ9LCAicnViaWNvbiI6eyJ1aWQiOiJ1cGRhdGVkVW" + + "lkIn19fQ==")); // when setuidHandler.handle(routingContext); // then - verify(httpResponse, never()).end(); + verify(httpResponse).sendFile(any()); + verify(routingContext, never()).addCookie(any(Cookie.class)); + + final String uidsCookie = getUidsCookie(); + final Uids decodedUids = decodeUids(uidsCookie); + assertThat(decodedUids.getUids()).hasSize(2); + assertThat(decodedUids.getUids().get(RUBICON).getUid()).isEqualTo("updatedUid"); + assertThat(decodedUids.getUids().get(ADNXS).getUid()).isEqualTo("12345"); } @Test - public void shouldUpdateSetsMetric() { + public void shouldRespondWithCookieIfUserIsNotInGdprScope() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())) + given(tcfDefinerService.resultForVendorIds(anySet(), any())) + .willReturn(Future.succeededFuture(TcfResponse.of(false, emptyMap(), null))); + + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); + // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} + given(uidsCookieService.toCookie(any())).willReturn(Cookie + .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + given(httpRequest.getParam("bidder")).willReturn(RUBICON); - given(httpRequest.getParam("uid")).willReturn("updatedUid"); + given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); // when setuidHandler.handle(routingContext); // then - verify(metrics).updateUserSyncSetsMetric(eq(RUBICON)); + verify(routingContext, never()).addCookie(any(Cookie.class)); + verify(httpResponse).sendFile(any()); + + final String uidsCookie = getUidsCookie(); + final Uids decodedUids = decodeUids(uidsCookie); + assertThat(decodedUids.getUids()).hasSize(1); + assertThat(decodedUids.getUids().get(RUBICON).getUid()).isEqualTo("J5VLCWQP-26-CWFT"); } @Test - public void shouldPassUnauthorizedEventToAnalyticsReporterIfOptedOut() { + public void shouldSkipTcfChecksAndRespondWithCookieIfHostVendorIdNotDefined() throws IOException { // given - given(uidsCookieService.parseFromRequest(any())) - .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).optout(true).build(), jacksonMapper)); + final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + setuidHandler = new SetuidHandler(2000, uidsCookieService, applicationSettings, + bidderCatalog, privacyEnforcementService, tcfDefinerService, null, analyticsReporterDelegator, metrics, + new TimeoutFactory(clock)); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) + .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); + + // {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}} + given(uidsCookieService.toCookie(any())).willReturn(Cookie + .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19")); + + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); // when setuidHandler.handle(routingContext); // then - final SetuidEvent setuidEvent = captureSetuidEvent(); - assertThat(setuidEvent).isEqualTo(SetuidEvent.builder().status(401).build()); + verify(tcfDefinerService, never()).resultForVendorIds(anySet(), any()); + verify(routingContext, never()).addCookie(any(Cookie.class)); + verify(httpResponse).sendFile(any()); + + final String uidsCookie = getUidsCookie(); + final Uids decodedUids = decodeUids(uidsCookie); + assertThat(decodedUids.getUids()).hasSize(1); + assertThat(decodedUids.getUids().get(RUBICON).getUid()).isEqualTo("J5VLCWQP-26-CWFT"); } @Test - public void shouldPassBadRequestEventToAnalyticsReporterIfBidderParamIsMissing() { + public void shouldNotSendResponseIfClientClosedConnection() { // given - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); - given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(httpRequest.getParam("uid")).willReturn("uid"); + + given(routingContext.response().closed()).willReturn(true); // when setuidHandler.handle(routingContext); // then - final SetuidEvent setuidEvent = captureSetuidEvent(); - assertThat(setuidEvent).isEqualTo(SetuidEvent.builder().status(400).build()); + verify(httpResponse, never()).end(); } @Test - public void shouldPassUnsuccessfulEventToAnalyticsReporterIfUidMissingInRequest() { + public void shouldUpdateSetsMetric() { // given - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(httpRequest.getParam("bidder")).willReturn(RUBICON); + given(httpRequest.getParam("uid")).willReturn("updatedUid"); // when setuidHandler.handle(routingContext); // then - final SetuidEvent setuidEvent = captureSetuidEvent(); - assertThat(setuidEvent).isEqualTo(SetuidEvent.builder() - .status(200) - .bidder(RUBICON) - .success(false) - .build()); + verify(metrics).updateUserSyncSetsMetric(eq(RUBICON)); } @Test public void shouldPassUnsuccessfulEventToAnalyticsReporterIfFacebookSentinel() { // given - given(uidsCookieService.parseFromRequest(any())) + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))) .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(httpRequest.getParam("bidder")).willReturn(FACEBOOK); given(httpRequest.getParam("uid")).willReturn("0"); + given(bidderCatalog.names()).willReturn(singleton(FACEBOOK)); given(bidderCatalog.usersyncerByName(any())).willReturn( - new Usersyncer(FACEBOOK, null, null, null, false)); + Usersyncer.of(FACEBOOK, Usersyncer.UsersyncMethod.of("redirect", null, null, false), null)); final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); final TimeoutFactory timeoutFactory = new TimeoutFactory(clock); @@ -591,7 +730,7 @@ public void shouldPassUnsuccessfulEventToAnalyticsReporterIfFacebookSentinel() { privacyEnforcementService, tcfDefinerService, null, - analyticsReporter, + analyticsReporterDelegator, metrics, timeoutFactory); @@ -611,7 +750,7 @@ public void shouldPassUnsuccessfulEventToAnalyticsReporterIfFacebookSentinel() { @Test public void shouldPassSuccessfulEventToAnalyticsReporter() { // given - given(uidsCookieService.parseFromRequest(any())).willReturn(new UidsCookie( + given(uidsCookieService.parseFromRequest(any(RoutingContext.class))).willReturn(new UidsCookie( Uids.builder().uids(singletonMap(RUBICON, UidWithExpiry.live("J5VLCWQP-26-CWFT"))).build(), jacksonMapper)); @@ -642,7 +781,7 @@ private static Uids decodeUids(String value) throws IOException { private SetuidEvent captureSetuidEvent() { final ArgumentCaptor setuidEventCaptor = ArgumentCaptor.forClass(SetuidEvent.class); - verify(analyticsReporter).processEvent(setuidEventCaptor.capture()); + verify(analyticsReporterDelegator).processEvent(setuidEventCaptor.capture(), eq(tcfContext)); return setuidEventCaptor.getValue(); } } diff --git a/src/test/java/org/prebid/server/handler/StatusHandlerTest.java b/src/test/java/org/prebid/server/handler/StatusHandlerTest.java index d3dfc0ea4a2..842261ef38f 100644 --- a/src/test/java/org/prebid/server/handler/StatusHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/StatusHandlerTest.java @@ -1,6 +1,8 @@ package org.prebid.server.handler; import com.fasterxml.jackson.core.JsonProcessingException; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.util.AsciiString; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; import org.junit.Rule; @@ -11,10 +13,12 @@ import org.prebid.server.VertxTest; import org.prebid.server.health.HealthChecker; import org.prebid.server.health.model.StatusResponse; +import org.prebid.server.util.HttpUtil; import java.time.Clock; import java.time.ZonedDateTime; import java.util.Arrays; +import java.util.Collections; import java.util.Map; import java.util.TreeMap; @@ -23,6 +27,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.any; public class StatusHandlerTest extends VertxTest { @@ -50,6 +55,8 @@ public void shouldRespondHttp200OkWithExpectedBody() throws JsonProcessingExcept statusHandler = new StatusHandler(Arrays.asList(healthCheck, healthCheck, healthCheck), jacksonMapper); given(routingContext.response()).willReturn(httpResponse); + given(httpResponse.putHeader(any(CharSequence.class), any(AsciiString.class))).willReturn(httpResponse); + given(healthCheck.name()).willReturn("application", "db", "other"); given(healthCheck.status()).willReturn(StatusResponse.of("ready", null), StatusResponse.of("UP", testTime), StatusResponse.of("DOWN", testTime)); @@ -62,12 +69,12 @@ public void shouldRespondHttp200OkWithExpectedBody() throws JsonProcessingExcept expectedMap.put("application", StatusResponse.of("ready", null)); expectedMap.put("db", StatusResponse.of("UP", testTime)); expectedMap.put("other", StatusResponse.of("DOWN", testTime)); - verify(httpResponse).end(eq(mapper.writeValueAsString(expectedMap))); } @Test public void shouldRespondWithNoContentWhenMessageWasNotDefined() { + // given statusHandler = new StatusHandler(emptyList(), jacksonMapper); given(routingContext.response()).willReturn(httpResponse); given(httpResponse.setStatusCode(eq(204))).willReturn(httpResponse); @@ -78,4 +85,21 @@ public void shouldRespondWithNoContentWhenMessageWasNotDefined() { // then verify(httpResponse).setStatusCode(eq(204)); } + + @Test + public void shouldRespondWithContentTypeHeaders() { + // given + statusHandler = new StatusHandler(Collections.singletonList(healthCheck), jacksonMapper); + + given(healthCheck.name()).willReturn("healthCheckName"); + given(healthCheck.status()).willReturn(StatusResponse.of("healthCheckStatus", null)); + + given(routingContext.response()).willReturn(httpResponse); + + // when + statusHandler.handle(routingContext); + + // then + verify(httpResponse).putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + } } diff --git a/src/test/java/org/prebid/server/handler/TracerLogHandlerTest.java b/src/test/java/org/prebid/server/handler/TracerLogHandlerTest.java new file mode 100644 index 00000000000..1dcab0b2085 --- /dev/null +++ b/src/test/java/org/prebid/server/handler/TracerLogHandlerTest.java @@ -0,0 +1,103 @@ +package org.prebid.server.handler; + +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.log.CriteriaManager; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +public class TracerLogHandlerTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private CriteriaManager criteriaManager; + @Mock + private RoutingContext routingContext; + @Mock + private HttpServerResponse httpResponse; + @Mock + private HttpServerRequest httpRequest; + + private TracerLogHandler tracerLogHandler; + + @Before + public void setUp() { + tracerLogHandler = new TracerLogHandler(criteriaManager); + given(routingContext.response()).willReturn(httpResponse); + given(routingContext.request()).willReturn(httpRequest); + given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + } + + @Test + public void handleShouldReturnBadRequestWhenNoParametersInRequest() { + // given + given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap()); + + // when + tracerLogHandler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("At least one parameter should ne defined: account, bidderCode, lineItemId")); + } + + @Test + public void handleShouldReturnBadRequestWhenDurationWasNotDefined() { + // given + + given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap().add("account", "1001")); + + // when + tracerLogHandler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("duration parameter should be defined")); + } + + @Test + public void handleShouldReturnBadRequestWhenDurationHasIncorrectFormat() { + // given + given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap().add("account", "1001") + .add("duration", "invalid")); + + // when + tracerLogHandler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("duration parameter should be defined as integer, but was invalid")); + } + + @Test + public void handleShouldReturnBadRequestWhenLogLevelHasIncorrectValue() { + // given + given(httpRequest.params()).willReturn(MultiMap.caseInsensitiveMultiMap().add("account", "1001") + .add("duration", "200").add("level", "invalid")); + doThrow(new IllegalArgumentException("Invalid LoggingLevel: invalid")) + .when(criteriaManager).addCriteria(any(), any(), any(), any(), any()); + + // when + tracerLogHandler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("Invalid parameter: Invalid LoggingLevel: invalid")); + } +} diff --git a/src/test/java/org/prebid/server/handler/VersionHandlerTest.java b/src/test/java/org/prebid/server/handler/VersionHandlerTest.java index f0b5156724a..5d3b9f62128 100644 --- a/src/test/java/org/prebid/server/handler/VersionHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/VersionHandlerTest.java @@ -28,43 +28,31 @@ public class VersionHandlerTest extends VertxTest { private VersionHandler versionHandler; @Test - public void shouldCreateRevisionWithNoSetVersionValueWhenFileWasNotFound() throws JsonProcessingException { + public void handleShouldRespondWithHashAndVersionPassedInCreate() throws JsonProcessingException { // given - versionHandler = VersionHandler.create("not_found.json", jacksonMapper); + versionHandler = new VersionHandler("1.41.0", "4df3f6192d7938ccdaac04df783c46c7e8847d08", jacksonMapper, + "endpoint"); given(routingContext.response()).willReturn(httpResponse); // when versionHandler.handle(routingContext); // then - verify(httpResponse).end(mapper.writeValueAsString(RevisionResponse.of("undefined", "undefined"))); - } - - @Test - public void handleShouldRespondWithNotSetWhenPropertyIsNotInFile() throws JsonProcessingException { - // given - versionHandler = VersionHandler.create("org/prebid/server/handler/version/empty.json", jacksonMapper); - given(routingContext.response()).willReturn(httpResponse); - - // when - versionHandler.handle(routingContext); - - // then - verify(httpResponse).end(mapper.writeValueAsString(RevisionResponse.of("undefined", "undefined"))); + verify(httpResponse).end(mapper.writeValueAsString( + RevisionResponse.of("4df3f6192d7938ccdaac04df783c46c7e8847d08", "1.41.0"))); } @Test - public void handleShouldRespondWithHashWhenPropertyIsInFile() throws JsonProcessingException { + public void handleShouldRespondWithoutVersionAndCommitWhenNullPassedAtCreation() throws JsonProcessingException { // given - versionHandler = VersionHandler.create("org/prebid/server/handler/version/version.json", jacksonMapper); + versionHandler = new VersionHandler(null, null, jacksonMapper, "endpoint"); given(routingContext.response()).willReturn(httpResponse); // when versionHandler.handle(routingContext); // then - verify(httpResponse).end(mapper.writeValueAsString( - RevisionResponse.of("4df3f6192d7938ccdaac04df783c46c7e8847d08", "1.41.0"))); + verify(httpResponse).end(mapper.writeValueAsString(RevisionResponse.of(null, null))); } @AllArgsConstructor(staticName = "of") diff --git a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java b/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java index 0f3d97a42ae..42deb16a373 100644 --- a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.TextNode; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.util.AsciiString; import io.vertx.core.Future; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpServerRequest; @@ -23,6 +25,9 @@ import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountEventsConfig; +import org.prebid.server.util.HttpUtil; import java.util.ArrayList; import java.util.HashSet; @@ -39,6 +44,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -69,6 +75,7 @@ public class VtrackHandlerTest extends VertxTest { public void setUp() { given(routingContext.request()).willReturn(httpRequest); given(routingContext.response()).willReturn(httpResponse); + given(httpResponse.putHeader(any(CharSequence.class), any(AsciiString.class))).willReturn(httpResponse); given(httpRequest.getParam("a")).willReturn("accountId"); given(httpRequest.getParam("int")).willReturn("pbjs"); @@ -76,7 +83,7 @@ public void setUp() { given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); handler = new VtrackHandler( - 2000, true, applicationSettings, bidderCatalog, cacheService, timeoutFactory, jacksonMapper); + 2000, true, true, applicationSettings, bidderCatalog, cacheService, timeoutFactory, jacksonMapper); } @Test @@ -94,6 +101,15 @@ public void shouldRespondWithBadRequestWhenAccountParameterIsMissing() { verify(httpResponse).end(eq("Account 'a' is required query parameter and can't be empty")); } + @Test + public void shouldRespondWithExpectedHeaders() { + // when + handler.handle(routingContext); + + // then + verify(httpResponse).putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + } + @Test public void shouldRespondWithBadRequestWhenBodyIsEmpty() { // given @@ -156,11 +172,50 @@ public void shouldRespondWithBadRequestWhenBidderIsMissing() throws JsonProcessi verify(httpResponse).end(eq("'bidder' is required field and can't be empty")); } + @Test + public void shouldRespondWithBadRequestWhenTypeIsNotXML() throws JsonProcessingException { + // given + given(routingContext.getBody()) + .willReturn(givenVtrackRequest(builder -> builder.bidid("bidId").bidder("bidder").type("json"))); + + // when + handler.handle(routingContext); + + // then + verifyZeroInteractions(applicationSettings, cacheService); + + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("vtrack only accepts type xml")); + } + + @Test + public void shouldRespondWithBadRequestWhenValueDoesNotContainVast() throws JsonProcessingException { + // given + given(routingContext.getBody()) + .willReturn(givenVtrackRequest(builder -> builder.bidid("bidId") + .bidder("bidder") + .type("xml") + .value(new TextNode("invalidValue")))); + + // when + handler.handle(routingContext); + + // then + verifyZeroInteractions(applicationSettings, cacheService); + + verify(httpResponse).setStatusCode(eq(400)); + verify(httpResponse).end(eq("vtrack content must be vast")); + } + @Test public void shouldRespondWithInternalServerErrorWhenFetchingAccountFails() throws JsonProcessingException { // given given(routingContext.getBody()) - .willReturn(givenVtrackRequest(builder -> builder.bidid("bidId").bidder("bidder"))); + .willReturn(givenVtrackRequest(builder -> builder + .bidder("bidder") + .bidid("bidId") + .type("xml") + .value(new TextNode(" builder.bidid("bidId").bidder("bidder"))); + .willReturn(givenVtrackRequest(builder -> builder + .bidder("bidder") + .bidid("bidId") + .type("xml") + .value(new TextNode(" putObjects = singletonList( - PutObject.builder().bidid("bidId").bidder("bidder").value(new TextNode("value")).build()); + PutObject.builder() + .bidid("bidId") + .bidder("bidder") + .type("xml") + .value(new TextNode(" putObjects = singletonList( - PutObject.builder().bidid("bidId").bidder("bidder").value(new TextNode("value")).build()); + PutObject.builder() + .bidid("bidId") + .bidder("bidder") + .type("xml") + .value(new TextNode(" putObjects = asList( - PutObject.builder().bidid("bidId1").bidder("bidder").value(new TextNode("value1")).build(), - PutObject.builder().bidid("bidId2").bidder("updatable_bidder").value(new TextNode("value2")).build()); + PutObject.builder().bidid("bidId1") + .bidder("bidder") + .type("xml") + .value(new TextNode(" putObjects = asList( - PutObject.builder().bidid("bidId1").bidder("bidder").value(new TextNode("value1")).build(), - PutObject.builder().bidid("bidId2").bidder("updatable_bidder").value(new TextNode("value2")).build()); + PutObject.builder().bidid("bidId1") + .bidder("bidder") + .type("xml") + .value(new TextNode(" expectedBidders = new HashSet<>(asList("bidder", "updatable_bidder")); - verify(cacheService).cachePutObjects(eq(putObjects), eq(expectedBidders), eq("accountId"), eq("pbjs"), any()); + verify(cacheService).cachePutObjects(eq(putObjects), any(), eq(expectedBidders), eq("accountId"), eq("pbjs"), + any()); verify(httpResponse).end(eq("{\"responses\":[{\"uuid\":\"uuid1\"},{\"uuid\":\"uuid2\"}]}")); } @Test - public void shouldSendToCacheExpectedPutsAndUpdatableUnknownBiddersWhenUnknownBidderIsAllowed() + public void shouldSendToCacheExpectedPutsWhenModifyVastForUnknownBidderAndAllowUnknownBidderIsTrue() throws JsonProcessingException { // given final List putObjects = asList( PutObject.builder() .bidid("bidId1") .bidder("bidder") - .value(new TextNode("value1")) + .type("xml") + .value(new TextNode(" expectedBidders = new HashSet<>(asList("bidder", "updatable_bidder")); - verify(cacheService).cachePutObjects(eq(putObjects), eq(expectedBidders), eq("accountId"), eq("pbjs"), any()); + verify(cacheService).cachePutObjects(eq(putObjects), any(), eq(expectedBidders), eq("accountId"), eq("pbjs"), + any()); verify(httpResponse).end(eq("{\"responses\":[{\"uuid\":\"uuid1\"},{\"uuid\":\"uuid2\"}]}")); } + @Test + public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenAllowUnknownBidderIsFalse() + throws JsonProcessingException { + // given + handler = new VtrackHandler( + 2000, false, true, applicationSettings, bidderCatalog, cacheService, timeoutFactory, jacksonMapper); + + final List putObjects = asList( + PutObject.builder() + .bidid("bidId1") + .bidder("bidder") + .type("xml") + .value(new TextNode(" putObjects = asList( + PutObject.builder() + .bidid("bidId1") + .bidder("bidder") + .type("xml") + .value(new TextNode("... customizers) diff --git a/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java b/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java index 2ee73684045..ff251736427 100644 --- a/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java @@ -54,29 +54,26 @@ public void setUp() { given(httpRequest.getParam(anyString())).willReturn("bidderName1"); - given(bidderCatalog.names()).willReturn(new HashSet<>(asList("bidderName1", "bidderName2"))); - given(bidderCatalog.bidderInfoByName(anyString())).willReturn(givenBidderInfo()); - given(bidderCatalog.isActive("bidderName1")).willReturn(true); - given(bidderCatalog.isActive("bidderName2")).willReturn(false); - - given(bidderCatalog.aliases()).willReturn(new HashSet<>(asList("bidderAlias1", "bidderAlias2"))); - given(bidderCatalog.nameByAlias("bidderAlias1")).willReturn("bidderName1"); - given(bidderCatalog.nameByAlias("bidderAlias2")).willReturn("bidderName2"); + given(bidderCatalog.names()).willReturn(new HashSet<>( + asList("bidderName1", "bidderName2", "bidderAlias1", "bidderAlias2"))); + given(bidderCatalog.bidderInfoByName("bidderName1")).willReturn(givenBidderInfo()); + given(bidderCatalog.bidderInfoByName("bidderAlias2")).willReturn(givenBidderInfo()); + given(bidderCatalog.bidderInfoByName(eq("bidderName2"))) + .willReturn(givenBidderInfo(false, "http://", null)); + given(bidderCatalog.bidderInfoByName(eq("bidderAlias1"))) + .willReturn(givenBidderInfo(false, "http://", "bidderName1")); handler = new BidderDetailsHandler(bidderCatalog, jacksonMapper); } @Test - public void creationShouldFailIfAllAliasIsConfigured() { - given(bidderCatalog.aliases()).willReturn(singleton("all")); + public void creationShouldFailIfAllNameIsConfigured() { + given(bidderCatalog.names()).willReturn(singleton("all")); assertThatIllegalArgumentException().isThrownBy(() -> new BidderDetailsHandler(bidderCatalog, jacksonMapper)); } @Test public void shouldRespondWithExpectedHeaders() { - // given - handler = new BidderDetailsHandler(bidderCatalog, jacksonMapper); - // when handler.handle(routingContext); @@ -98,7 +95,7 @@ public void shouldRespondWithHttpStatus404IfNoBidderFound() { } @Test - public void shouldRespondWithHttpStatus404IfBidderIsDisabled() { + public void shouldRespondWithExpectedBodyForDisabledBidder() { // given given(httpRequest.getParam(anyString())).willReturn("bidderName2"); @@ -106,11 +103,14 @@ public void shouldRespondWithHttpStatus404IfBidderIsDisabled() { handler.handle(routingContext); // then - verify(httpResponse).setStatusCode(404); + verify(httpResponse).end("{\"status\":\"DISABLED\",\"usesHttps\":false," + + "\"maintainer\":{\"email\":\"test@email.org\"}," + + "\"capabilities\":{\"app\":{\"mediaTypes\":[\"mediaType1\"]}," + + "\"site\":{\"mediaTypes\":[\"mediaType2\"]}}}"); } @Test - public void shouldRespondWithHttpStatus404IfBidderAliasIsDisabled() { + public void shouldRespondWithExpecteddBodyForDisabledAlias() { // given given(httpRequest.getParam(anyString())).willReturn("bidderAlias2"); @@ -118,7 +118,10 @@ public void shouldRespondWithHttpStatus404IfBidderAliasIsDisabled() { handler.handle(routingContext); // then - verify(httpResponse).setStatusCode(404); + verify(httpResponse).end("{\"status\":\"ACTIVE\",\"usesHttps\":true," + + "\"maintainer\":{\"email\":\"test@email.org\"}," + + "\"capabilities\":{\"app\":{\"mediaTypes\":[\"mediaType1\"]}," + + "\"site\":{\"mediaTypes\":[\"mediaType2\"]}}}"); } @Test @@ -128,8 +131,9 @@ public void shouldRespondWithExpectedBody() { // then verify(httpResponse).end( - eq("{\"maintainer\":{\"email\":\"test@email.org\"},\"capabilities\":{\"app\":" - + "{\"mediaTypes\":[\"mediaType1\"]},\"site\":{\"mediaTypes\":[\"mediaType2\"]}}}")); + eq("{\"status\":\"ACTIVE\",\"usesHttps\":true,\"maintainer\":{\"email\":\"test@email.org\"}," + + "\"capabilities\":{\"app\":{\"mediaTypes\":[\"mediaType1\"]}," + + "\"site\":{\"mediaTypes\":[\"mediaType2\"]}}}")); } @Test @@ -142,9 +146,9 @@ public void shouldRespondWithExpectedBodyForBidderAlias() { // then verify(httpResponse).end( - eq("{\"maintainer\":{\"email\":\"test@email.org\"},\"capabilities\":{\"app\":" - + "{\"mediaTypes\":[\"mediaType1\"]},\"site\":{\"mediaTypes\":[\"mediaType2\"]}}," - + "\"aliasOf\":\"bidderName1\"}")); + eq("{\"status\":\"DISABLED\",\"usesHttps\":false,\"maintainer\":{\"email\":\"test@email.org\"}," + + "\"capabilities\":{\"app\":{\"mediaTypes\":[\"mediaType1\"]}," + + "\"site\":{\"mediaTypes\":[\"mediaType2\"]}},\"aliasOf\":\"bidderName1\"}")); } @Test @@ -157,15 +161,39 @@ public void shouldRespondWithExpectedBodyForAllQueryParam() { // then verify(httpResponse).end( - eq("{\"bidderAlias1\":{\"maintainer\":{\"email\":\"test@email.org\"},\"capabilities\":" - + "{\"app\":{\"mediaTypes\":[\"mediaType1\"]},\"site\":{\"mediaTypes\":[\"mediaType2\"]}}," - + "\"aliasOf\":\"bidderName1\"}," - + "\"bidderName1\":{\"maintainer\":{\"email\":\"test@email.org\"},\"capabilities\":" - + "{\"app\":{\"mediaTypes\":[\"mediaType1\"]},\"site\":{\"mediaTypes\":[\"mediaType2\"]}}}}")); + eq("{\"bidderAlias1\":{\"status\":\"DISABLED\",\"usesHttps\":false," + + "\"maintainer\":{\"email\":\"test@email.org\"}," + + "\"capabilities\":{\"app\":{\"mediaTypes\":[\"mediaType1\"]}," + + "\"site\":{\"mediaTypes\":[\"mediaType2\"]}},\"aliasOf\":\"bidderName1\"}," + + "\"bidderAlias2\":{\"status\":\"ACTIVE\",\"usesHttps\":true," + + "\"maintainer\":{\"email\":\"test@email.org\"}," + + "\"capabilities\":{\"app\":{\"mediaTypes\":[\"mediaType1\"]}," + + "\"site\":{\"mediaTypes\":[\"mediaType2\"]}}},\"bidderName1\":{\"status\":\"ACTIVE\"," + + "\"usesHttps\":true,\"maintainer\":{\"email\":\"test@email.org\"}," + + "\"capabilities\":{\"app\":{\"mediaTypes\":[\"mediaType1\"]}," + + "\"site\":{\"mediaTypes\":[\"mediaType2\"]}}}," + + "\"bidderName2\":{\"status\":\"DISABLED\",\"usesHttps\":false," + + "\"maintainer\":{\"email\":\"test@email.org\"}," + + "\"capabilities\":{\"app\":{\"mediaTypes\":[\"mediaType1\"]}," + + "\"site\":{\"mediaTypes\":[\"mediaType2\"]}}}}")); + } + + private static BidderInfo givenBidderInfo(boolean enabled, String endpoint, String aliasOf) { + return BidderInfo.create( + enabled, + endpoint, + aliasOf, + "test@email.org", + singletonList("mediaType1"), + singletonList("mediaType2"), + null, + 0, + true, + true, + false); } private static BidderInfo givenBidderInfo() { - return BidderInfo.create(true, "test@email.org", singletonList("mediaType1"), - singletonList("mediaType2"), null, 0, true, true, false); + return givenBidderInfo(true, "https://endpoint.com", null); } } diff --git a/src/test/java/org/prebid/server/handler/info/BiddersHandlerTest.java b/src/test/java/org/prebid/server/handler/info/BiddersHandlerTest.java index a110fcd584d..8d0ecceb7b0 100644 --- a/src/test/java/org/prebid/server/handler/info/BiddersHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/info/BiddersHandlerTest.java @@ -1,6 +1,8 @@ package org.prebid.server.handler.info; +import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.util.AsciiString; +import io.vertx.core.MultiMap; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; @@ -19,7 +21,6 @@ import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; @@ -44,7 +45,10 @@ public class BiddersHandlerTest extends VertxTest { public void setUp() { given(routingContext.request()).willReturn(httpRequest); given(routingContext.response()).willReturn(httpResponse); + given(routingContext.queryParams()) + .willReturn(MultiMap.caseInsensitiveMultiMap().add("enabledonly", "false")); given(httpResponse.putHeader(any(CharSequence.class), any(CharSequence.class))).willReturn(httpResponse); + given(httpResponse.setStatusCode(any(Integer.class))).willReturn(httpResponse); given(bidderCatalog.names()).willReturn(emptySet()); handler = new BiddersHandler(bidderCatalog, jacksonMapper); @@ -66,39 +70,73 @@ public void shouldRespondWithExpectedHeaders() { } @Test - public void shouldRespondWithExpectedBodyAndExcludeNotActiveBidders() { + public void shouldRespondWithExpectedMessageAndStatusBadRequestWhenEnabledOnlyNotProvided() { // given - given(bidderCatalog.names()).willReturn(new HashSet<>(asList("bidder2", "bidder3", "bidder1"))); - given(bidderCatalog.isActive(anyString())).willReturn(true); - given(bidderCatalog.isActive(eq("bidder3"))).willReturn(false); + given(routingContext.queryParams()).willReturn(MultiMap.caseInsensitiveMultiMap()); - handler = new BiddersHandler(bidderCatalog, jacksonMapper); + // when + handler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(HttpResponseStatus.BAD_REQUEST.code()); + verify(httpResponse).end(eq("Invalid value for 'enabledonly' query param, must be of boolean type")); + } + + @Test + public void shouldRespondWithExpectedMessageAndStatusBadRequestWhenEnabledOnlyFlagHasInvalidValue() { + // given + given(routingContext.queryParams()) + .willReturn(MultiMap.caseInsensitiveMultiMap().add("enabledonly", "yes")); + + // when + handler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(HttpResponseStatus.BAD_REQUEST.code()); + verify(httpResponse).end(eq("Invalid value for 'enabledonly' query param, must be of boolean type")); + } + + @Test + public void shouldTolerateWithEnabledOnlyFlagInCaseInsensitiveMode() { + // given + given(routingContext.queryParams()) + .willReturn(MultiMap.caseInsensitiveMultiMap().add("enabledonly", "tRuE")); // when handler.handle(routingContext); // then - verify(httpResponse).end(eq("[\"bidder1\",\"bidder2\"]")); + verify(httpResponse).setStatusCode(HttpResponseStatus.OK.code()); } @Test - public void shouldRespondWithExpectedBodyAndExcludeNotActiveBidderAliases() { + public void shouldRespondWithExpectedBodyAndStatusOkForEnabledOnlyFalseFlag() { // given - given(bidderCatalog.names()).willReturn(new HashSet<>(asList("bidder1", "bidder2"))); - given(bidderCatalog.aliases()).willReturn(new HashSet<>(asList("bidder1-alias", "bidder2-alias"))); + given(bidderCatalog.names()).willReturn(new HashSet<>(asList("bidder2", "bidder3", "bidder1"))); + handler = new BiddersHandler(bidderCatalog, jacksonMapper); - given(bidderCatalog.nameByAlias(eq("bidder1-alias"))).willReturn("bidder1"); - given(bidderCatalog.nameByAlias(eq("bidder2-alias"))).willReturn("bidder2"); + // when + handler.handle(routingContext); - given(bidderCatalog.isActive(eq("bidder1"))).willReturn(true); - given(bidderCatalog.isActive(eq("bidder2"))).willReturn(false); + // then + verify(httpResponse).setStatusCode(HttpResponseStatus.OK.code()); + verify(httpResponse).end(eq("[\"bidder1\",\"bidder2\",\"bidder3\"]")); + } + @Test + public void shouldRespondWithExpectedBodyAndStatusOkForEnabledOnlyTrueFlag() { + // given + given(routingContext.queryParams()) + .willReturn(MultiMap.caseInsensitiveMultiMap().add("enabledonly", "true")); + given(bidderCatalog.isActive("bidder3")).willReturn(true); + given(bidderCatalog.names()).willReturn(new HashSet<>(asList("bidder2", "bidder3", "bidder1"))); handler = new BiddersHandler(bidderCatalog, jacksonMapper); // when handler.handle(routingContext); // then - verify(httpResponse).end(eq("[\"bidder1\",\"bidder1-alias\"]")); + verify(httpResponse).setStatusCode(HttpResponseStatus.OK.code()); + verify(httpResponse).end(eq("[\"bidder3\"]")); } } diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java index 32513a625f4..6314e76626d 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java @@ -23,13 +23,14 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.AmpEvent; import org.prebid.server.analytics.model.HttpContext; -import org.prebid.server.auction.AmpRequestFactory; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.DebugContext; +import org.prebid.server.auction.requestfactory.AmpRequestFactory; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.cookie.UidsCookie; @@ -43,11 +44,11 @@ import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtModules; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; import org.prebid.server.util.HttpUtil; @@ -93,7 +94,7 @@ public class AmpHandlerTest extends VertxTest { @Mock private BidderCatalog bidderCatalog; @Mock - private AnalyticsReporter analyticsReporter; + private AnalyticsReporterDelegator analyticsReporterDelegator; @Mock private Metrics metrics; @Mock @@ -135,7 +136,7 @@ public void setUp() { ampHandler = new AmpHandler( ampRequestFactory, exchangeService, - analyticsReporter, + analyticsReporterDelegator, metrics, clock, bidderCatalog, @@ -194,9 +195,6 @@ public void shouldRespondWithBadRequestIfRequestIsInvalid() { verifyZeroInteractions(exchangeService); verify(httpResponse).setStatusCode(eq(400)); - // TODO adminManager: enable when admin endpoints can be bound on application port - //verify(adminManager).accept(eq(AdminManager.COUNTER_KEY), any(), any()); - assertThat(httpResponse.headers()).hasSize(2) .extracting(Map.Entry::getKey, Map.Entry::getValue) .containsOnly( @@ -399,45 +397,57 @@ public void shouldRespondWithCustomTargetingIncluded() { @Test public void shouldRespondWithDebugInfoIncludedIfTestFlagIsTrue() { // given - final AuctionContext auctionContext = givenAuctionContext(builder -> builder.id("reqId1").test(1)); + final AuctionContext auctionContext = givenAuctionContext(builder -> builder.id("reqId1")).toBuilder() + .debugContext(DebugContext.of(true, null)) + .build(); given(ampRequestFactory.fromRequest(any(), anyLong())) .willReturn(Future.succeededFuture(auctionContext)); given(exchangeService.holdAuction(any())) - .willReturn(givenBidResponseWithExt(mapper.valueToTree( - ExtBidResponse.of(ExtResponseDebug.of(null, auctionContext.getBidRequest()), null, null, null, - null, ExtBidResponsePrebid.of(1000L))))); + .willReturn(givenBidResponseWithExt( + ExtBidResponse.builder() + .debug(ExtResponseDebug.of(null, auctionContext.getBidRequest(), null, null)) + .prebid(ExtBidResponsePrebid.of(1000L, null)) + .build())); // when ampHandler.handle(routingContext); // then verify(httpResponse).end(eq( - "{\"targeting\":{},\"debug\":{\"resolvedrequest\":{\"id\":\"reqId1\",\"imp\":[],\"test\":1," - + "\"tmax\":5000}}}")); + "{\"targeting\":{},\"debug\":{\"resolvedrequest\":{\"id\":\"reqId1\",\"imp\":[],\"tmax\":5000}}}")); } @Test - public void shouldRespondWithDebugInfoIncludedIfExtPrebidDebugIsOn() { + public void shouldRespondWithHooksDebugAndTraceOutput() { // given - final AuctionContext auctionContext = givenAuctionContext(builder -> builder - .id("reqId1") - .ext(ExtRequest.of(ExtRequestPrebid.builder().debug(1).build()))); + final AuctionContext auctionContext = givenAuctionContext(identity()); given(ampRequestFactory.fromRequest(any(), anyLong())) .willReturn(Future.succeededFuture(auctionContext)); given(exchangeService.holdAuction(any())) - .willReturn(givenBidResponseWithExt(mapper.valueToTree( - ExtBidResponse.of(ExtResponseDebug.of(null, auctionContext.getBidRequest()), null, null, null, - null, ExtBidResponsePrebid.of(1000L))))); + .willReturn(givenBidResponseWithExt( + ExtBidResponse.builder() + .prebid(ExtBidResponsePrebid.of( + 1000L, + ExtModules.of( + singletonMap( + "module1", singletonMap("hook1", singletonList("error1"))), + singletonMap( + "module1", singletonMap("hook1", singletonList("warning1"))), + ExtModulesTrace.of(2L, emptyList())))) + .build())); // when ampHandler.handle(routingContext); // then - verify(httpResponse).end( - eq("{\"targeting\":{},\"debug\":{\"resolvedrequest\":{\"id\":\"reqId1\",\"imp\":[],\"tmax\":5000," - + "\"ext\":{\"prebid\":{\"debug\":1}}}}}")); + verify(httpResponse).end(eq( + "{\"targeting\":{}," + + "\"ext\":{\"prebid\":{\"modules\":{" + + "\"errors\":{\"module1\":{\"hook1\":[\"error1\"]}}," + + "\"warnings\":{\"module1\":{\"hook1\":[\"warning1\"]}}," + + "\"trace\":{\"executiontimemillis\":2,\"stages\":[]}}}}}")); } @Test @@ -471,7 +481,7 @@ public void shouldIncrementAppRequestMetrics() { ampHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyBoolean(), anyInt()); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt()); } @Test @@ -493,7 +503,7 @@ public void shouldIncrementNoCookieMetrics() { ampHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), eq(true), anyInt()); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt()); } @Test @@ -511,7 +521,7 @@ public void shouldIncrementImpsRequestedMetrics() { ampHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), anyBoolean(), eq(1)); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1)); } @Test @@ -583,7 +593,7 @@ public void shouldUpdateRequestTimeMetric() { ampHandler.handle(routingContext); // then - verify(metrics).updateRequestTimeMetric(eq(500L)); + verify(metrics).updateRequestTimeMetric(eq(MetricName.request_time), eq(500L)); } @Test @@ -757,6 +767,7 @@ private AuctionContext givenAuctionContext( .bidRequest(bidRequest) .requestTypeMetric(MetricName.amp) .timeout(timeout) + .debugContext(DebugContext.empty()) .build(); } @@ -770,7 +781,7 @@ private static Future givenBidResponse(ObjectNode extBid) { .build()); } - private static Future givenBidResponseWithExt(ObjectNode extBidResponse) { + private static Future givenBidResponseWithExt(ExtBidResponse extBidResponse) { return Future.succeededFuture(BidResponse.builder() .ext(extBidResponse) .build()); @@ -784,7 +795,7 @@ private AuctionContext captureAuctionContext() { private AmpEvent captureAmpEvent() { final ArgumentCaptor captor = ArgumentCaptor.forClass(AmpEvent.class); - verify(analyticsReporter).processEvent(captor.capture()); + verify(analyticsReporterDelegator).processEvent(captor.capture(), any()); return captor.getValue(); } diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java index cfca6fb8d69..038f5c723d4 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java @@ -19,12 +19,12 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.model.HttpContext; -import org.prebid.server.auction.AuctionRequestFactory; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.requestfactory.AuctionRequestFactory; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.BlacklistedAccountException; import org.prebid.server.exception.BlacklistedAppException; @@ -81,7 +81,7 @@ public class AuctionHandlerTest extends VertxTest { @Mock private ExchangeService exchangeService; @Mock - private AnalyticsReporter analyticsReporter; + private AnalyticsReporterDelegator analyticsReporterDelegator; @Mock private Metrics metrics; @Mock @@ -119,7 +119,7 @@ public void setUp() { auctionHandler = new AuctionHandler( auctionRequestFactory, exchangeService, - analyticsReporter, + analyticsReporterDelegator, metrics, clock, httpInteractionLogger, @@ -310,8 +310,9 @@ public void shouldRespondWithCorrectResolvedRequestMediaTypePriceGranularity() { .build(); given(exchangeService.holdAuction(any())) .willReturn(Future.succeededFuture(BidResponse.builder() - .ext(mapper.valueToTree(ExtBidResponse.of(ExtResponseDebug.of(null, resolvedRequest), - null, null, null, null, null))) + .ext(ExtBidResponse.builder() + .debug(ExtResponseDebug.of(null, resolvedRequest, null, null)) + .build()) .build())); // when @@ -369,7 +370,7 @@ public void shouldIncrementAppRequestMetrics() { auctionHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyBoolean(), anyInt()); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt()); } @Test @@ -390,7 +391,7 @@ public void shouldIncrementNoCookieMetrics() { auctionHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), eq(true), anyInt()); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt()); } @Test @@ -407,7 +408,7 @@ public void shouldIncrementImpsRequestedMetrics() { auctionHandler.handle(routingContext); // then - verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), anyBoolean(), eq(1)); + verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1)); } @Test @@ -478,7 +479,7 @@ public void shouldUpdateRequestTimeMetric() { auctionHandler.handle(routingContext); // then - verify(metrics).updateRequestTimeMetric(eq(500L)); + verify(metrics).updateRequestTimeMetric(eq(MetricName.request_time), eq(500L)); } @Test @@ -555,8 +556,8 @@ public void shouldUpdateNetworkErrorMetricIfClientClosedConnection() { public void shouldIncrementRejectedMetricsIfUnknownUser() { // given given(auctionRequestFactory.fromRequest(any(), anyLong())).willReturn( - Future.failedFuture(new UnauthorizedAccountException("Unauthorised account id 1", "1")) - ); + Future.failedFuture(new UnauthorizedAccountException("Unauthorised account id 1", "1"))); + // when auctionHandler.handle(routingContext); @@ -574,9 +575,6 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid() auctionHandler.handle(routingContext); // then - // TODO adminManager: enable when admin endpoints can be bound on application port - //verify(adminManager).accept(eq(AdminManager.COUNTER_KEY), any(), any()); - final AuctionEvent auctionEvent = captureAuctionEvent(); assertThat(auctionEvent).isEqualTo(AuctionEvent.builder() .httpContext(givenHttpContext()) @@ -682,7 +680,7 @@ private AuctionContext captureAuctionContext() { private AuctionEvent captureAuctionEvent() { final ArgumentCaptor captor = ArgumentCaptor.forClass(AuctionEvent.class); - verify(analyticsReporter).processEvent(captor.capture()); + verify(analyticsReporterDelegator).processEvent(captor.capture(), any()); return captor.getValue(); } diff --git a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java index 947645bbe1a..3eb31e4a11a 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java @@ -17,12 +17,12 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.auction.ExchangeService; -import org.prebid.server.auction.VideoRequestFactory; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.WithPodErrors; +import org.prebid.server.auction.requestfactory.VideoRequestFactory; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; @@ -63,7 +63,7 @@ public class VideoHandlerTest extends VertxTest { @Mock private ExchangeService exchangeService; @Mock - private AnalyticsReporter analyticsReporter; + private AnalyticsReporterDelegator analyticsReporterDelegator; @Mock private Metrics metrics; @Mock @@ -98,8 +98,8 @@ public void setUp() { given(exchangeService.holdAuction(any())).willReturn(Future.succeededFuture(BidResponse.builder().build())); - videoHandler = new VideoHandler(videoRequestFactory, videoResponseFactory, exchangeService, analyticsReporter, - metrics, clock, jacksonMapper); + videoHandler = new VideoHandler(videoRequestFactory, videoResponseFactory, exchangeService, + analyticsReporterDelegator, metrics, clock, jacksonMapper); } @Test diff --git a/src/test/java/org/prebid/server/health/HealthMonitorTest.java b/src/test/java/org/prebid/server/health/HealthMonitorTest.java new file mode 100644 index 00000000000..adf8f6fbe66 --- /dev/null +++ b/src/test/java/org/prebid/server/health/HealthMonitorTest.java @@ -0,0 +1,54 @@ +package org.prebid.server.health; + +import org.junit.Before; +import org.junit.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HealthMonitorTest { + + private HealthMonitor healthMonitor; + + @Before + public void setUp() { + healthMonitor = new HealthMonitor(); + } + + @Test + public void calculateHealthIndexShouldReturnFullHealthIfNoRequestsSubmitted() { + // when + final BigDecimal result = healthMonitor.calculateHealthIndex(); + + // then + assertThat(result).isEqualTo(BigDecimal.ONE); + } + + @Test + public void calculateHealthIndexShouldReturnExpectedResult() { + // when + healthMonitor.incTotal(); + healthMonitor.incTotal(); + healthMonitor.incTotal(); + healthMonitor.incSuccess(); + + final BigDecimal result = healthMonitor.calculateHealthIndex(); + + // then + assertThat(result).isEqualTo(new BigDecimal("0.33")); + } + + @Test + public void calculateHealthIndexShouldResetResult() { + // when + healthMonitor.incTotal(); + healthMonitor.incSuccess(); + healthMonitor.calculateHealthIndex(); + + final BigDecimal result = healthMonitor.calculateHealthIndex(); + + // then + assertThat(result).isEqualTo(BigDecimal.ONE); + } +} diff --git a/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java b/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java new file mode 100644 index 00000000000..fca5e0f4543 --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java @@ -0,0 +1,176 @@ +package org.prebid.server.hooks.execution; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; +import org.prebid.server.hooks.v1.auction.AuctionResponseHook; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; +import org.prebid.server.hooks.v1.bidder.BidderRequestHook; +import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; +import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; +import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; + +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class HookCatalogTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private Module sampleModule; + @Mock + private Hook sampleHook; + + private HookCatalog hookCatalog; + + @Before + public void setUp() { + given(sampleModule.code()).willReturn("sample-module"); + + hookCatalog = new HookCatalog(singleton(sampleModule)); + } + + @Test + public void hookByIdShouldTolerateUnknownModule() { + // when + final EntrypointHook foundHook = hookCatalog.hookById( + "unknown-module", null, StageWithHookType.ENTRYPOINT); + + // then + assertThat(foundHook).isNull(); + } + + @Test + public void hookByIdShouldTolerateUnknownHook() { + // when + final EntrypointHook foundHook = hookCatalog.hookById( + "sample-module", "unknown-hook", StageWithHookType.ENTRYPOINT); + + // then + assertThat(foundHook).isNull(); + } + + @Test + public void hookByIdShouldReturnEntrypointHook() { + // given + givenHook(EntrypointHook.class); + + // when + final EntrypointHook foundHook = hookCatalog.hookById( + "sample-module", "sample-hook", StageWithHookType.ENTRYPOINT); + + // then + assertThat(foundHook).isNotNull() + .extracting(Hook::code) + .containsOnly("sample-hook"); + } + + @Test + public void hookByIdShouldReturnRawAuctionRequestHook() { + // given + givenHook(RawAuctionRequestHook.class); + + // when + final RawAuctionRequestHook foundHook = hookCatalog.hookById( + "sample-module", "sample-hook", StageWithHookType.RAW_AUCTION_REQUEST); + + // then + assertThat(foundHook).isNotNull() + .extracting(Hook::code) + .containsOnly("sample-hook"); + } + + @Test + public void hookByIdShouldReturnProcessedAuctionRequestHook() { + // given + givenHook(ProcessedAuctionRequestHook.class); + + // when + final ProcessedAuctionRequestHook foundHook = hookCatalog.hookById( + "sample-module", "sample-hook", StageWithHookType.PROCESSED_AUCTION_REQUEST); + + // then + assertThat(foundHook).isNotNull() + .extracting(Hook::code) + .containsOnly("sample-hook"); + } + + @Test + public void hookByIdShouldReturnBidderRequestHook() { + // given + givenHook(BidderRequestHook.class); + + // when + final BidderRequestHook foundHook = hookCatalog.hookById( + "sample-module", "sample-hook", StageWithHookType.BIDDER_REQUEST); + + // then + assertThat(foundHook).isNotNull() + .extracting(Hook::code) + .containsOnly("sample-hook"); + } + + @Test + public void hookByIdShouldReturnRawBidderResponseHook() { + // given + givenHook(RawBidderResponseHook.class); + + // when + final RawBidderResponseHook foundHook = hookCatalog.hookById( + "sample-module", "sample-hook", StageWithHookType.RAW_BIDDER_RESPONSE); + + // then + assertThat(foundHook).isNotNull() + .extracting(Hook::code) + .containsOnly("sample-hook"); + } + + @Test + public void hookByIdShouldReturnProcessedBidderResponseHook() { + // given + givenHook(ProcessedBidderResponseHook.class); + + // when + final ProcessedBidderResponseHook foundHook = hookCatalog.hookById( + "sample-module", "sample-hook", StageWithHookType.PROCESSED_BIDDER_RESPONSE); + + // then + assertThat(foundHook).isNotNull() + .extracting(Hook::code) + .containsOnly("sample-hook"); + } + + @Test + public void hookByIdShouldReturnAuctionResponseHook() { + // given + givenHook(AuctionResponseHook.class); + + // when + final AuctionResponseHook foundHook = hookCatalog.hookById( + "sample-module", "sample-hook", StageWithHookType.AUCTION_RESPONSE); + + // then + assertThat(foundHook).isNotNull() + .extracting(Hook::code) + .containsOnly("sample-hook"); + } + + private void givenHook(Class> clazz) { + sampleHook = mock(clazz); + given(sampleHook.code()).willReturn("sample-hook"); + doReturn(singleton(sampleHook)).when(sampleModule).hooks(); + } +} diff --git a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java new file mode 100644 index 00000000000..160aed2502d --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java @@ -0,0 +1,2865 @@ +package org.prebid.server.hooks.execution; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import lombok.Value; +import lombok.experimental.NonFinal; +import org.apache.commons.lang3.StringUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.auction.model.DebugContext; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionGroup; +import org.prebid.server.hooks.execution.model.ExecutionPlan; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.model.StageExecutionPlan; +import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; +import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; +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.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.v1.analytics.ResultImpl; +import org.prebid.server.hooks.v1.analytics.TagsImpl; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.AuctionResponseHook; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderRequestHook; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; +import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; +import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; +import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.Endpoint; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountHooksConfiguration; + +import java.time.Clock; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.prebid.server.assertion.FutureAssertion.assertThat; +import static org.prebid.server.hooks.v1.PayloadUpdate.identity; + +@RunWith(VertxUnitRunner.class) +public class HookStageExecutorTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private HookCatalog hookCatalog; + private TimeoutFactory timeoutFactory; + private Vertx vertx; + private Clock clock; + + @Before + public void setUp() { + vertx = Vertx.vertx(); + clock = Clock.systemUTC(); + timeoutFactory = new TimeoutFactory(Clock.fixed(clock.instant(), ZoneOffset.UTC)); + } + + @After + public void tearDown(TestContext context) { + vertx.close(context.asyncAssertSuccess()); + } + + @Test + public void creationShouldFailWhenExecutionPlanIsInvalid() { + assertThatThrownBy(() -> createExecutor("{endpoints: {abc: {}}}")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Hooks execution plan could not be parsed"); + } + + @Test + public void creationShouldFailWhenHostExecutionPlanHasUnknownHook() { + final String hostPlan = executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.entrypoint, StageExecutionPlan.of(singletonList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a"))))))))); + + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) + .willReturn(null); + + givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultImpl.noAction())); + + assertThatThrownBy(() -> createExecutor(hostPlan)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Hooks execution plan contains unknown or disabled hook: " + + "stage=entrypoint, hookId=HookId(moduleCode=module-alpha, hookImplCode=hook-a)"); + } + + @Test + public void creationShouldFailWhenDefaultAccountExecutionPlanHasUnknownHook() { + final String defaultAccountPlan = executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.entrypoint, StageExecutionPlan.of(singletonList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a"))))))))); + + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) + .willReturn(null); + + givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultImpl.noAction())); + + assertThatThrownBy(() -> createExecutor(null, defaultAccountPlan)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Hooks execution plan contains unknown or disabled hook: " + + "stage=entrypoint, hookId=HookId(moduleCode=module-alpha, hookImplCode=hook-a)"); + } + + @Test + public void shouldTolerateMissingHostAndDefaultAccountExecutionPlans() { + // given + final HookStageExecutor executor = createExecutor(null, null); + + final CaseInsensitiveMultiMap queryParams = CaseInsensitiveMultiMap.empty(); + final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.empty(); + final String body = "body"; + + // when + final Future> future = executor.executeEntrypointStage( + queryParams, headers, body, HookExecutionContext.of(Endpoint.openrtb2_auction)); + + // then + assertThat(future).isSucceeded(); + + final EntrypointPayload payload = future.result().getPayload(); + assertThat(payload.queryParams()).isSameAs(queryParams); + assertThat(payload.headers()).isSameAs(headers); + assertThat(payload.body()).isSameAs(body); + } + + @Test + public void shouldTolerateMissingAllExecutionPlans() { + // given + final HookStageExecutor executor = createExecutor(null, null); + + // when + final BidRequest bidRequest = BidRequest.builder().build(); + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(bidRequest) + .account(Account.empty("accountId")) + .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_auction)) + .debugContext(DebugContext.empty()) + .build()); + + // then + assertThat(future).isSucceeded(); + + final AuctionRequestPayload payload = future.result().getPayload(); + assertThat(payload.bidRequest()).isSameAs(bidRequest); + } + + @Test + public void shouldExecuteEntrypointHooksHappyPath(TestContext context) { + // given + givenEntrypointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-abc")))); + + givenEntrypointHook( + "module-alpha", + "hook-b", + delayedHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-def")), 40)); + + givenEntrypointHook( + "module-beta", + "hook-a", + delayedHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-ghi")), 80)); + + givenEntrypointHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.entrypoint, execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result).isNotNull(); + assertThat(result.isShouldReject()).isFalse(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.body()).isEqualTo("body-abc-ghi-jkl-def")); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasSize(1) + .hasEntrySatisfying( + Stage.entrypoint, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .hasOnlyOneElementSatisfying(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("http-request"); + + final List groups = stageOutcome.getGroups(); + assertThat(groups).hasSize(2); + + final List group0Hooks = groups.get(0).getHooks(); + assertThat(group0Hooks).hasSize(2); + + assertThat(group0Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + assertThat(hookOutcome.getExecutionTime()).isBetween(0L, 10L); + }); + + assertThat(group0Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + assertThat(hookOutcome.getExecutionTime()).isBetween(80L, 90L); + }); + + final List group1Hooks = groups.get(1).getHooks(); + assertThat(group1Hooks).hasSize(2); + + assertThat(group1Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-b")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + assertThat(hookOutcome.getExecutionTime()).isBetween(0L, 10L); + }); + + assertThat(group1Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-b")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + assertThat(hookOutcome.getExecutionTime()).isBetween(40L, 50L); + }); + })); + + async.complete(); + })); + + async.awaitSuccess(150L); + } + + @Test + public void shouldBypassEntrypointHooksWhenNoPlanForEndpoint(TestContext context) { + // given + final HookStageExecutor executor = createExecutor( + executionPlan(emptyMap())); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_amp); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.getPayload()).satisfies(payload -> + assertThat(payload.body()).isEqualTo("body")); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasSize(1) + .containsEntry( + Stage.entrypoint, + singletonList(StageExecutionOutcome.of("http-request", emptyList()))); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldBypassEntrypointHooksWhenNoPlanForStage(TestContext context) { + // given + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(emptyMap())))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.getPayload()).satisfies(payload -> + assertThat(payload.body()).isEqualTo("body")); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasSize(1) + .containsEntry( + Stage.entrypoint, + singletonList(StageExecutionOutcome.of("http-request", emptyList()))); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteEntrypointHooksToleratingMisbehavingHooks(TestContext context) { + // given + // hook implementation returns null + givenEntrypointHook( + "module-alpha", + "hook-a", + (payload, invocationContext) -> null); + + // hook implementation returns null + givenEntrypointHook( + "module-alpha", + "hook-b", + (payload, invocationContext) -> null); + + // hook implementation throws exception + givenEntrypointHook( + "module-beta", + "hook-a", + (payload, invocationContext) -> { + throw new RuntimeException("I'm not allowed to throw exceptions"); + }); + + givenEntrypointHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.entrypoint, execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.body()).isEqualTo("body-jkl")); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.entrypoint, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .hasOnlyOneElementSatisfying(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("http-request"); + + final List groups = stageOutcome.getGroups(); + + final List group0Hooks = groups.get(0).getHooks(); + assertThat(group0Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()) + .isEqualTo(ExecutionStatus.invocation_failure); + assertThat(hookOutcome.getMessage()).isEqualTo("Action returned null"); + assertThat(hookOutcome.getExecutionTime()).isBetween(0L, 10L); + }); + + assertThat(group0Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-a")); + assertThat(hookOutcome.getStatus()) + .isEqualTo(ExecutionStatus.invocation_failure); + assertThat(hookOutcome.getMessage()).isEqualTo( + "java.lang.RuntimeException: I'm not allowed to throw exceptions"); + assertThat(hookOutcome.getExecutionTime()).isBetween(0L, 10L); + }); + + final List group1Hooks = groups.get(1).getHooks(); + assertThat(group1Hooks).hasSize(2); + + assertThat(group1Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-b")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + assertThat(hookOutcome.getExecutionTime()).isBetween(0L, 10L); + }); + + assertThat(group1Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-b")); + assertThat(hookOutcome.getStatus()) + .isEqualTo(ExecutionStatus.invocation_failure); + assertThat(hookOutcome.getMessage()).isEqualTo("Action returned null"); + assertThat(hookOutcome.getExecutionTime()).isBetween(0L, 10L); + }); + })); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteEntrypointHooksToleratingTimeoutAndFailedFuture(TestContext context) { + // given + // hook implementation returns future failing after a while + givenEntrypointHook( + "module-alpha", + "hook-a", + (payload, invocationContext) -> { + final Promise> promise = Promise.promise(); + vertx.setTimer(50L, timerId -> promise.fail(new RuntimeException("Failed after a while"))); + return promise.future(); + }); + + // hook implementation takes too long + givenEntrypointHook( + "module-alpha", + "hook-b", + delayedHook( + InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-def")), + 250)); + + // hook implementation takes too long + givenEntrypointHook( + "module-beta", + "hook-a", + delayedHook( + InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-ghi")), + 250)); + + givenEntrypointHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.entrypoint, execPlanTwoGroupsTwoHooksEach()))))); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.body()).isEqualTo("body-jkl")); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.entrypoint, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .hasOnlyOneElementSatisfying(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("http-request"); + + final List groups = stageOutcome.getGroups(); + + final List group0Hooks = groups.get(0).getHooks(); + assertThat(group0Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()) + .isEqualTo(ExecutionStatus.execution_failure); + assertThat(hookOutcome.getMessage()).isEqualTo("Failed after a while"); + assertThat(hookOutcome.getExecutionTime()).isBetween(50L, 60L); + }); + + assertThat(group0Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.timeout); + assertThat(hookOutcome.getMessage()).isEqualTo( + "Timed out while executing action"); + assertThat(hookOutcome.getExecutionTime()).isBetween(200L, 210L); + }); + + final List group1Hooks = groups.get(1).getHooks(); + assertThat(group1Hooks).hasSize(2); + + assertThat(group1Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-b")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + assertThat(hookOutcome.getExecutionTime()).isBetween(0L, 10L); + }); + + assertThat(group1Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-b")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.timeout); + assertThat(hookOutcome.getMessage()).isEqualTo( + "Timed out while executing action"); + assertThat(hookOutcome.getExecutionTime()).isBetween(200L, 210L); + }); + })); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteEntrypointHooksHonoringStatusAndAction(TestContext context) { + // given + givenEntrypointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.failed("Failed to contact service ACME"))); + + givenEntrypointHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultImpl.noAction())); + + givenEntrypointHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-ghi")))); + + givenEntrypointHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.entrypoint, execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.getPayload()).satisfies(payload -> + assertThat(payload.body()).isEqualTo("body-ghi-jkl")); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.entrypoint, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .hasOnlyOneElementSatisfying(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("http-request"); + + final List groups = stageOutcome.getGroups(); + + final List group0Hooks = groups.get(0).getHooks(); + assertThat(group0Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.failure); + assertThat(hookOutcome.getMessage()).isEqualTo( + "Failed to contact service ACME"); + }); + + assertThat(group0Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + }); + + final List group1Hooks = groups.get(1).getHooks(); + assertThat(group1Hooks).hasSize(2); + + assertThat(group1Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-b")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + }); + + assertThat(group1Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-b")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.no_action); + }); + })); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteEntrypointHooksWhenRequestIsRejectedByFirstGroup(TestContext context) { + // given + givenEntrypointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-abc")))); + + givenEntrypointHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.rejected("Request is of low quality"))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.entrypoint, + StageExecutionPlan.of(singletonList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a")))))))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.isShouldReject()).isTrue(); + assertThat(result.getPayload()).isNull(); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.entrypoint, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .hasOnlyOneElementSatisfying(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("http-request"); + + final List groups = stageOutcome.getGroups(); + assertThat(groups).hasSize(1); + + final List group0Hooks = groups.get(0).getHooks(); + assertThat(group0Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + }); + + assertThat(group0Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.reject); + assertThat(hookOutcome.getMessage()).isEqualTo("Request is of low quality"); + }); + })); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteEntrypointHooksWhenRequestIsRejectedBySecondGroup(TestContext context) { + // given + givenEntrypointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-abc")))); + + givenEntrypointHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultImpl.rejected("Request is of low quality"))); + + givenEntrypointHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-def")))); + + givenEntrypointHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.entrypoint, execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.isShouldReject()).isTrue(); + assertThat(result.getPayload()).isNull(); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.entrypoint, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .hasOnlyOneElementSatisfying(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("http-request"); + + final List groups = stageOutcome.getGroups(); + assertThat(groups).hasSize(2); + + final List group0Hooks = groups.get(0).getHooks(); + assertThat(group0Hooks).hasSize(2); + + assertThat(group0Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + }); + + assertThat(group0Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + }); + + final List group1Hooks = groups.get(1).getHooks(); + assertThat(group1Hooks).hasSize(2); + + assertThat(group1Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-b")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + }); + + assertThat(group1Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-b")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.reject); + assertThat(hookOutcome.getMessage()).isEqualTo("Request is of low quality"); + }); + })); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteEntrypointHooksToleratingMisbehavingInvocationResult(TestContext context) { + // given + givenEntrypointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.builder().build())); + + givenEntrypointHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .build())); + + givenEntrypointHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .build())); + + givenEntrypointHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> { + throw new RuntimeException("Can not alter payload"); + }))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.entrypoint, execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.body()).isEqualTo("body")); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.entrypoint, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .hasOnlyOneElementSatisfying(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("http-request"); + + final List groups = stageOutcome.getGroups(); + + final List group0Hooks = groups.get(0).getHooks(); + assertThat(group0Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()).isNull(); + }); + + assertThat(group0Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-a")); + assertThat(hookOutcome.getStatus()) + .isEqualTo(ExecutionStatus.execution_failure); + assertThat(hookOutcome.getMessage()) + .isEqualTo("Payload update is missing in invocation result"); + }); + + final List group1Hooks = groups.get(1).getHooks(); + assertThat(group1Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-b")); + assertThat(hookOutcome.getStatus()) + .isEqualTo(ExecutionStatus.execution_failure); + assertThat(hookOutcome.getMessage()) + .isEqualTo("Payload update has thrown an exception: " + + "java.lang.RuntimeException: Can not alter payload"); + }); + + assertThat(group1Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-b")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isNull(); + }); + })); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteEntrypointHooksAndStoreResultInExecutionContext(TestContext context) { + // given + final TagsImpl analyticsTags = TagsImpl.of(singletonList(ActivityImpl.of( + "update", + "success", + singletonList(ResultImpl.of( + "success", + null, + AppliedToImpl.builder().request(true).build()))))); + + givenEntrypointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .message("Updated the request") + .action(InvocationAction.update) + .payloadUpdate(identity()) + .errors(singletonList("There have been some errors though")) + .warnings(singletonList("Not without warnings too")) + .debugMessages(singletonList("And chatty debug messages of course")) + .analyticsTags(analyticsTags) + .build())); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.entrypoint, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.entrypoint, + stageOutcomes -> + assertThat(stageOutcomes.get(0).getGroups().get(0).getHooks().get(0)) + .satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getMessage()).isEqualTo("Updated the request"); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + assertThat(hookOutcome.getErrors()) + .containsOnly("There have been some errors though"); + assertThat(hookOutcome.getWarnings()) + .containsOnly("Not without warnings too"); + assertThat(hookOutcome.getDebugMessages()) + .containsOnly("And chatty debug messages of course"); + assertThat(hookOutcome.getAnalyticsTags()).isSameAs(analyticsTags); + })); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteEntrypointHooksAndPassInvocationContext(TestContext context) { + // given + final EntrypointHookImpl hookImpl = spy( + EntrypointHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.ENTRYPOINT))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.ENTRYPOINT))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.entrypoint, execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeEntrypointStage( + CaseInsensitiveMultiMap.empty(), + CaseInsensitiveMultiMap.empty(), + "body", + hookExecutionContext); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(InvocationContext.class); + verify(hookImpl, times(4)).call(any(), invocationContextCaptor.capture()); + final List capturedContexts = invocationContextCaptor.getAllValues(); + + assertThat(capturedContexts.get(0)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isEqualTo(Endpoint.openrtb2_auction); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.timeout().remaining()).isEqualTo(200L); + }); + + assertThat(capturedContexts.get(1)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isEqualTo(Endpoint.openrtb2_auction); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.timeout().remaining()).isEqualTo(200L); + }); + + assertThat(capturedContexts.get(2)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isEqualTo(Endpoint.openrtb2_auction); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.timeout().remaining()).isEqualTo(200L); + }); + + assertThat(capturedContexts.get(3)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isEqualTo(Endpoint.openrtb2_auction); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.timeout().remaining()).isEqualTo(200L); + }); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(TestContext context) { + // given + final RawAuctionRequestHookImpl hookImpl = spy( + RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.noAction()))); + given(hookCatalog.hookById(anyString(), anyString(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + + final String hostPlan = executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, execPlanOneGroupOneHook("module-alpha", "hook-a"))))); + final String defaultAccountPlan = executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, execPlanOneGroupOneHook("module-alpha", "hook-b"))))); + final HookStageExecutor executor = createExecutor(hostPlan, defaultAccountPlan); + + final BidRequest bidRequest = BidRequest.builder().build(); + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(bidRequest) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidRequest()).isSameAs(bidRequest)); + + verify(hookImpl, times(2)).call(any(), any()); + verify(hookCatalog, times(2)) + .hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + verify(hookCatalog, times(2)) + .hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan(TestContext context) { + // given + final RawAuctionRequestHookImpl hookImpl = spy( + RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.noAction()))); + given(hookCatalog.hookById(anyString(), anyString(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + + final String hostPlan = executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, + execPlanOneGroupOneHook("module-alpha", "hook-a"))))); + final String defaultAccountPlan = executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, + execPlanOneGroupOneHook("module-alpha", "hook-b"))))); + final HookStageExecutor executor = createExecutor(hostPlan, defaultAccountPlan); + + final BidRequest bidRequest = BidRequest.builder().build(); + final ExecutionPlan accountPlan = ExecutionPlan.of(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, + execPlanOneGroupOneHook("module-beta", "hook-b"))))); + final Account account = Account.builder() + .id("accountId") + .hooks(AccountHooksConfiguration.of(accountPlan, null)) + .build(); + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(bidRequest) + .account(account) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidRequest()).isSameAs(bidRequest)); + + verify(hookImpl, times(2)).call(any(), any()); + verify(hookCatalog, times(2)) + .hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + verify(hookCatalog) + .hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + verify(hookCatalog) + .hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPlan(TestContext context) { + // given + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(null); + + givenRawAuctionRequestHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().id("id").build())))); + + final HookStageExecutor executor = createExecutor(null, null); + + final ExecutionPlan accountPlan = ExecutionPlan.of(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, + StageExecutionPlan.of(singletonList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a"))))))))); + final Account account = Account.builder() + .id("accountId") + .hooks(AccountHooksConfiguration.of(accountPlan, null)) + .build(); + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(account) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidRequest()).isEqualTo(BidRequest.builder() + .id("id") + .build())); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.raw_auction_request, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .hasOnlyOneElementSatisfying(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("auction-request"); + + final List groups = stageOutcome.getGroups(); + + final List group0Hooks = groups.get(0).getHooks(); + assertThat(group0Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()) + .isEqualTo(ExecutionStatus.invocation_failure); + assertThat(hookOutcome.getMessage()).isEqualTo( + "Hook implementation does not exist or disabled"); + assertThat(hookOutcome.getExecutionTime()).isBetween(0L, 10L); + }); + + assertThat(group0Hooks.get(1)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-beta", "hook-a")); + assertThat(hookOutcome.getStatus()).isEqualTo(ExecutionStatus.success); + assertThat(hookOutcome.getAction()).isEqualTo(ExecutionAction.update); + assertThat(hookOutcome.getExecutionTime()).isBetween(0L, 10L); + }); + })); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteRawAuctionRequestHooksHappyPath(TestContext context) { + // given + givenRawAuctionRequestHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().at(1).build())))); + + givenRawAuctionRequestHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().id("id").build())))); + + givenRawAuctionRequestHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().test(1).build())))); + + givenRawAuctionRequestHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().tmax(1000L).build())))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, + execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidRequest()).isEqualTo(BidRequest.builder() + .at(1) + .id("id") + .test(1) + .tmax(1000L) + .build())); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.raw_auction_request, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .extracting(StageExecutionOutcome::getEntity) + .containsOnly("auction-request")); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteRawAuctionRequestHooksAndPassAuctionInvocationContext(TestContext context) { + // given + final RawAuctionRequestHookImpl hookImpl = spy( + RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, + execPlanTwoGroupsTwoHooksEach()))))); + + final Map accountModulesConfiguration = new HashMap<>(); + final ObjectNode moduleAlphaConfiguration = mapper.createObjectNode(); + final ObjectNode moduleBetaConfiguration = mapper.createObjectNode(); + accountModulesConfiguration.put("module-alpha", moduleAlphaConfiguration); + accountModulesConfiguration.put("module-beta", moduleBetaConfiguration); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.builder() + .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration)) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(AuctionInvocationContext.class); + verify(hookImpl, times(4)).call(any(), invocationContextCaptor.capture()); + final List capturedContexts = invocationContextCaptor.getAllValues(); + + assertThat(capturedContexts.get(0)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.debugEnabled()).isFalse(); + assertThat(invocationContext.accountConfig()).isSameAs(moduleAlphaConfiguration); + }); + + assertThat(capturedContexts.get(1)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.debugEnabled()).isFalse(); + assertThat(invocationContext.accountConfig()).isSameAs(moduleBetaConfiguration); + }); + + assertThat(capturedContexts.get(2)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.debugEnabled()).isFalse(); + assertThat(invocationContext.accountConfig()).isSameAs(moduleBetaConfiguration); + }); + + assertThat(capturedContexts.get(3)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.debugEnabled()).isFalse(); + assertThat(invocationContext.accountConfig()).isSameAs(moduleAlphaConfiguration); + }); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteRawAuctionRequestHooksAndPassModuleContextBetweenHooks(TestContext context) { + // given + final RawAuctionRequestHookImpl hookImpl = spy(RawAuctionRequestHookImpl.of( + (payload, invocationContext) -> { + final Promise> promise = Promise.promise(); + vertx.setTimer(20, timerId -> promise.complete( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(identity()) + .moduleContext( + StringUtils.trimToEmpty((String) invocationContext.moduleContext()) + "a") + .build())); + return promise.future(); + })); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, + StageExecutionPlan.of(asList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a"), + HookId.of("module-alpha", "hook-c"))), + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-beta", "hook-b"), + HookId.of("module-alpha", "hook-b"), + HookId.of("module-beta", "hook-c")))))))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(AuctionInvocationContext.class); + verify(hookImpl, times(6)).call(any(), invocationContextCaptor.capture()); + final List capturedContexts = invocationContextCaptor.getAllValues(); + + assertThat(capturedContexts.get(0)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isNull()); + + assertThat(capturedContexts.get(1)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isNull()); + + assertThat(capturedContexts.get(2)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isNull()); + + assertThat(capturedContexts.get(3)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isEqualTo("a")); + + assertThat(capturedContexts.get(4)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isEqualTo("a")); + + assertThat(capturedContexts.get(5)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isEqualTo("a")); + + assertThat(hookExecutionContext.getModuleContexts()).containsOnly( + entry("module-alpha", "aa"), + entry("module-beta", "aa")); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteRawAuctionRequestHooksWhenRequestIsRejected(TestContext context) { + // given + givenRawAuctionRequestHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.rejected("Request is no good"))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.isShouldReject()).isTrue(); + assertThat(result.getPayload()).isNull(); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteProcessedAuctionRequestHooksHappyPath(TestContext context) { + // given + givenProcessedAuctionRequestHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().at(1).build())))); + + givenProcessedAuctionRequestHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().id("id").build())))); + + givenProcessedAuctionRequestHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().test(1).build())))); + + givenProcessedAuctionRequestHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().tmax(1000L).build())))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.processed_auction_request, + execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = + executor.executeProcessedAuctionRequestStage( + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidRequest()).isEqualTo(BidRequest.builder() + .at(1) + .id("id") + .test(1) + .tmax(1000L) + .build())); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.processed_auction_request, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .extracting(StageExecutionOutcome::getEntity) + .containsOnly("auction-request")); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteProcessedAuctionRequestHooksAndPassAuctionInvocationContext(TestContext context) { + // given + final ProcessedAuctionRequestHookImpl hookImpl = spy( + ProcessedAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.processed_auction_request, + execPlanTwoGroupsTwoHooksEach()))))); + + final Map accountModulesConfiguration = new HashMap<>(); + final ObjectNode moduleAlphaConfiguration = mapper.createObjectNode(); + final ObjectNode moduleBetaConfiguration = mapper.createObjectNode(); + accountModulesConfiguration.put("module-alpha", moduleAlphaConfiguration); + accountModulesConfiguration.put("module-beta", moduleBetaConfiguration); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = + executor.executeProcessedAuctionRequestStage( + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.builder() + .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration)) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(AuctionInvocationContext.class); + verify(hookImpl, times(4)).call(any(), invocationContextCaptor.capture()); + final List capturedContexts = invocationContextCaptor.getAllValues(); + + assertThat(capturedContexts.get(0)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.debugEnabled()).isFalse(); + assertThat(invocationContext.accountConfig()).isSameAs(moduleAlphaConfiguration); + }); + + assertThat(capturedContexts.get(1)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.debugEnabled()).isFalse(); + assertThat(invocationContext.accountConfig()).isSameAs(moduleBetaConfiguration); + }); + + assertThat(capturedContexts.get(2)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.debugEnabled()).isFalse(); + assertThat(invocationContext.accountConfig()).isSameAs(moduleBetaConfiguration); + }); + + assertThat(capturedContexts.get(3)).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.debugEnabled()).isFalse(); + assertThat(invocationContext.accountConfig()).isSameAs(moduleAlphaConfiguration); + }); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteProcessedAuctionRequestHooksAndPassModuleContextBetweenHooks(TestContext context) { + // given + final ProcessedAuctionRequestHookImpl hookImpl = spy(ProcessedAuctionRequestHookImpl.of( + (payload, invocationContext) -> { + final Promise> promise = Promise.promise(); + vertx.setTimer(20, timerId -> promise.complete( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(identity()) + .moduleContext( + StringUtils.trimToEmpty((String) invocationContext.moduleContext()) + "a") + .build())); + return promise.future(); + })); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(hookImpl); + given(hookCatalog.hookById(eq("module-beta"), eq("hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.processed_auction_request, + StageExecutionPlan.of(asList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a"), + HookId.of("module-alpha", "hook-c"))), + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-beta", "hook-b"), + HookId.of("module-alpha", "hook-b"), + HookId.of("module-beta", "hook-c")))))))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = + executor.executeProcessedAuctionRequestStage( + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(AuctionInvocationContext.class); + verify(hookImpl, times(6)).call(any(), invocationContextCaptor.capture()); + final List capturedContexts = invocationContextCaptor.getAllValues(); + + assertThat(capturedContexts.get(0)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isNull()); + + assertThat(capturedContexts.get(1)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isNull()); + + assertThat(capturedContexts.get(2)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isNull()); + + assertThat(capturedContexts.get(3)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isEqualTo("a")); + + assertThat(capturedContexts.get(4)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isEqualTo("a")); + + assertThat(capturedContexts.get(5)).satisfies(invocationContext -> + assertThat(invocationContext.moduleContext()).isEqualTo("a")); + + assertThat(hookExecutionContext.getModuleContexts()).containsOnly( + entry("module-alpha", "aa"), + entry("module-beta", "aa")); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteProcessedAuctionRequestHooksWhenRequestIsRejected(TestContext context) { + // given + givenProcessedAuctionRequestHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.rejected("Request is no good"))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.processed_auction_request, + execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = + executor.executeProcessedAuctionRequestStage( + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.isShouldReject()).isTrue(); + assertThat(result.getPayload()).isNull(); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteBidderRequestHooksHappyPath(TestContext context) { + // given + givenBidderRequestHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + payload.bidRequest().toBuilder().at(1).build())))); + + givenBidderRequestHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + payload.bidRequest().toBuilder().id("id").build())))); + + givenBidderRequestHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + payload.bidRequest().toBuilder().test(1).build())))); + + givenBidderRequestHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + payload.bidRequest().toBuilder().tmax(1000L).build())))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.bidder_request, + execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build(); + + // when + final Future> future1 = executor.executeBidderRequestStage( + BidderRequest.of("bidder1", null, BidRequest.builder().build()), + auctionContext); + final Future> future2 = executor.executeBidderRequestStage( + BidderRequest.of("bidder2", null, BidRequest.builder().build()), + auctionContext); + + // then + final Async async = context.async(); + future1.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidRequest()).isEqualTo(BidRequest.builder() + .at(1) + .id("id") + .test(1) + .tmax(1000L) + .build())); + + async.complete(); + })); + + CompositeFuture.join(future1, future2).setHandler(context.asyncAssertSuccess(result -> + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.bidder_request, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(2) + .extracting(StageExecutionOutcome::getEntity) + .containsOnly("bidder1", "bidder2")))); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteBidderRequestHooksAndPassBidderInvocationContext(TestContext context) { + // given + final BidderRequestHookImpl hookImpl = spy( + BidderRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.BIDDER_REQUEST))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.bidder_request, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeBidderRequestStage( + BidderRequest.of("bidder1", null, BidRequest.builder().build()), + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.builder() + .hooks(AccountHooksConfiguration.of( + null, singletonMap("module-alpha", mapper.createObjectNode()))) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(BidderInvocationContext.class); + verify(hookImpl).call(any(), invocationContextCaptor.capture()); + + assertThat(invocationContextCaptor.getValue()).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.accountConfig()).isNotNull(); + assertThat(invocationContext.bidder()).isEqualTo("bidder1"); + }); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteRawBidderResponseHooksHappyPath(TestContext context) { + // given + givenRawBidderResponseHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + payload.bids().stream() + .map(bid -> BidderBid.of( + bid.getBid().toBuilder().id("bidId").build(), + bid.getType(), + bid.getBidCurrency())) + .collect(Collectors.toList()))))); + + givenRawBidderResponseHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + payload.bids().stream() + .map(bid -> BidderBid.of( + bid.getBid().toBuilder().adid("adId").build(), + bid.getType(), + bid.getBidCurrency())) + .collect(Collectors.toList()))))); + + givenRawBidderResponseHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + payload.bids().stream() + .map(bid -> BidderBid.of( + bid.getBid().toBuilder().cid("cid").build(), + bid.getType(), + bid.getBidCurrency())) + .collect(Collectors.toList()))))); + + givenRawBidderResponseHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + payload.bids().stream() + .map(bid -> BidderBid.of( + bid.getBid().toBuilder().adm("adm").build(), + bid.getType(), + bid.getBidCurrency())) + .collect(Collectors.toList()))))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_bidder_response, + execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build(); + + // when + final Future> future1 = executor.executeRawBidderResponseStage( + BidderResponse.of( + "bidder1", + BidderSeatBid.of( + singletonList(BidderBid.of(Bid.builder().build(), BidType.banner, "USD")), + emptyList(), + emptyList()), + 0), + auctionContext); + final Future> future2 = executor.executeRawBidderResponseStage( + BidderResponse.of( + "bidder2", + BidderSeatBid.of(emptyList(), emptyList(), emptyList()), + 0), + auctionContext); + + // then + final Async async = context.async(); + future1.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bids()).containsOnly(BidderBid.of( + Bid.builder() + .id("bidId") + .adid("adId") + .cid("cid") + .adm("adm") + .build(), + BidType.banner, + "USD"))); + + async.complete(); + })); + + CompositeFuture.join(future1, future2).setHandler(context.asyncAssertSuccess(result -> + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.raw_bidder_response, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(2) + .extracting(StageExecutionOutcome::getEntity) + .containsOnly("bidder1", "bidder2")))); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteRawBidderResponseHooksAndPassBidderInvocationContext(TestContext context) { + // given + final RawBidderResponseHookImpl hookImpl = spy( + RawBidderResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.raw_bidder_response, execPlanOneGroupOneHook( + "module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeRawBidderResponseStage( + BidderResponse.of( + "bidder1", + BidderSeatBid.of( + singletonList(BidderBid.of(Bid.builder().build(), BidType.banner, "USD")), + emptyList(), + emptyList()), + 0), + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.builder() + .hooks(AccountHooksConfiguration.of( + null, singletonMap("module-alpha", mapper.createObjectNode()))) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(BidderInvocationContext.class); + verify(hookImpl).call(any(), invocationContextCaptor.capture()); + + assertThat(invocationContextCaptor.getValue()).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.accountConfig()).isNotNull(); + assertThat(invocationContext.bidder()).isEqualTo("bidder1"); + }); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteProcessedBidderResponseHooksHappyPath(TestContext context) { + // given + givenProcessedBidderResponseHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + payload.bids().stream() + .map(bid -> BidderBid.of( + bid.getBid().toBuilder().id("bidId").build(), + bid.getType(), + bid.getBidCurrency())) + .collect(Collectors.toList()))))); + + givenProcessedBidderResponseHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + payload.bids().stream() + .map(bid -> BidderBid.of( + bid.getBid().toBuilder().adid("adId").build(), + bid.getType(), + bid.getBidCurrency())) + .collect(Collectors.toList()))))); + + givenProcessedBidderResponseHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + payload.bids().stream() + .map(bid -> BidderBid.of( + bid.getBid().toBuilder().cid("cid").build(), + bid.getType(), + bid.getBidCurrency())) + .collect(Collectors.toList()))))); + + givenProcessedBidderResponseHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + payload.bids().stream() + .map(bid -> BidderBid.of( + bid.getBid().toBuilder().adm("adm").build(), + bid.getType(), + bid.getBidCurrency())) + .collect(Collectors.toList()))))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.processed_bidder_response, + execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + final AuctionContext auctionContext = AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build(); + + // when + final Future> future1 = + executor.executeProcessedBidderResponseStage( + BidderResponse.of( + "bidder1", + BidderSeatBid.of( + singletonList(BidderBid.of(Bid.builder().build(), BidType.banner, "USD")), + emptyList(), + emptyList()), + 0), + auctionContext); + final Future> future2 = + executor.executeProcessedBidderResponseStage( + BidderResponse.of( + "bidder2", + BidderSeatBid.of(emptyList(), emptyList(), emptyList()), + 0), + auctionContext); + + // then + final Async async = context.async(); + future1.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bids()).containsOnly(BidderBid.of( + Bid.builder() + .id("bidId") + .adid("adId") + .cid("cid") + .adm("adm") + .build(), + BidType.banner, + "USD"))); + + async.complete(); + })); + + CompositeFuture.join(future1, future2).setHandler(context.asyncAssertSuccess(result -> + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.processed_bidder_response, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(2) + .extracting(StageExecutionOutcome::getEntity) + .containsOnly("bidder1", "bidder2")))); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteProcessedBidderResponseHooksAndPassBidderInvocationContext(TestContext context) { + // given + final ProcessedBidderResponseHookImpl hookImpl = spy( + ProcessedBidderResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.processed_bidder_response, + execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = + executor.executeProcessedBidderResponseStage( + BidderResponse.of( + "bidder1", + BidderSeatBid.of( + singletonList(BidderBid.of(Bid.builder().build(), BidType.banner, "USD")), + emptyList(), + emptyList()), + 0), + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.builder() + .hooks(AccountHooksConfiguration.of( + null, singletonMap("module-alpha", mapper.createObjectNode()))) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(BidderInvocationContext.class); + verify(hookImpl).call(any(), invocationContextCaptor.capture()); + + assertThat(invocationContextCaptor.getValue()).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.accountConfig()).isNotNull(); + assertThat(invocationContext.bidder()).isEqualTo("bidder1"); + }); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteBidderRequestHooksWhenRequestIsRejected(TestContext context) { + // given + givenBidderRequestHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.rejected("Request is no good"))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.bidder_request, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeBidderRequestStage( + BidderRequest.of("bidder1", null, BidRequest.builder().build()), + AuctionContext.builder() + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.isShouldReject()).isTrue(); + assertThat(result.getPayload()).isNull(); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteAuctionResponseHooksHappyPath(TestContext context) { + // given + givenAuctionResponseHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + payload.bidResponse().toBuilder().id("id").build())))); + + givenAuctionResponseHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + payload.bidResponse().toBuilder().bidid("bidid").build())))); + + givenAuctionResponseHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + payload.bidResponse().toBuilder().cur("cur").build())))); + + givenAuctionResponseHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + payload.bidResponse().toBuilder().nbr(1).build())))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.auction_response, + execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeAuctionResponseStage( + BidResponse.builder().build(), + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidResponse()).isEqualTo(BidResponse.builder() + .id("id") + .bidid("bidid") + .cur("cur") + .nbr(1) + .build())); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteAuctionResponseHooksAndPassAuctionInvocationContext(TestContext context) { + // given + final AuctionResponseHookImpl hookImpl = spy( + AuctionResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.AUCTION_RESPONSE))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.auction_response, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeAuctionResponseStage( + BidResponse.builder().build(), + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.builder() + .hooks(AccountHooksConfiguration.of( + null, singletonMap("module-alpha", mapper.createObjectNode()))) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(AuctionInvocationContext.class); + verify(hookImpl).call(any(), invocationContextCaptor.capture()); + + assertThat(invocationContextCaptor.getValue()).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.accountConfig()).isNotNull(); + }); + + async.complete(); + })); + + async.awaitSuccess(); + } + + @Test + public void shouldExecuteAuctionResponseHooksAndIgnoreRejection(TestContext context) { + // given + givenAuctionResponseHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultImpl.rejected("Will not apply"))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.auction_response, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeAuctionResponseStage( + BidResponse.builder().build(), + AuctionContext.builder() + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + final Async async = context.async(); + future.setHandler(context.asyncAssertSuccess(result -> { + assertThat(result.isShouldReject()).isFalse(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidResponse()).isNotNull()); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.auction_response, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .hasOnlyOneElementSatisfying(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("auction-response"); + + final List groups = stageOutcome.getGroups(); + + final List group0Hooks = groups.get(0).getHooks(); + assertThat(group0Hooks.get(0)).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()) + .isEqualTo(ExecutionStatus.execution_failure); + assertThat(hookOutcome.getMessage()) + .isEqualTo("Rejection is not supported during this stage"); + }); + })); + + async.complete(); + })); + + async.awaitSuccess(); + } + + private String executionPlan(Map endpoints) { + return jacksonMapper.encode(ExecutionPlan.of(endpoints)); + } + + private static StageExecutionPlan execPlanTwoGroupsTwoHooksEach() { + return StageExecutionPlan.of(asList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a"))), + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-beta", "hook-b"), + HookId.of("module-alpha", "hook-b"))))); + } + + private StageExecutionPlan execPlanOneGroupOneHook(String moduleCode, String hookImplCode) { + return StageExecutionPlan.of(singletonList( + ExecutionGroup.of( + 200L, + singletonList(HookId.of(moduleCode, hookImplCode))))); + } + + private void givenEntrypointHook( + String moduleCode, + String hookImplCode, + BiFunction>> delegate) { + + given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.ENTRYPOINT))) + .willReturn(EntrypointHookImpl.of(delegate)); + } + + private void givenRawAuctionRequestHook( + String moduleCode, + String hookImplCode, + BiFunction< + AuctionRequestPayload, + AuctionInvocationContext, + Future>> delegate) { + + given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willReturn(RawAuctionRequestHookImpl.of(delegate)); + } + + private void givenProcessedAuctionRequestHook( + String moduleCode, + String hookImplCode, + BiFunction< + AuctionRequestPayload, + AuctionInvocationContext, + Future>> delegate) { + + given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + .willReturn(ProcessedAuctionRequestHookImpl.of(delegate)); + } + + private void givenBidderRequestHook( + String moduleCode, + String hookImplCode, + BiFunction< + BidderRequestPayload, + BidderInvocationContext, + Future>> delegate) { + + given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.BIDDER_REQUEST))) + .willReturn(BidderRequestHookImpl.of(delegate)); + } + + private void givenRawBidderResponseHook( + String moduleCode, + String hookImplCode, + BiFunction< + BidderResponsePayload, + BidderInvocationContext, + Future>> delegate) { + + given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) + .willReturn(RawBidderResponseHookImpl.of(delegate)); + } + + private void givenProcessedBidderResponseHook( + String moduleCode, + String hookImplCode, + BiFunction< + BidderResponsePayload, + BidderInvocationContext, + Future>> delegate) { + + given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) + .willReturn(ProcessedBidderResponseHookImpl.of(delegate)); + } + + private void givenAuctionResponseHook( + String moduleCode, + String hookImplCode, + BiFunction< + AuctionResponsePayload, + AuctionInvocationContext, + Future>> delegate) { + + given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.AUCTION_RESPONSE))) + .willReturn(AuctionResponseHookImpl.of(delegate)); + } + + private BiFunction>> delayedHook( + InvocationResult result, + int delay) { + + return (payload, context) -> { + final Promise> promise = Promise.promise(); + vertx.setTimer(delay, timerId -> promise.complete(result)); + return promise.future(); + }; + } + + private BiFunction>> immediateHook( + InvocationResult result) { + + return (payload, context) -> Future.succeededFuture(result); + } + + private HookStageExecutor createExecutor(String hostExecutionPlan) { + return createExecutor(hostExecutionPlan, null); + } + + private HookStageExecutor createExecutor(String hostExecutionPlan, String defaultAccountExecutionPlan) { + return HookStageExecutor.create( + hostExecutionPlan, + defaultAccountExecutionPlan, + hookCatalog, + timeoutFactory, + vertx, + clock, + jacksonMapper); + } + + @Value(staticConstructor = "of") + @NonFinal + private static class EntrypointHookImpl implements EntrypointHook { + + String code = "hook-code"; + + BiFunction>> delegate; + + @Override + public Future> call(EntrypointPayload payload, + InvocationContext invocationContext) { + + return delegate.apply(payload, invocationContext); + } + + @Override + public String code() { + return code; + } + } + + @Value(staticConstructor = "of") + @NonFinal + private static class RawAuctionRequestHookImpl implements RawAuctionRequestHook { + + String code = "hook-code"; + + BiFunction< + AuctionRequestPayload, + AuctionInvocationContext, + Future>> delegate; + + @Override + public Future> call(AuctionRequestPayload payload, + AuctionInvocationContext invocationContext) { + + return delegate.apply(payload, invocationContext); + } + + @Override + public String code() { + return code; + } + } + + @Value(staticConstructor = "of") + @NonFinal + private static class ProcessedAuctionRequestHookImpl implements ProcessedAuctionRequestHook { + + String code = "hook-code"; + + BiFunction< + AuctionRequestPayload, + AuctionInvocationContext, + Future>> delegate; + + @Override + public Future> call(AuctionRequestPayload payload, + AuctionInvocationContext invocationContext) { + + return delegate.apply(payload, invocationContext); + } + + @Override + public String code() { + return code; + } + } + + @Value(staticConstructor = "of") + @NonFinal + private static class BidderRequestHookImpl implements BidderRequestHook { + + String code = "hook-code"; + + BiFunction< + BidderRequestPayload, + BidderInvocationContext, + Future>> delegate; + + @Override + public Future> call(BidderRequestPayload payload, + BidderInvocationContext invocationContext) { + + return delegate.apply(payload, invocationContext); + } + + @Override + public String code() { + return code; + } + } + + @Value(staticConstructor = "of") + @NonFinal + private static class RawBidderResponseHookImpl implements RawBidderResponseHook { + + String code = "hook-code"; + + BiFunction< + BidderResponsePayload, + BidderInvocationContext, + Future>> delegate; + + @Override + public Future> call(BidderResponsePayload payload, + BidderInvocationContext invocationContext) { + + return delegate.apply(payload, invocationContext); + } + + @Override + public String code() { + return code; + } + } + + @Value(staticConstructor = "of") + @NonFinal + private static class ProcessedBidderResponseHookImpl implements ProcessedBidderResponseHook { + + String code = "hook-code"; + + BiFunction< + BidderResponsePayload, + BidderInvocationContext, + Future>> delegate; + + @Override + public Future> call(BidderResponsePayload payload, + BidderInvocationContext invocationContext) { + + return delegate.apply(payload, invocationContext); + } + + @Override + public String code() { + return code; + } + } + + @Value(staticConstructor = "of") + @NonFinal + private static class AuctionResponseHookImpl implements AuctionResponseHook { + + String code = "hook-code"; + + BiFunction< + AuctionResponsePayload, + AuctionInvocationContext, + Future>> delegate; + + @Override + public Future> call(AuctionResponsePayload payload, + AuctionInvocationContext invocationContext) { + + return delegate.apply(payload, invocationContext); + } + + @Override + public String code() { + return code; + } + } +} diff --git a/src/test/java/org/prebid/server/hooks/execution/model/StageWithHookTypeTest.java b/src/test/java/org/prebid/server/hooks/execution/model/StageWithHookTypeTest.java new file mode 100644 index 00000000000..c9f423abb5c --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/execution/model/StageWithHookTypeTest.java @@ -0,0 +1,57 @@ +package org.prebid.server.hooks.execution.model; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StageWithHookTypeTest { + + @Test + public void forStageShouldSupportAllStages() { + for (final Stage stage : Stage.values()) { + Assertions.assertThatCode(() -> StageWithHookType.forStage(stage)).doesNotThrowAnyException(); + } + } + + @Test + public void forStageShouldReturnEntrypoint() { + assertThat(StageWithHookType.forStage(Stage.entrypoint)).isSameAs(StageWithHookType.ENTRYPOINT); + } + + @Test + public void forStageShouldReturnRawAuctionRequest() { + assertThat(StageWithHookType.forStage(Stage.raw_auction_request)) + .isSameAs(StageWithHookType.RAW_AUCTION_REQUEST); + } + + @Test + public void forStageShouldReturnProcessedAuctionRequest() { + assertThat(StageWithHookType.forStage(Stage.processed_auction_request)) + .isSameAs(StageWithHookType.PROCESSED_AUCTION_REQUEST); + } + + @Test + public void forStageShouldReturnBidderRequest() { + assertThat(StageWithHookType.forStage(Stage.bidder_request)) + .isSameAs(StageWithHookType.BIDDER_REQUEST); + } + + @Test + public void forStageShouldReturnRawBidderResponse() { + assertThat(StageWithHookType.forStage(Stage.raw_bidder_response)) + .isSameAs(StageWithHookType.RAW_BIDDER_RESPONSE); + } + + @Test + public void forStageShouldReturnProcessedBidderResponse() { + assertThat(StageWithHookType.forStage(Stage.processed_bidder_response)) + .isSameAs(StageWithHookType.PROCESSED_BIDDER_RESPONSE); + } + + @Test + public void forStageShouldReturnAuctionResponse() { + assertThat(StageWithHookType.forStage(Stage.auction_response)) + .isSameAs(StageWithHookType.AUCTION_RESPONSE); + } +} diff --git a/src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java b/src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java new file mode 100644 index 00000000000..98e25d4e3a5 --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java @@ -0,0 +1,62 @@ +package org.prebid.server.hooks.v1; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.List; + +@Accessors(fluent = true) +@Builder +@Value +public class InvocationResultImpl implements InvocationResult { + + InvocationStatus status; + + String message; + + InvocationAction action; + + PayloadUpdate payloadUpdate; + + List errors; + + List warnings; + + List debugMessages; + + Object moduleContext; + + Tags analyticsTags; + + public static InvocationResult succeeded(PayloadUpdate payloadUpdate) { + return InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(payloadUpdate) + .build(); + } + + public static InvocationResult failed(String message) { + return InvocationResultImpl.builder() + .status(InvocationStatus.failure) + .message(message) + .build(); + } + + public static InvocationResult noAction() { + return InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build(); + } + + public static InvocationResult rejected(String message) { + return InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.reject) + .message(message) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java new file mode 100644 index 00000000000..0965bef2b40 --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java @@ -0,0 +1,17 @@ +package org.prebid.server.hooks.v1.analytics; + +import lombok.Value; +import lombok.experimental.Accessors; + +import java.util.List; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ActivityImpl implements Activity { + + String name; + + String status; + + List results; +} diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java new file mode 100644 index 00000000000..810313936a8 --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java @@ -0,0 +1,23 @@ +package org.prebid.server.hooks.v1.analytics; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; + +import java.util.List; + +@Accessors(fluent = true) +@Builder +@Value +public class AppliedToImpl implements AppliedTo { + + List impIds; + + List bidders; + + boolean request; + + boolean response; + + List bidIds; +} diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java new file mode 100644 index 00000000000..3558a22c3cd --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.v1.analytics; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; +import lombok.experimental.Accessors; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ResultImpl implements Result { + + String status; + + ObjectNode values; + + AppliedTo appliedTo; +} diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java new file mode 100644 index 00000000000..92278f2469c --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.v1.analytics; + +import lombok.Value; +import lombok.experimental.Accessors; + +import java.util.List; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class TagsImpl implements Tags { + + List activities; +} diff --git a/src/test/java/org/prebid/server/it/AcuityadsTest.java b/src/test/java/org/prebid/server/it/AcuityadsTest.java new file mode 100644 index 00000000000..60f7b9fd62c --- /dev/null +++ b/src/test/java/org/prebid/server/it/AcuityadsTest.java @@ -0,0 +1,34 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +@RunWith(SpringRunner.class) +public class AcuityadsTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAcuityAds() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/acuityads-exchange")) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/acuityads/test-acuityads-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/acuityads/test-auction-acuityads-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/acuityads/test-auction-acuityads-response.json", response, + singletonList("acuityads")); + } +} diff --git a/src/test/java/org/prebid/server/it/AdfTest.java b/src/test/java/org/prebid/server/it/AdfTest.java new file mode 100644 index 00000000000..abd22bf2766 --- /dev/null +++ b/src/test/java/org/prebid/server/it/AdfTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class AdfTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAdf() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adf-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/adf/test-adf-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/adf/test-adf-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/adf/test-auction-adf-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/adf/test-auction-adf-response.json", response, + singletonList("adf")); + } +} diff --git a/src/test/java/org/prebid/server/it/AdformTest.java b/src/test/java/org/prebid/server/it/AdformTest.java index beeb31dbbd6..29615cb0b81 100644 --- a/src/test/java/org/prebid/server/it/AdformTest.java +++ b/src/test/java/org/prebid/server/it/AdformTest.java @@ -4,23 +4,16 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.absent; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.equalToIgnoreCase; -import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) public class AdformTest extends IntegrationTest { @@ -28,110 +21,16 @@ public class AdformTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdform() throws IOException, JSONException { // given - // adform bid response for imp 12 WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/adform-exchange")) - .withQueryParam("CC", equalTo("1")) - .withQueryParam("rp", equalTo("4")) - .withQueryParam("fd", equalTo("1")) - .withQueryParam("stid", equalTo("tid")) - .withQueryParam("pt", equalTo("gross")) - .withQueryParam("ip", equalTo("193.168.244.1")) - .withQueryParam("adid", equalTo("ifaId")) - .withQueryParam("gdpr", equalTo("0")) - .withQueryParam("gdpr_consent", equalTo("consentValue")) - .withQueryParam("url", equalTo("https://adform.com?a=b")) - // bWlkPTE1JnJjdXI9VVNEJm1rdj1jb2xvcjpyZWQmbWt3PXJlZCZjZGltcz0zMDB4NjAwJm1pbnA9Mi41MA is Base64 encoded - // "mid=15&rcur=USD&mkv=color:red&mkw=red&cdims=300X600&minp=2.50" - .withQueryParam("bWlkPTE1JnJjdXI9VVNEJm1rdj1jb2xvcjpyZWQmbWt3PXJlZCZjZGltcz0zMDB4NjAwJm1pbnA9Mi41MA", - equalTo("")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Request-Agent", equalTo("PrebidAdapter 0.1.3")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Cookie", equalTo("uid=AF-UID;DigiTrust.v1.identity=eyJpZCI6ImlkIiwidmVyc2l" - + "vbiI6MSwia2V5diI6MTIzLCJwcml2YWN5Ijp7Im9wdG91dCI6ZmFsc2V9fQ")) .withRequestBody(absent()) .willReturn(aResponse().withBody(jsonFrom("openrtb2/adform/test-adform-bid-response-1.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adform/test-cache-adform-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/adform/test-cache-adform-response.json")))); - - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"adform":"AF-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImFkZm9ybSI6IkFGLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/adform/test-auction-adform-request.json")) - .post("/openrtb2/auction"); - - // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adform/test-auction-adform-response.json", - response, singletonList("adform")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); - } - - @Test - public void auctionShouldRespondWithBidsFromAdform() throws IOException { - // given - // adform bid response for ad unit 12 - WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/adform-exchange")) - .withQueryParam("CC", equalTo("1")) - .withQueryParam("rp", equalTo("4")) - .withQueryParam("fd", equalTo("1")) - .withQueryParam("stid", equalTo("tid")) - .withQueryParam("ip", equalTo("193.168.244.1")) - .withQueryParam("adid", equalTo("ifaId")) - .withQueryParam("gdpr", equalTo("1")) - .withQueryParam("gdpr_consent", equalTo("consent1")) - .withQueryParam("pt", equalTo("gross")) - // bWlkPTE1JnJjdXI9VVNE is Base64 encoded "mid=15&rcur=USD" - .withQueryParam("bWlkPTE1JnJjdXI9VVNE", equalTo("")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Request-Agent", equalTo("PrebidAdapter 0.1.3")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Cookie", equalTo("uid=AF-UID;DigiTrust.v1.identity" - //{"id":"id","version":1,"keyv":123,"privacy":{"optout":true}} - + "=eyJpZCI6ImlkIiwidmVyc2lvbiI6MSwia2V5diI6MTIzLCJwcml2YWN5Ijp7Im9wdG91dCI6dHJ1ZX19")) - .withRequestBody(absent()) - .willReturn(aResponse().withBody(jsonFrom("auction/adform/test-adform-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("auction/adform/test-cache-adform-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/adform/test-cache-adform-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - //this uids cookie value stands for {"uids":{"adform":"AF-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImFkZm9ybSI6IkFGLVVJRCJ9fQ==") - .queryParam("debug", "1") - .body(jsonFrom("auction/adform/test-auction-adform-request.json")) - .post("/auction"); + final Response response = responseFor("openrtb2/adform/test-auction-adform-request.json", + Endpoint.openrtb2_auction); // then - assertThat(response.header("Cache-Control")).isEqualTo("no-cache, no-store, must-revalidate"); - assertThat(response.header("Pragma")).isEqualTo("no-cache"); - assertThat(response.header("Expires")).isEqualTo("0"); - assertThat(response.header("Access-Control-Allow-Credentials")).isEqualTo("true"); - assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("http://www.example.com"); - - final String expectedAuctionResponse = legacyAuctionResponseFrom( - "auction/adform/test-auction-adform-response.json", - response, singletonList("adform")); - assertThat(response.asString()).isEqualTo(expectedAuctionResponse); + assertJsonEquals("openrtb2/adform/test-auction-adform-response.json", response, + singletonList("adform")); } } diff --git a/src/test/java/org/prebid/server/it/AdgenerationTest.java b/src/test/java/org/prebid/server/it/AdgenerationTest.java index 07d4e44bacb..1d345e319cf 100644 --- a/src/test/java/org/prebid/server/it/AdgenerationTest.java +++ b/src/test/java/org/prebid/server/it/AdgenerationTest.java @@ -4,19 +4,14 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.get; -import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,45 +20,16 @@ public class AdgenerationTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdgeneration() throws IOException, JSONException { // given - // Adgeneration bid response for imp 001 WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/adgeneration-exchange")) - .withQueryParam("posall", equalTo("SSPLOC")) - .withQueryParam("id", equalTo("58278")) - .withQueryParam("sdktype", equalTo("0")) - .withQueryParam("hb", equalTo("true")) - .withQueryParam("t", equalTo("json3")) - .withQueryParam("currency", equalTo("USD")) - .withQueryParam("sdkname", equalTo("prebidserver")) - .withQueryParam("sizes", equalTo("300x250")) - .withQueryParam("tp", equalTo("http://www.example.com")) - .withQueryParam("adapterver", equalTo("1.0.2")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withHeader("User-Agent", equalTo("some-agent")) .willReturn(aResponse() .withBody(jsonFrom("openrtb2/adgeneration/test-adgeneration-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adgeneration/test-cache-adgeneration-request.json"))) - .willReturn(aResponse() - .withBody(jsonFrom("openrtb2/adgeneration/test-cache-adgeneration-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7ImFkZ2VuZXJhdGlvbiI6IkFHLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/adgeneration/test-auction-adgeneration-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/adgeneration/test-auction-adgeneration-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adgeneration/test-auction-adgeneration-response.json", - response, singletonList("adgeneration")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/adgeneration/test-auction-adgeneration-response.json", response, + singletonList("adgeneration")); } } diff --git a/src/test/java/org/prebid/server/it/AdheseTest.java b/src/test/java/org/prebid/server/it/AdheseTest.java index 9b63e718f20..ae1c971f173 100644 --- a/src/test/java/org/prebid/server/it/AdheseTest.java +++ b/src/test/java/org/prebid/server/it/AdheseTest.java @@ -5,13 +5,11 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; -import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -19,35 +17,18 @@ public class AdheseTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdhese() throws IOException, JSONException { - - WIRE_MOCK_RULE.stubFor(WireMock.get(WireMock.urlPathEqualTo("/adhese-exchange/sl_adhese_prebid_demo_-" - + "leaderboard/ag55/cigent;brussels/tlall/xtconsentValue/xfhttp%3A%2F%2Fwww.example.com")) - .withHeader("Accept", WireMock.equalTo("application/json")) - .withHeader("Content-Type", WireMock.equalTo("application/json;charset=UTF-8")) - .withRequestBody(WireMock.absent()) + // given + WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/adhese-exchange")) + .withRequestBody(WireMock.equalToJson( + "{\"slots\":[{\"slotname\":\"_adhese_prebid_demo_-leaderboard\"}],\"parameters\":{\"ag\":[\"55\"],\"ci\":[\"gent\",\"brussels\"],\"tl\":[\"all\"],\"xf\":[\"http://www.example.com\"]}}")) .willReturn(WireMock.aResponse().withBody(jsonFrom("openrtb2/adhese/test-adhese-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/cache")) - .withRequestBody(WireMock.equalToJson(jsonFrom("openrtb2/adhese/test-cache-adhese-request.json"))) - .willReturn(WireMock.aResponse() - .withBody(jsonFrom("openrtb2/adhese/test-cache-adhese-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7ImFkaGVzZSI6IkFILVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/adhese/test-auction-adhese-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/adhese/test-auction-adhese-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adhese/test-auction-adhese-response.json", - response, singletonList("adhese")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/adhese/test-auction-adhese-response.json", response, + singletonList("adhese")); } } diff --git a/src/test/java/org/prebid/server/it/AdkernelAdnTest.java b/src/test/java/org/prebid/server/it/AdkernelAdnTest.java index 8f143fafb68..c10147617f1 100644 --- a/src/test/java/org/prebid/server/it/AdkernelAdnTest.java +++ b/src/test/java/org/prebid/server/it/AdkernelAdnTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,48 +21,17 @@ public class AdkernelAdnTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdkerneladn() throws IOException, JSONException { // given - // adkernelAdn bid response for imp 021 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adkernelAdn-exchange")) - .withQueryParam("account", equalTo("101")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adkerneladn/test-adkerneladn-bid-request-1.json"))) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adkernelAdn.tag.adkernel.com")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/adkerneladn/test-adkerneladn-bid-request.json"))) .willReturn(aResponse().withBody( - jsonFrom("openrtb2/adkerneladn/test-adkerneladn-bid-response-1.json")))); - - // adkernelAdn bid response for imp 022 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adkernelAdn-exchange")) - .withQueryParam("account", equalTo("102")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adkerneladn/test-adkerneladn-bid-request-2.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/adkerneladn/test-adkerneladn-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adkerneladn/test-cache-adkerneladn-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/adkerneladn/test-cache-adkerneladn-response.json")))); + jsonFrom("openrtb2/adkerneladn/test-adkerneladn-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"adkernelAdn":"AK-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImFka2VybmVsQWRuIjoiQUstVUlEIn19") - .body(jsonFrom("openrtb2/adkerneladn/test-auction-adkerneladn-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/adkerneladn/test-auction-adkerneladn-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adkerneladn/test-auction-adkerneladn-response.json", - response, singletonList("adkernelAdn")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/adkerneladn/test-auction-adkerneladn-response.json", response, + singletonList("adkernelAdn")); } } diff --git a/src/test/java/org/prebid/server/it/AdkernelTest.java b/src/test/java/org/prebid/server/it/AdkernelTest.java index 989b4799647..f28000a3da2 100644 --- a/src/test/java/org/prebid/server/it/AdkernelTest.java +++ b/src/test/java/org/prebid/server/it/AdkernelTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,38 +21,16 @@ public class AdkernelTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdkernel() throws IOException, JSONException { // given - // adkernel bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adkernel-exchange")) - .withQueryParam("zone", equalTo("101")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("x-openrtb-version", equalTo("2.5")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adkernel/test-adkernel-bid-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/adkernel/test-adkernel-bid-response.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adkernel/test-cache-adkernel-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/adkernel/test-cache-adkernel-response.json")))); + .willReturn(aResponse().withBody(jsonFrom("openrtb2/adkernel/test-adkernel-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"adkernel":"AK-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImFka2VybmVsIjoiQUstVUlEIn19") - .body(jsonFrom("openrtb2/adkernel/test-auction-adkernel-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/adkernel/test-auction-adkernel-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adkernel/test-auction-adkernel-response.json", - response, singletonList("adkernel")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/adkernel/test-auction-adkernel-response.json", response, + singletonList("adkernel")); } } diff --git a/src/test/java/org/prebid/server/it/AdmanTest.java b/src/test/java/org/prebid/server/it/AdmanTest.java index ccc17834054..d90428339cd 100644 --- a/src/test/java/org/prebid/server/it/AdmanTest.java +++ b/src/test/java/org/prebid/server/it/AdmanTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,37 +21,20 @@ public class AdmanTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdman() throws IOException, JSONException { // given - // Adman bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adman-exchange")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adman/test-adman-bid-request-1.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/adman/test-adman-bid-response-1.json")))); - // Adman bid response for imp 002 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adman-exchange")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adman/test-adman-bid-request-2.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/adman/test-adman-bid-response-2.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adman/test-cache-adman-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/adman/test-cache-adman-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"adman":"AD-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImFkbWFuIjoiQUQtVUlEIn19") - .body(jsonFrom("openrtb2/adman/test-auction-adman-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/adman/test-auction-adman-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adman/test-auction-adman-response.json", - response, singletonList("adman")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/adman/test-auction-adman-response.json", response, + singletonList("adman")); } } diff --git a/src/test/java/org/prebid/server/it/AdmixerTest.java b/src/test/java/org/prebid/server/it/AdmixerTest.java index 63c6a315424..ba1796ca607 100644 --- a/src/test/java/org/prebid/server/it/AdmixerTest.java +++ b/src/test/java/org/prebid/server/it/AdmixerTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,33 +21,16 @@ public class AdmixerTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdmixer() throws IOException, JSONException { // given - // AdmixerBidder bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/admixer-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) .withRequestBody(equalToJson(jsonFrom("openrtb2/admixer/test-admixer-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/admixer/test-admixer-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/admixer/test-cache-admixer-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/admixer/test-cache-admixer-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7ImFkbWl4ZXIiOiJBRC1VSUQifX0=") - .body(jsonFrom("openrtb2/admixer/test-auction-admixer-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/admixer/test-auction-admixer-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/admixer/test-auction-admixer-response.json", - response, singletonList("admixer")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/admixer/test-auction-admixer-response.json", response, + singletonList("admixer")); } } diff --git a/src/test/java/org/prebid/server/it/AdoceanTest.java b/src/test/java/org/prebid/server/it/AdoceanTest.java index 26a93b698e9..0715c7baa2e 100644 --- a/src/test/java/org/prebid/server/it/AdoceanTest.java +++ b/src/test/java/org/prebid/server/it/AdoceanTest.java @@ -5,15 +5,13 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -22,47 +20,15 @@ public class AdoceanTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdocean() throws IOException, JSONException { - WIRE_MOCK_RULE.stubFor(get(WireMock.urlPathEqualTo("/adocean-exchange/_10000000/ad.json")) - .withQueryParam("pbsrv_v", equalTo("1.0.0")) - .withQueryParam("id", equalTo("tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7")) - .withQueryParam("nc", equalTo("1")) - .withQueryParam("nosecure", equalTo("1")) - .withQueryParam("aid", equalTo("adoceanmyaozpniqismex:impId12")) - .withQueryParam("gdpr", equalTo("1")) - .withQueryParam("gdpr_consent", equalTo("consentValue")) - .withQueryParam("hcuserid", equalTo("AO-UID")) - .withHeader("Accept", WireMock.equalTo("application/json")) - .withHeader("Content-Type", WireMock.equalTo("application/json;charset=UTF-8")) - .withHeader("Host", equalTo("localhost:8090")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Referer", equalTo("http://www.example.com")) - .withHeader("User-Agent", equalTo("userAgent")) - .withRequestBody(WireMock.absent()) - .willReturn(WireMock.aResponse() - .withBody(jsonFrom("openrtb2/adocean/test-adocean-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/cache")) - .withRequestBody(WireMock.equalToJson(jsonFrom("openrtb2/adocean/test-cache-adocean-request.json"))) - .willReturn(WireMock.aResponse() - .withBody(jsonFrom("openrtb2/adocean/test-cache-adocean-response.json")))); + WIRE_MOCK_RULE.stubFor(get(WireMock.urlPathMatching("/adocean-exchange/_[0-9]*/ad.json")) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/adocean/test-adocean-bid-response-1.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7ImFkb2NlYW4iOiJBTy1VSUQifX0=") - .body(jsonFrom("openrtb2/adocean/test-auction-adocean-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/adocean/test-auction-adocean-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adocean/test-auction-adocean-response.json", - response, singletonList("adocean")); - - String actualStr = response.asString(); - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/adocean/test-auction-adocean-response.json", response, + singletonList("adocean")); } } diff --git a/src/test/java/org/prebid/server/it/AdopplerTest.java b/src/test/java/org/prebid/server/it/AdopplerTest.java index 8aa0a6601c6..c68973abbfe 100644 --- a/src/test/java/org/prebid/server/it/AdopplerTest.java +++ b/src/test/java/org/prebid/server/it/AdopplerTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,34 +21,16 @@ public class AdopplerTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdoppler() throws IOException, JSONException { // given - // Adoppler bid response for imp 001 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adoppler-exchange/processHeaderBid/unit1")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adoppler-exchange")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adoppler/test-adoppler-bid-request-1.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/adoppler/test-adoppler-bid-response-1.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adoppler/test-cache-adoppler-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/adoppler/test-cache-adoppler-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7ImFkb3BwbGVyIjoiQVAtVUlEIn19") - .body(jsonFrom("openrtb2/adoppler/test-auction-adoppler-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/adoppler/test-auction-adoppler-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adoppler/test-auction-adoppler-response.json", - response, singletonList("adoppler")); - - String actualStr = response.asString(); - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/adoppler/test-auction-adoppler-response.json", response, + singletonList("adoppler")); } } diff --git a/src/test/java/org/prebid/server/it/AdotTest.java b/src/test/java/org/prebid/server/it/AdotTest.java new file mode 100644 index 00000000000..3c99ec616b0 --- /dev/null +++ b/src/test/java/org/prebid/server/it/AdotTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class AdotTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAdot() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adot-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/adot/test-adot-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/adot/test-adot-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/adot/test-auction-adot-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/adot/test-auction-adot-response.json", response, + singletonList("adot")); + } +} diff --git a/src/test/java/org/prebid/server/it/AdponeTest.java b/src/test/java/org/prebid/server/it/AdponeTest.java index 32d351269ce..fe573885d24 100644 --- a/src/test/java/org/prebid/server/it/AdponeTest.java +++ b/src/test/java/org/prebid/server/it/AdponeTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,33 +21,16 @@ public class AdponeTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdpone() throws IOException, JSONException { // given - // Adpone bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adpone-exchange")) - .withHeader("x-openrtb-version", equalTo("2.5")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adpone/test-adpone-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/adpone/test-adpone-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adpone/test-cache-adpone-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/adpone/test-cache-adpone-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"adpone":"AP-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImFkcG9uZSI6IkFQLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/adpone/test-auction-adpone-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/adpone/test-auction-adpone-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adpone/test-auction-adpone-response.json", - response, singletonList("adpone")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/adpone/test-auction-adpone-response.json", response, + singletonList("adpone")); } } diff --git a/src/test/java/org/prebid/server/it/AdprimeTest.java b/src/test/java/org/prebid/server/it/AdprimeTest.java new file mode 100644 index 00000000000..0cf6782065e --- /dev/null +++ b/src/test/java/org/prebid/server/it/AdprimeTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class AdprimeTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAdprime() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adprime-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/adprime/test-adprime-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/adprime/test-adprime-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/adprime/test-auction-adprime-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/adprime/test-auction-adprime-response.json", response, + singletonList("adprime")); + } +} diff --git a/src/test/java/org/prebid/server/it/AdtargetTest.java b/src/test/java/org/prebid/server/it/AdtargetTest.java index 16d0dbab34f..3aa2ef02fce 100644 --- a/src/test/java/org/prebid/server/it/AdtargetTest.java +++ b/src/test/java/org/prebid/server/it/AdtargetTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,35 +21,16 @@ public class AdtargetTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdtarget() throws IOException, JSONException { // given - // Adtarget bid response for imp 14 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adtarget-exchange")) - .withQueryParam("aid", equalTo("1000")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adtarget/test-adtarget-bid-request-1.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/adtarget/test-adtarget-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adtarget/test-cache-adtarget-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/adtarget/test-cache-adtarget-response.json")))); + .willReturn(aResponse().withBody(jsonFrom("openrtb2/adtarget/test-adtarget-bid-response-1.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"adtarget":"AD-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImFkdGFyZ2V0IjoiQUQtVUlEIn19") - .body(jsonFrom("openrtb2/adtarget/test-auction-adtarget-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/adtarget/test-auction-adtarget-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adtarget/test-auction-adtarget-response.json", - response, singletonList("adtarget")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/adtarget/test-auction-adtarget-response.json", response, + singletonList("adtarget")); } } diff --git a/src/test/java/org/prebid/server/it/AdtelligentTest.java b/src/test/java/org/prebid/server/it/AdtelligentTest.java index c293727c904..7cf5cfeb486 100644 --- a/src/test/java/org/prebid/server/it/AdtelligentTest.java +++ b/src/test/java/org/prebid/server/it/AdtelligentTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,35 +21,17 @@ public class AdtelligentTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdtelligent() throws IOException, JSONException { // given - // adtelligent bid response for imp 14 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adtelligent-exchange")) - .withQueryParam("aid", equalTo("1000")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adtelligent/test-adtelligent-bid-request-1.json"))) .willReturn(aResponse().withBody( jsonFrom("openrtb2/adtelligent/test-adtelligent-bid-response-1.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adtelligent/test-cache-adtelligent-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/adtelligent/test-cache-adtelligent-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"adtelligent":"AT-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImFkdGVsbGlnZW50IjoiQVQtVUlEIn19") - .body(jsonFrom("openrtb2/adtelligent/test-auction-adtelligent-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/adtelligent/test-auction-adtelligent-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/adtelligent/test-auction-adtelligent-response.json", - response, singletonList("adtelligent")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/adtelligent/test-auction-adtelligent-response.json", response, + singletonList("adtelligent")); } } diff --git a/src/test/java/org/prebid/server/it/AdvangelistsTest.java b/src/test/java/org/prebid/server/it/AdvangelistsTest.java index 55452a6b7d5..279e6e2d9c6 100644 --- a/src/test/java/org/prebid/server/it/AdvangelistsTest.java +++ b/src/test/java/org/prebid/server/it/AdvangelistsTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,39 +21,18 @@ public class AdvangelistsTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdvangelists() throws IOException, JSONException { // given - // advangelists bid response for imp WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/advangelists-exchange")) - .withQueryParam("pubid", equalTo("19f1b372c7548ec1fe734d2c9f8dc688")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("x-openrtb-version", equalTo("2.5")) .withRequestBody(equalToJson(jsonFrom("openrtb2/advangelists/test-advangelists-bid-request.json"))) .willReturn(aResponse().withBody( jsonFrom("openrtb2/advangelists/test-advangelists-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/advangelists/test-cache-advangelists-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/advangelists/test-cache-advangelists-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"advangelists":"AV-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImFkdmFuZ2VsaXN0cyI6IkFWLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/advangelists/test-auction-advangelists-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/advangelists/test-auction-advangelists-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/advangelists/test-auction-advangelists-response.json", - response, singletonList("advangelists")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/advangelists/test-auction-advangelists-response.json", response, + singletonList("advangelists")); } } diff --git a/src/test/java/org/prebid/server/it/AdxcgTest.java b/src/test/java/org/prebid/server/it/AdxcgTest.java new file mode 100644 index 00000000000..2aeb1f7e7ca --- /dev/null +++ b/src/test/java/org/prebid/server/it/AdxcgTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class AdxcgTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAdxcg() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adxcg-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/adxcg/test-adxcg-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/adxcg/test-adxcg-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/adxcg/test-auction-adxcg-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/adxcg/test-auction-adxcg-response.json", response, + singletonList("adxcg")); + } +} diff --git a/src/test/java/org/prebid/server/it/AdyoulikeTest.java b/src/test/java/org/prebid/server/it/AdyoulikeTest.java new file mode 100644 index 00000000000..dce19435f7e --- /dev/null +++ b/src/test/java/org/prebid/server/it/AdyoulikeTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class AdyoulikeTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAdyoulike() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adyoulike-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/adyoulike/test-adyoulike-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/adyoulike/test-adyoulike-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/adyoulike/test-auction-adyoulike-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/adyoulike/test-auction-adyoulike-response.json", response, + singletonList("adyoulike")); + } +} diff --git a/src/test/java/org/prebid/server/it/AjaTest.java b/src/test/java/org/prebid/server/it/AjaTest.java index 360d35d4aa3..0c84df67d6d 100644 --- a/src/test/java/org/prebid/server/it/AjaTest.java +++ b/src/test/java/org/prebid/server/it/AjaTest.java @@ -4,17 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 com.github.tomakehurst.wiremock.client.WireMock.equalToJson; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,37 +21,15 @@ public class AjaTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromTheAjaBidder() throws IOException, JSONException { // given - // TheMediaAja bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/aja")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/aja/test-aja-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/aja/test-aja-bid-response-1.json")))); - - // TheMediaAja bid response for imp 002 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/aja")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/aja/test-aja-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/aja/test-aja-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/aja/test-cache-aja-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/aja/test-cache-aja-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/aja/test-aja-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/aja/test-aja-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"aja":"AJA-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImFqYSI6IkFKQS1VSUQifX0=") - .body(jsonFrom("openrtb2/aja/test-auction-aja-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/aja/test-auction-aja-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/aja/test-auction-aja-response.json", - response, singletonList("aja")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/aja/test-auction-aja-response.json", response, singletonList("aja")); } } diff --git a/src/test/java/org/prebid/server/it/AlgorixTest.java b/src/test/java/org/prebid/server/it/AlgorixTest.java new file mode 100644 index 00000000000..165ca356a60 --- /dev/null +++ b/src/test/java/org/prebid/server/it/AlgorixTest.java @@ -0,0 +1,38 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +/** + * Algorix Test in org.prebid.server.it + */ +@RunWith(SpringRunner.class) +public class AlgorixTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAlgorix() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/algorix-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/algorix/test-algorix-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/algorix/test-algorix-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/algorix/test-auction-algorix-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/algorix/test-auction-algorix-response.json", response, singletonList("algorix")); + } +} diff --git a/src/test/java/org/prebid/server/it/AmxTest.java b/src/test/java/org/prebid/server/it/AmxTest.java new file mode 100644 index 00000000000..ef1879918f8 --- /dev/null +++ b/src/test/java/org/prebid/server/it/AmxTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class AmxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAmx() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/amx-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/amx/test-amx-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/amx/test-amx-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/amx/test-auction-amx-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/amx/test-auction-amx-response.json", response, singletonList("amx")); + } +} diff --git a/src/test/java/org/prebid/server/it/ApplicationTest.java b/src/test/java/org/prebid/server/it/ApplicationTest.java index 238ecb0416e..dd1aace1141 100644 --- a/src/test/java/org/prebid/server/it/ApplicationTest.java +++ b/src/test/java/org/prebid/server/it/ApplicationTest.java @@ -1,11 +1,11 @@ package org.prebid.server.it; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.iab.gdpr.consent.VendorConsentEncoder; -import com.iab.gdpr.consent.implementation.v1.VendorConsentBuilder; -import com.iab.gdpr.consent.range.StartEndRangeEntry; +import com.iabtcf.encoder.TCStringEncoder; +import com.iabtcf.utils.BitSetIntIterable; import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; import io.restassured.config.ObjectMapperConfig; @@ -35,11 +35,12 @@ import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; +import java.util.Collection; import java.util.Collections; -import java.util.HashSet; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -53,6 +54,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static io.restassured.RestAssured.given; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; @@ -63,7 +65,6 @@ public class ApplicationTest extends IntegrationTest { private static final String ADFORM = "adform"; private static final String APPNEXUS = "appnexus"; private static final String APPNEXUS_ALIAS = "appnexusAlias"; - private static final String APPNEXUS_CONFIGURED_ALIAS = "districtm"; private static final String RUBICON = "rubicon"; private static final int ADMIN_PORT = 8060; @@ -78,39 +79,39 @@ public class ApplicationTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromRubiconAndAppnexus() throws IOException, JSONException { // given - // rubicon bid response for imp 1 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) .withQueryParam("tk_xint", equalTo("dmbjs")) .withBasicAuth("rubicon_user", "rubicon_password") .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) .withHeader("Accept", equalTo("application/json")) .withHeader("User-Agent", equalTo("prebid-server/1.0")) + .withHeader("Sec-GPC", equalTo("1")) .withRequestBody(equalToJson(jsonFrom("openrtb2/rubicon_appnexus/test-rubicon-bid-request-1.json"))) .willReturn(aResponse().withBody(jsonFrom( "openrtb2/rubicon_appnexus/test-rubicon-bid-response-1.json")))); - // rubicon bid response for imp 2 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .withHeader("Sec-GPC", equalTo("1")) .withRequestBody(equalToJson(jsonFrom("openrtb2/rubicon_appnexus/test-rubicon-bid-request-2.json"))) .willReturn(aResponse().withBody(jsonFrom( "openrtb2/rubicon_appnexus/test-rubicon-bid-response-2.json")))); - // appnexus bid response for imp 3 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/appnexus-exchange")) + .withHeader("Sec-GPC", equalTo("1")) .withRequestBody(equalToJson(jsonFrom("openrtb2/rubicon_appnexus/test-appnexus-bid-request-1.json"))) .willReturn(aResponse().withBody(jsonFrom( "openrtb2/rubicon_appnexus/test-appnexus-bid-response-1.json")))); - // appnexus bid response for imp 3 with alias parameters WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/appnexus-exchange")) + .withHeader("Sec-GPC", equalTo("1")) .withRequestBody(equalToJson(jsonFrom("openrtb2/rubicon_appnexus/test-appnexus-bid-request-2.json"))) .willReturn(aResponse().withBody(jsonFrom( "openrtb2/rubicon_appnexus/test-appnexus-bid-response-2.json")))); // pre-bid cache WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom( - "openrtb2/rubicon_appnexus/test-cache-rubicon-appnexus-request.json"), true, false)) + .withRequestBody(equalToBidCacheRequest( + jsonFrom("openrtb2/rubicon_appnexus/test-cache-rubicon-appnexus-request.json"))) .willReturn(aResponse() .withTransformers("cache-response-transformer") .withTransformerParameter("matcherName", @@ -122,31 +123,36 @@ public void openrtb2AuctionShouldRespondWithBidsFromRubiconAndAppnexus() throws .header("Referer", "http://www.example.com") .header("User-Agent", "userAgent") .header("Origin", "http://www.example.com") + .header("Sec-GPC", 1) // this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT","adnxs":"12345"}} .cookie("uids", "eyJ1aWRzIjp7InJ1Ymljb24iOiJKNVZMQ1dRUC0yNi1DV0ZUIiwiYWRueHMiOiIxMjM0NSJ9fQ==") .body(jsonFrom("openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json")) .post("/openrtb2/auction"); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json", - response, asList(RUBICON, APPNEXUS, APPNEXUS_ALIAS)); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbCacheDebugComparator()); + assertJsonEquals("openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json", + response, asList(RUBICON, APPNEXUS, APPNEXUS_ALIAS), + openrtbCacheDebugCustomization(), headersDebugCustomization()); } @Test - public void auctionShouldRespondWithBidsFromAppnexusAlias() throws IOException { + public void testOpenrtb2AuctionCoreFunctionality() throws IOException, JSONException { // given - // appnexus bid response for ad unit 4 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/appnexus-exchange")) - .withRequestBody(equalToJson(jsonFrom("auction/districtm/test-districtm-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/districtm/test-districtm-bid-response-1.json")))); + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) + .withQueryParam("tk_xint", equalTo("rp-pbs")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/rubicon_core_functionality/test-rubicon-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/rubicon_core_functionality/test-rubicon-bid-response.json")))); // pre-bid cache WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("auction/districtm/test-cache-districtm-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/districtm/test-cache-districtm-response.json")))); + .withRequestBody(equalToJson( + jsonFrom("openrtb2/rubicon_core_functionality/test-cache-rubicon-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/rubicon_core_functionality/test-cache-rubicon-response.json")))); // when final Response response = given(SPEC) @@ -154,87 +160,84 @@ public void auctionShouldRespondWithBidsFromAppnexusAlias() throws IOException { .header("X-Forwarded-For", "193.168.244.1") .header("User-Agent", "userAgent") .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"adnxs":"12345"}} - .cookie("uids", "eyJ1aWRzIjp7ImFkbnhzIjoiMTIzNDUifX0=") - .queryParam("debug", "1") - .body(jsonFrom("auction/districtm/test-auction-districtm-request.json")) - .post("/auction"); + // this uids cookie value stands for {"uids":{"rubicon":"RUB-UID"}} + .cookie("uids", "eyJ1aWRzIjp7InJ1Ymljb24iOiJSVUItVUlEIn19") + .body(jsonFrom("openrtb2/rubicon_core_functionality/test-auction-rubicon-request.json")) + .post("/openrtb2/auction"); // then - assertThat(response.header("Cache-Control")).isEqualTo("no-cache, no-store, must-revalidate"); - assertThat(response.header("Pragma")).isEqualTo("no-cache"); - assertThat(response.header("Expires")).isEqualTo("0"); - assertThat(response.header("Access-Control-Allow-Credentials")).isEqualTo("true"); - assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("http://www.example.com"); + final String expectedAuctionResponse = openrtbAuctionResponseFrom( + "openrtb2/rubicon_core_functionality/test-auction-rubicon-response.json", + response, singletonList("rubicon")); - final String expectedAuctionResponse = legacyAuctionResponseFrom( - "auction/districtm/test-auction-districtm-response.json", - response, asList(APPNEXUS, APPNEXUS_CONFIGURED_ALIAS)); - assertThat(response.asString()).isEqualTo(expectedAuctionResponse); + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); } @Test - public void auctionShouldRespondWithBidsFromRubiconAndAppnexus() throws IOException { + public void openrtb2MultiBidAuctionShouldRespondWithBidsFromRubiconAndAppnexus() throws IOException, JSONException { // given - // rubicon bid response for ad unit 1 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) .withQueryParam("tk_xint", equalTo("rp-pbs")) .withBasicAuth("rubicon_user", "rubicon_password") .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) .withHeader("Accept", equalTo("application/json")) .withHeader("User-Agent", equalTo("prebid-server/1.0")) - .withRequestBody(equalToJson(jsonFrom("auction/rubicon_appnexus/test-rubicon-bid-request-1.json"))) - .willReturn(aResponse().withBody( - jsonFrom("auction/rubicon_appnexus/test-rubicon-bid-response-1.json")))); - - // rubicon bid response for ad unit 2 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) - .withRequestBody(equalToJson(jsonFrom("auction/rubicon_appnexus/test-rubicon-bid-request-2.json"))) - .willReturn(aResponse().withBody( - jsonFrom("auction/rubicon_appnexus/test-rubicon-bid-response-2.json")))); - - // rubicon bid response for ad unit 3 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) - .withRequestBody(equalToJson(jsonFrom("auction/rubicon_appnexus/test-rubicon-bid-request-3.json"))) - .willReturn(aResponse().withBody( - jsonFrom("auction/rubicon_appnexus/test-rubicon-bid-response-3.json")))); + .withRequestBody(equalToJson( + jsonFrom("openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-request-1.json"))) + .willReturn(aResponse().withBody(jsonFrom( + "openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-response-1.json")))); - // appnexus bid response for ad unit 4 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/appnexus-exchange")) - .withRequestBody(equalToJson(jsonFrom("auction/rubicon_appnexus/test-appnexus-bid-request-1.json"))) - .willReturn(aResponse().withBody( - jsonFrom("auction/rubicon_appnexus/test-appnexus-bid-response-1.json")))); + .withRequestBody(equalToJson( + jsonFrom("openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-request-1.json"))) + .willReturn(aResponse().withBody(jsonFrom( + "openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-response-1.json")))); // pre-bid cache WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom( - "auction/rubicon_appnexus/test-cache-rubicon-appnexus-request.json"))) - .willReturn(aResponse().withBody(jsonFrom( - "auction/rubicon_appnexus/test-cache-rubicon-appnexus-response.json")))); + .withRequestBody(equalToBidCacheRequest( + jsonFrom("openrtb2/rubicon_appnexus_multi_bid/test-cache-rubicon-appnexus-request.json"))) + .willReturn(aResponse() + .withTransformers("cache-response-transformer") + .withTransformerParameter("matcherName", + "openrtb2/rubicon_appnexus_multi_bid/test-cache-matcher-rubicon-appnexus.json"))); // when final Response response = given(SPEC) .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") .header("User-Agent", "userAgent") .header("Origin", "http://www.example.com") - //this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT","adnxs":"12345"}} + // this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT","adnxs":"12345"}} .cookie("uids", "eyJ1aWRzIjp7InJ1Ymljb24iOiJKNVZMQ1dRUC0yNi1DV0ZUIiwiYWRueHMiOiIxMjM0NSJ9fQ==") - .queryParam("debug", "1") - .body(jsonFrom("auction/rubicon_appnexus/test-auction-rubicon-appnexus-request.json")) - .post("/auction"); + .body(jsonFrom("openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-request.json")) + .post("/openrtb2/auction"); // then - assertThat(response.header("Cache-Control")).isEqualTo("no-cache, no-store, must-revalidate"); - assertThat(response.header("Pragma")).isEqualTo("no-cache"); - assertThat(response.header("Expires")).isEqualTo("0"); - assertThat(response.header("Access-Control-Allow-Credentials")).isEqualTo("true"); - assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("http://www.example.com"); - - final String expectedAuctionResponse = legacyAuctionResponseFrom( - "auction/rubicon_appnexus/test-auction-rubicon-appnexus-response.json", + assertJsonEquals("openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-response.json", response, asList(RUBICON, APPNEXUS, APPNEXUS_ALIAS)); - assertThat(response.asString()).isEqualTo(expectedAuctionResponse); + } + + @Test + public void openrtb2AuctionShouldRespondWithStoredBidResponse() throws IOException, JSONException { + // given + // pre-bid cache + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/storedresponse/test-cache-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/storedresponse/test-cache-response.json")))); + + // when + final Response response = given(SPEC) + .header("Referer", "http://www.example.com") + .header("User-Agent", "userAgent") + .header("Origin", "http://www.example.com") + // this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT","adnxs":"12345"}} + .cookie("uids", "eyJ1aWRzIjp7InJ1Ymljb24iOiJKNVZMQ1dRUC0yNi1DV0ZUIiwiYWRueHMiOiIxMjM0NSJ9fQ==") + .body(jsonFrom("openrtb2/storedresponse/test-auction-request.json")) + .post("/openrtb2/auction"); + + // then + assertJsonEquals("openrtb2/storedresponse/test-auction-response.json", + response, singletonList(RUBICON), openrtbCacheDebugCustomization()); } @Test @@ -252,7 +255,7 @@ public void ampShouldReturnTargeting() throws IOException, JSONException { // pre-bid cache WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("amp/test-cache-request.json"), true, false)) + .withRequestBody(equalToBidCacheRequest(jsonFrom("amp/test-cache-request.json"))) .willReturn(aResponse() .withTransformers("cache-response-transformer") .withTransformerParameter("matcherName", "amp/test-cache-matcher-amp.json") @@ -273,13 +276,16 @@ public void ampShouldReturnTargeting() throws IOException, JSONException { + "&oh=120" + "&timeout=10000000" + "&slot=overwrite-tagId" + + "&targeting=%7B%22gam-key1%22%3A%22val1%22%2C%22gam-key2%22%3A%22val2%22%7D" + "&curl=https%3A%2F%2Fgoogle.com" + "&account=accountId" + + "&attl_consent=someConsent" + + "&gdpr_applies=false" + + "&consent_type=3" + "&consent_string=1YNN"); // then - JSONAssert.assertEquals(jsonFrom("amp/test-amp-response.json"), response.asString(), - JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("amp/test-amp-response.json", response, asList(RUBICON, APPNEXUS)); } @Test @@ -331,16 +337,14 @@ public void staticShouldReturnHttp200Ok() { @Test public void cookieSyncShouldReturnBidderStatusWithExpectedUsersyncInfo() { // given - final String gdprConsent = VendorConsentEncoder.toBase64String(new VendorConsentBuilder() - .withConsentRecordCreatedOn(Instant.now()) - .withConsentRecordLastUpdatedOn(Instant.now()) - .withConsentLanguage("en") - .withVendorListVersion(79) - .withRangeEntries(singletonList(new StartEndRangeEntry(1, 100))) - .withMaxVendorId(100) - .withBitField(new HashSet<>(asList(1, 32, 52))) - .withAllowedPurposeIds(new HashSet<>(asList(1, 3))) - .build()); + final String gdprConsent = TCStringEncoder.newBuilder() + .version(2) + .consentLanguage("EN") + .vendorListVersion(52) + .tcfPolicyVersion(2) + .addPurposesConsent(BitSetIntIterable.from(1)) + .addVendorConsent(BitSetIntIterable.from(1, 32, 52)) + .encode(); // when final CookieSyncResponse cookieSyncResponse = given(SPEC) @@ -370,6 +374,7 @@ public void cookieSyncShouldReturnBidderStatusWithExpectedUsersyncInfo() { "http://localhost:8080/setuid?bidder=rubicon" + "&gdpr=1&gdpr_consent=" + gdprConsent + "&us_privacy=1YNN" + + "&f=i" + "&uid=host-cookie-uid", "redirect", false)) .build(), @@ -380,6 +385,7 @@ public void cookieSyncShouldReturnBidderStatusWithExpectedUsersyncInfo() { "//usersync-url/getuid?http%3A%2F%2Flocalhost%3A8080%2Fsetuid%3Fbidder" + "%3Dadnxs%26gdpr%3D1%26gdpr_consent%3D" + gdprConsent + "%26us_privacy%3D1YNN" + + "%26f%3Di" + "%26uid%3D%24UID", "redirect", false)) .build(), @@ -401,7 +407,7 @@ public void setuidShouldUpdateRubiconUidInUidCookie() throws IOException { .queryParam("bidder", RUBICON) .queryParam("uid", "updatedUid") .queryParam("gdpr", "1") - .queryParam("gdpr_consent", "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA") + .queryParam("gdpr_consent", "CPBCKiyPBCKiyAAAAAENA0CAAIAAAAAAACiQAaQAwAAgAgABoAAAAAA") .when() .get("/setuid") .then() @@ -426,7 +432,7 @@ public void setuidShouldUpdateRubiconUidInUidCookie() throws IOException { } @Test - public void getuidsShouldReturnJsonWithUids() throws JSONException { + public void getuidsShouldReturnJsonWithUids() throws JSONException, IOException { // given and when final Response response = given(SPEC) // this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT","adnxs":"12345"}, @@ -437,16 +443,15 @@ public void getuidsShouldReturnJsonWithUids() throws JSONException { .get("/getuids"); // then - JSONAssert.assertEquals("{\"buyeruids\":{\"rubicon\":\"J5VLCWQP-26-CWFT\",\"adnxs\":\"12345\"}}", - response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("uid/test-uid-response.json", response, emptyList()); } @Test public void vtrackShouldReturnJsonWithUids() throws JSONException, IOException { // given and when WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("vtrack/test-cache-request.json"), true, false)) - .willReturn(aResponse().withBody(jsonFrom("vtrack/test-vtrack-response.json")))); + .withRequestBody(equalToBidCacheRequest(jsonFrom("vtrack/test-cache-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("vtrack/test-cache-response.json")))); final Response response = given(SPEC) .when() @@ -455,8 +460,7 @@ public void vtrackShouldReturnJsonWithUids() throws JSONException, IOException { .post("/vtrack"); // then - JSONAssert.assertEquals("{\"responses\":[{\"uuid\":\"94531ab8-c662-4fc7-904e-6b5d3be43b1a\"}]}", - response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("vtrack/test-vtrack-response.json", response, emptyList()); } @Test @@ -477,34 +481,48 @@ public void optionsRequestShouldRespondWithOriginalPolicyHeaders() { } @Test - public void biddersParamsShouldReturnBidderSchemas() throws JSONException { - // given - final Map bidderNameToSchema = getBidderNamesFromParamFiles().stream() - .collect(Collectors.toMap(Function.identity(), ApplicationTest::jsonSchemaToJsonNode)); - + public void biddersParamsShouldReturnBidderSchemas() throws JSONException, IOException { // when final Response response = given(SPEC) .when() .get("/bidders/params"); // then - JSONAssert.assertEquals(bidderNameToSchema.toString(), response.asString(), JSONCompareMode.NON_EXTENSIBLE); + final Map responseAsMap = jacksonMapper.decodeValue(response.asString(), + new TypeReference>() { + }); + + final List bidders = getBidderNamesFromParamFiles(); + final Map aliases = getBidderAliasesFromConfigFiles(); + final Map expectedMap = CollectionUtils.union(bidders, aliases.keySet()).stream() + .collect(Collectors.toMap( + Function.identity(), + bidderName -> jsonSchemaToJsonNode(aliases.getOrDefault(bidderName, bidderName)))); + + assertThat(responseAsMap.keySet()).containsOnlyElementsOf(expectedMap.keySet()); + assertThat(responseAsMap).containsAllEntriesOf(expectedMap); + + JSONAssert.assertEquals(expectedMap.toString(), response.asString(), JSONCompareMode.NON_EXTENSIBLE); } @Test - public void infoBiddersShouldReturnRegisteredActiveBidderNames() throws JSONException, IOException { - // given - final List bidderNames = getBidderNamesFromParamFiles(); - final List bidderAliases = getBidderAliasesFromConfigFiles(); - + public void infoBiddersShouldReturnRegisteredActiveBidderNames() throws IOException { // when final Response response = given(SPEC) .when() + .queryParam("enabledonly", "false") .get("/info/bidders"); // then - final String expectedResponse = CollectionUtils.union(bidderNames, bidderAliases).toString(); - JSONAssert.assertEquals(expectedResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + final List responseAsList = jacksonMapper.decodeValue(response.asString(), + new TypeReference>() { + }); + + final List bidders = getBidderNamesFromParamFiles(); + final Map aliases = getBidderAliasesFromConfigFiles(); + final Collection expectedBidders = CollectionUtils.union(bidders, aliases.keySet()); + + assertThat(responseAsList).containsOnlyElementsOf(expectedBidders); } @Test @@ -569,10 +587,7 @@ public void shouldAskExchangeWithUpdatedSettingsFromCache() throws IOException, .post("/openrtb2/auction"); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "cache/update/test-auction-response.json", response, singletonList(RUBICON)); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("cache/update/test-auction-response.json", response, singletonList(RUBICON)); } @Test @@ -649,6 +664,21 @@ public void invalidateAmpSettingsCacheShouldReturnExpectedResponse() { .statusCode(200); } + @Test + public void traceHandlerShouldReturn200Ok() { + given(ADMIN_SPEC) + .when() + .param("level", "error") + .param("duration", "1000") + .param("account", "1001") + .param("bidderCode", "rubicon") + .param("lineitemId", "1001") + .post("/pbs-admin/tracelog") + .then() + .assertThat() + .statusCode(200); + } + private Uids decodeUids(String value) throws IOException { return mapper.readValue(Base64.getUrlDecoder().decode(value), Uids.class); } @@ -664,27 +694,30 @@ private static List getBidderNamesFromParamFiles() { return Collections.emptyList(); } - private static List getBidderAliasesFromConfigFiles() throws IOException { + private static Map getBidderAliasesFromConfigFiles() throws IOException { final String folderPath = "src/main/resources/bidder-config"; final File folder = new File(folderPath); final String[] files = folder.list(); if (files != null) { final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - final List aliases = new ArrayList<>(); + final Map aliases = new HashMap<>(); for (String fileName : files) { final JsonNode configNode = mapper.readValue(new File(folderPath, fileName), JsonNode.class); - final JsonNode aliasesNode = configNode.get("adapters").fields().next().getValue().get("aliases"); - - if (!aliasesNode.isNull()) { - for (String alias : aliasesNode.textValue().split(",")) { - aliases.add(alias.trim()); + final Map.Entry bidderEntry = configNode.get("adapters").fields().next(); + final String bidderName = bidderEntry.getKey(); + final JsonNode aliasesNode = bidderEntry.getValue().get("aliases"); + + if (aliasesNode.isObject()) { + Iterator iterator = aliasesNode.fieldNames(); + while (iterator.hasNext()) { + aliases.put(iterator.next().trim(), bidderName); } } } return aliases; } - return Collections.emptyList(); + return Collections.emptyMap(); } private static JsonNode jsonSchemaToJsonNode(String bidderName) { diff --git a/src/test/java/org/prebid/server/it/ApplogyTest.java b/src/test/java/org/prebid/server/it/ApplogyTest.java index 4a6351b4389..ed07e1b5941 100644 --- a/src/test/java/org/prebid/server/it/ApplogyTest.java +++ b/src/test/java/org/prebid/server/it/ApplogyTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,40 +21,15 @@ public class ApplogyTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromApplogy() throws IOException, JSONException { // given - // Applogy bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/applogy-exchange/1234")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/applogy/test-applogy-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/applogy/test-applogy-bid-response-1.json")))); - - // Applogy bid response for imp 002 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/applogy-exchange/12345")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/applogy/test-applogy-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/applogy/test-applogy-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/applogy/test-cache-applogy-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/applogy/test-cache-applogy-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/applogy/test-applogy-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/applogy/test-applogy-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7ImdhbW9zaGkiOiJHTS1VSUQifX0=") - .body(jsonFrom("openrtb2/applogy/test-auction-applogy-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/applogy/test-auction-applogy-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/applogy/test-auction-applogy-response.json", - response, singletonList("applogy")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/applogy/test-auction-applogy-response.json", response, singletonList("applogy")); } } diff --git a/src/test/java/org/prebid/server/it/AppnexusVideoTest.java b/src/test/java/org/prebid/server/it/AppnexusVideoTest.java index 9a23e70aee1..05ae56253cb 100644 --- a/src/test/java/org/prebid/server/it/AppnexusVideoTest.java +++ b/src/test/java/org/prebid/server/it/AppnexusVideoTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -16,7 +15,7 @@ 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 io.restassured.RestAssured.given; +import static java.util.Collections.emptyList; @RunWith(SpringRunner.class) public class AppnexusVideoTest extends IntegrationTest { @@ -27,37 +26,31 @@ public void openrtb2VideoShouldRespondWithBidsFromAppnexus() throws IOException, WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/appnexus-exchange")) .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/video/test-video-appnexus-bid-request-1.json"))) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/video/test-video-appnexus-bid-request-1.json"), true, true)) .willReturn(aResponse().withBody(jsonFrom("openrtb2/video/test-video-appnexus-bid-response-1.json")))); WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/appnexus-exchange")) .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/video/test-video-appnexus-bid-request-2.json"))) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/video/test-video-appnexus-bid-request-2.json"), true, true)) .willReturn(aResponse().withBody(jsonFrom("openrtb2/video/test-video-appnexus-bid-response-2.json")))); // pre-bid cache WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson( - jsonFrom("openrtb2/video/test-video-cache-request.json"), true, false)) + .withRequestBody(equalToBidCacheRequest( + jsonFrom("openrtb2/video/test-video-cache-request.json"))) .willReturn(aResponse() .withTransformers("cache-response-transformer") .withTransformerParameter("matcherName", "openrtb2/video/test-video-cache-response-matcher.json"))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .body(jsonFrom("openrtb2/video/test-video-appnexus-request.json")) - .post("/openrtb2/video"); + final Response response = responseFor("openrtb2/video/test-video-appnexus-request.json", + Endpoint.openrtb2_video); // then - // TODO remove "empty" when VideoRequest will proceed consentValue. - final String expectedAuctionResponse = jsonFrom("openrtb2/video/test-video-appnexus-response-empty.json"); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/video/test-video-appnexus-response.json", response, emptyList()); } } diff --git a/src/test/java/org/prebid/server/it/AvocetTest.java b/src/test/java/org/prebid/server/it/AvocetTest.java index 0793314205f..8f35deef437 100644 --- a/src/test/java/org/prebid/server/it/AvocetTest.java +++ b/src/test/java/org/prebid/server/it/AvocetTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,34 +21,15 @@ public class AvocetTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAvocet() throws IOException, JSONException { // given - // Avocet bid response for imp 001 and 002 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/avocet-exchange")) .withRequestBody(equalToJson(jsonFrom("openrtb2/avocet/test-avocet-bid-request-1.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/avocet/test-avocet-bid-response-1.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/avocet/test-cache-avocet-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/avocet/test-cache-avocet-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"avocet":"AV-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImF2b2NldCI6IkFWLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/avocet/test-auction-avocet-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/avocet/test-auction-avocet-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/avocet/test-auction-avocet-response.json", - response, singletonList("avocet")); - - final String actualStr = response.asString(); - - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/avocet/test-auction-avocet-response.json", response, singletonList("avocet")); } } diff --git a/src/test/java/org/prebid/server/it/AxonixTest.java b/src/test/java/org/prebid/server/it/AxonixTest.java new file mode 100644 index 00000000000..21d1c12597d --- /dev/null +++ b/src/test/java/org/prebid/server/it/AxonixTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +@RunWith(SpringRunner.class) +public class AxonixTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAxonix() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/axonix-exchange")) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/axonix/test-axonix-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/axonix/test-auction-axonix-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/axonix/test-auction-axonix-response.json", response, singletonList("axonix")); + } +} diff --git a/src/test/java/org/prebid/server/it/BeachfrontTest.java b/src/test/java/org/prebid/server/it/BeachfrontTest.java index 05e5b74c375..4f5b7c265b2 100644 --- a/src/test/java/org/prebid/server/it/BeachfrontTest.java +++ b/src/test/java/org/prebid/server/it/BeachfrontTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,65 +21,16 @@ public class BeachfrontTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromBeachfront() throws IOException, JSONException { // given - // beachfront bid response for imp 02 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/beachfront-exchange/video")) - .withQueryParam("exchange_id", equalTo("beachfrontAppId")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("Host", equalTo("localhost:8090")) - .withHeader("Accept-Language", equalTo("en")) - .withHeader("DNT", equalTo("2")) - .withHeader("Cookie", equalTo("__io_cid=BF-UID")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/beachfront/test-beachfront-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/beachfront/test-beachfront-bid-response-2.json")))); - - // beachfront bid response for imp 01 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/beachfront-exchange/video")) - .withQueryParam("exchange_id", equalTo("beachfrontAppId1")) - .withQueryParam("prebidserver", equalTo("")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("Host", equalTo("localhost:8090")) - .withHeader("Accept-Language", equalTo("en")) - .withHeader("DNT", equalTo("2")) - .withHeader("Cookie", equalTo("__io_cid=BF-UID")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/beachfront/test-beachfront-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/beachfront/test-beachfront-bid-response-1.json")))); - - // beachfront bid response for imp 03 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/beachfront-exchange/banner")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("Host", equalTo("localhost:8090")) - .withHeader("Accept-Language", equalTo("en")) - .withHeader("DNT", equalTo("2")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/beachfront/test-beachfront-bid-request-3.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/beachfront/test-beachfront-bid-response-3.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/beachfront/test-cache-beachfront-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/beachfront/test-cache-beachfront-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/beachfront/test-beachfront-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/beachfront/test-beachfront-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .body(jsonFrom("openrtb2/beachfront/test-auction-beachfront-request.json")) - // this uids cookie value stands for {"uids":{"beachfront":"BF-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImJlYWNoZnJvbnQiOiJCRi1VSUQifX0=") - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/beachfront/test-auction-beachfront-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/beachfront/test-auction-beachfront-response.json", - response, singletonList("beachfront")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/beachfront/test-auction-beachfront-response.json", response, + singletonList("beachfront")); } } diff --git a/src/test/java/org/prebid/server/it/BeintooTest.java b/src/test/java/org/prebid/server/it/BeintooTest.java index b13408b5523..6aa133cdc67 100644 --- a/src/test/java/org/prebid/server/it/BeintooTest.java +++ b/src/test/java/org/prebid/server/it/BeintooTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -26,38 +22,14 @@ public class BeintooTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromBeintoo() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/beintoo-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("User-Agent", equalTo("Android Chrome/60")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Referer", equalTo("http://www.example.com")) - .withHeader("DNT", equalTo("2")) - .withHeader("Accept-Language", equalTo("en")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/beintoo/test-beintoo-bid-request.json"), - true, true)) + .withRequestBody(equalToJson(jsonFrom("openrtb2/beintoo/test-beintoo-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/beintoo/test-beintoo-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/beintoo/test-cache-beintoo-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/beintoo/test-cache-beintoo-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"beintoo":"BT-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImJlaW50b28iOiJCVC1VSUQifX0=") - .body(jsonFrom("openrtb2/beintoo/test-auction-beintoo-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/beintoo/test-auction-beintoo-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/beintoo/test-auction-beintoo-response.json", - response, singletonList("beintoo")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/beintoo/test-auction-beintoo-response.json", response, singletonList("beintoo")); } } diff --git a/src/test/java/org/prebid/server/it/BetweenTest.java b/src/test/java/org/prebid/server/it/BetweenTest.java new file mode 100644 index 00000000000..f5fb6cf8b13 --- /dev/null +++ b/src/test/java/org/prebid/server/it/BetweenTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class BetweenTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromBetween() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/between-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/between/test-between-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/between/test-between-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/between/test-auction-between-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/between/test-auction-between-response.json", response, singletonList("between")); + } +} diff --git a/src/test/java/org/prebid/server/it/BidmachineTest.java b/src/test/java/org/prebid/server/it/BidmachineTest.java new file mode 100644 index 00000000000..7afc7bfc93b --- /dev/null +++ b/src/test/java/org/prebid/server/it/BidmachineTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class BidmachineTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromBidmachine() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bidmachine-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/bidmachine/test-bidmachine-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/bidmachine/test-bidmachine-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/bidmachine/test-auction-bidmachine-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/bidmachine/test-auction-bidmachine-response.json", response, + singletonList("bidmachine")); + } +} diff --git a/src/test/java/org/prebid/server/it/BidmyadzTest.java b/src/test/java/org/prebid/server/it/BidmyadzTest.java new file mode 100644 index 00000000000..fc895c7063b --- /dev/null +++ b/src/test/java/org/prebid/server/it/BidmyadzTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class BidmyadzTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromBidmyadz() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bidmyadz-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/bidmyadz/test-bidmyadz-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/bidmyadz/test-bidmyadz-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/bidmyadz/test-auction-bidmyadz-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/bidmyadz/test-auction-bidmyadz-response.json", response, + singletonList("bidmyadz")); + } +} diff --git a/src/test/java/org/prebid/server/it/BidscubeTest.java b/src/test/java/org/prebid/server/it/BidscubeTest.java new file mode 100644 index 00000000000..5238f50f582 --- /dev/null +++ b/src/test/java/org/prebid/server/it/BidscubeTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class BidscubeTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromBidscube() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bidscube-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/bidscube/test-bidscube-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/bidscube/test-bidscube-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/bidscube/test-auction-bidscube-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/bidscube/test-auction-bidscube-response.json", response, + singletonList("bidscube")); + } +} diff --git a/src/test/java/org/prebid/server/it/BmtmTest.java b/src/test/java/org/prebid/server/it/BmtmTest.java new file mode 100644 index 00000000000..04a97b6914d --- /dev/null +++ b/src/test/java/org/prebid/server/it/BmtmTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class BmtmTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromBmtm() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bmtm-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/bmtm/test-bmtm-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/bmtm/test-bmtm-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/bmtm/test-auction-bmtm-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/bmtm/test-auction-bmtm-response.json", response, singletonList("bmtm")); + } +} diff --git a/src/test/java/org/prebid/server/it/BrightrollTest.java b/src/test/java/org/prebid/server/it/BrightrollTest.java index 45dbdc19ee2..6a7037ca2df 100644 --- a/src/test/java/org/prebid/server/it/BrightrollTest.java +++ b/src/test/java/org/prebid/server/it/BrightrollTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,40 +21,16 @@ public class BrightrollTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromBrightroll() throws IOException, JSONException { // given - // brightroll bid response for imp 15 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/brightroll-exchange")) - .withQueryParam("publisher", equalTo("businessinsider")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("DNT", equalTo("2")) - .withHeader("Accept-Language", equalTo("en")) - .withHeader("x-openrtb-version", equalTo("2.5")) .withRequestBody(equalToJson(jsonFrom("openrtb2/brightroll/test-brightroll-bid-request-1.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/brightroll/test-brightroll-bid-response-1.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/brightroll/test-cache-brightroll-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/brightroll/test-cache-brightroll-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for{"uids":{"brightroll":"BR-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImJyaWdodHJvbGwiOiJCUi1VSUQifX0=") - .body(jsonFrom("openrtb2/brightroll/test-auction-brightroll-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/brightroll/test-auction-brightroll-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/brightroll/test-auction-brightroll-response.json", - response, singletonList("brightroll")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/brightroll/test-auction-brightroll-response.json", response, + singletonList("brightroll")); } } diff --git a/src/test/java/org/prebid/server/it/ColossusTest.java b/src/test/java/org/prebid/server/it/ColossusTest.java new file mode 100644 index 00000000000..210e6ac5397 --- /dev/null +++ b/src/test/java/org/prebid/server/it/ColossusTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class ColossusTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromColossus() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/colossus-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/colossus/test-colossus-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/colossus/test-colossus-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/colossus/test-auction-colossus-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/colossus/test-auction-colossus-response.json", response, + singletonList("colossus")); + } +} diff --git a/src/test/java/org/prebid/server/it/ConnectAdTest.java b/src/test/java/org/prebid/server/it/ConnectAdTest.java new file mode 100644 index 00000000000..6b9854ac5c7 --- /dev/null +++ b/src/test/java/org/prebid/server/it/ConnectAdTest.java @@ -0,0 +1,42 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class ConnectAdTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromConnectAd() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/connectad-exchange")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) + .withHeader("User-Agent", equalTo("userAgent")) + .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) + .withHeader("DNT", equalTo("0")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/connectad/test-connectad-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/connectad/test-connectad-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/connectad/test-auction-connectad-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/connectad/test-auction-connectad-response.json", response, + singletonList("connectad")); + } +} diff --git a/src/test/java/org/prebid/server/it/ConsumableTest.java b/src/test/java/org/prebid/server/it/ConsumableTest.java index f216b6dc260..c0b10480a04 100644 --- a/src/test/java/org/prebid/server/it/ConsumableTest.java +++ b/src/test/java/org/prebid/server/it/ConsumableTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -13,9 +12,9 @@ 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.matching; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,9 +23,7 @@ public class ConsumableTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromConsumable() throws IOException, JSONException { // given - // consumable bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/consumable-exchange")) - .withHeader("Cookie", equalTo("azk=CS-UID")) .withHeader("Origin", equalTo("http://www.example.com")) .withHeader("Accept", equalTo("application/json")) .withHeader("User-Agent", equalTo("userAgent")) @@ -34,37 +31,18 @@ public void openrtb2AuctionShouldRespondWithBidsFromConsumable() throws IOExcept .withHeader("Referer", equalTo("http://www.example.com")) .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) .withHeader("Host", equalTo("localhost:8090")) - .withHeader("Content-Length", equalTo("372")) + .withHeader("Content-Length", matching("[0-9]*")) .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withCookie("azk", equalTo("CS-UID")) - // The "time" field in consumable bid request is not being checked as its value is Instance.now() .withRequestBody(equalToJson(jsonFrom("openrtb2/consumable/test-consumable-bid-request-1.json"), true, true)) .willReturn(aResponse().withBody(jsonFrom("openrtb2/consumable/test-consumable-bid-response-1.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/consumable/test-cache-consumable-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/consumable/test-cache-consumable-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"consumable":"CS-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImNvbnN1bWFibGUiOiJDUy1VSUQifX0=") - .body(jsonFrom("openrtb2/consumable/test-auction-consumable-request.json")) - .post("/openrtb2/auction"); - - final int indexOfTime = response.asString().indexOf("\\\"time\\\""); - final String timeMs = response.asString().substring(indexOfTime + 9, indexOfTime + 19); + final Response response = responseFor("openrtb2/consumable/test-auction-consumable-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/consumable/test-auction-consumable-response.json", - response, singletonList("consumable")).replaceAll("\\{\\{time}}", timeMs); - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/consumable/test-auction-consumable-response.json", response, + singletonList("consumable")); } } diff --git a/src/test/java/org/prebid/server/it/ConversantTest.java b/src/test/java/org/prebid/server/it/ConversantTest.java index 39259b1a8a0..69b0430a8fb 100644 --- a/src/test/java/org/prebid/server/it/ConversantTest.java +++ b/src/test/java/org/prebid/server/it/ConversantTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,10 +13,8 @@ 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 io.restassured.RestAssured.given; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) public class ConversantTest extends IntegrationTest { @@ -28,104 +25,33 @@ public class ConversantTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromConversant() throws IOException, JSONException { // given - // conversant bid response for imp 4 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/conversant-exchange")) .withRequestBody(equalToJson(jsonFrom("openrtb2/conversant/test-conversant-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/conversant/test-conversant-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/conversant/test-cache-conversant-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/conversant/test-cache-conversant-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"conversant":"CV-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImNvbnZlcnNhbnQiOiJDVi1VSUQifX0=") - .body(jsonFrom("openrtb2/conversant/test-auction-conversant-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/conversant/test-auction-conversant-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/conversant/test-auction-conversant-response.json", - response, singletonList(CONVERSANT)); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/conversant/test-auction-conversant-response.json", response, + singletonList(CONVERSANT)); } @Test public void openrtb2AuctionShouldRespondWithBidsFromConversantAlias() throws IOException, JSONException { // given - // conversant bid response for imp 4 with alias parameters WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/conversant-exchange")) .withRequestBody(equalToJson(jsonFrom("openrtb2/conversant/alias/test-conversant-bid-request.json"))) .willReturn(aResponse().withBody( jsonFrom("openrtb2/conversant/alias/test-conversant-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/conversant/alias/test-cache-conversant-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/conversant/alias/test-cache-conversant-response.json")))); - - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"conversant":"CV-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImNvbnZlcnNhbnQiOiJDVi1VSUQifX0=") - .body(jsonFrom("openrtb2/conversant/alias/test-auction-conversant-request.json")) - .post("/openrtb2/auction"); - - // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/conversant/alias/test-auction-conversant-response.json", - response, asList(CONVERSANT, CONVERSANT_ALIAS)); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); - } - - @Test - public void auctionShouldRespondWithBidsFromConversant() throws IOException { - // given - // conversant bid response for ad unit 10 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/conversant-exchange")) - .withRequestBody(equalToJson(jsonFrom("auction/conversant/test-conversant-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/conversant/test-conversant-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("auction/conversant/test-cache-conversant-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/conversant/test-cache-conversant-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - //this uids cookie value stands for {"uids":{"conversant":"CV-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImNvbnZlcnNhbnQiOiJDVi1VSUQifX0=") - .queryParam("debug", "1") - .body(jsonFrom("auction/conversant/test-auction-conversant-request.json")) - .post("/auction"); + final Response response = responseFor("openrtb2/conversant/alias/test-auction-conversant-request.json", + Endpoint.openrtb2_auction); // then - assertThat(response.header("Cache-Control")).isEqualTo("no-cache, no-store, must-revalidate"); - assertThat(response.header("Pragma")).isEqualTo("no-cache"); - assertThat(response.header("Expires")).isEqualTo("0"); - assertThat(response.header("Access-Control-Allow-Credentials")).isEqualTo("true"); - assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("http://www.example.com"); - - final String expectedAuctionResponse = legacyAuctionResponseFrom( - "auction/conversant/test-auction-conversant-response.json", - response, asList(CONVERSANT, CONVERSANT_ALIAS)); - assertThat(response.asString()).isEqualTo(expectedAuctionResponse); + assertJsonEquals("openrtb2/conversant/alias/test-auction-conversant-response.json", response, + asList(CONVERSANT, CONVERSANT_ALIAS)); } } diff --git a/src/test/java/org/prebid/server/it/CpmStarTest.java b/src/test/java/org/prebid/server/it/CpmStarTest.java index 57135fb477a..535179e1e48 100644 --- a/src/test/java/org/prebid/server/it/CpmStarTest.java +++ b/src/test/java/org/prebid/server/it/CpmStarTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,34 +21,16 @@ public class CpmStarTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromCPMStar() throws IOException, JSONException { // given - // Cpmstar bid response WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cpmstar-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/cpmstar/test-cpmstar-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/cpmstar/test-cpmstar-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/cpmstar/test-cache-cpmstar-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/cpmstar/test-cache-cpmstar-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/cpmstar/test-cpmstar-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/cpmstar/test-cpmstar-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7ImNwbXN0YXIiOiJDUy1VSUQifX0=") - .body(jsonFrom("openrtb2/cpmstar/test-auction-cpmstar-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/cpmstar/test-auction-cpmstar-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/cpmstar/test-auction-cpmstar-response.json", - response, singletonList("cpmstar")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/cpmstar/test-auction-cpmstar-response.json", response, singletonList("cpmstar")); } } diff --git a/src/test/java/org/prebid/server/it/CriteoTest.java b/src/test/java/org/prebid/server/it/CriteoTest.java new file mode 100644 index 00000000000..731fd1df9da --- /dev/null +++ b/src/test/java/org/prebid/server/it/CriteoTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class CriteoTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromCriteo() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/criteo-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/criteo/test-criteo-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/criteo/test-criteo-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/criteo/test-auction-criteo-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/criteo/test-auction-criteo-response.json", response, singletonList("criteo")); + } +} diff --git a/src/test/java/org/prebid/server/it/DatablocksTest.java b/src/test/java/org/prebid/server/it/DatablocksTest.java index dc7e8a2835b..d580abe0d54 100644 --- a/src/test/java/org/prebid/server/it/DatablocksTest.java +++ b/src/test/java/org/prebid/server/it/DatablocksTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,39 +21,16 @@ public class DatablocksTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromDatablocks() throws IOException, JSONException { // given - // Datablocks bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/datablocks-exchange")) - .withQueryParam("sid", equalTo("1")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/datablocks/test-datablocks-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/datablocks/test-datablocks-bid-response-1.json")))); - - // Datablocks bid response for imp 002 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/datablocks-exchange")) - .withQueryParam("sid", equalTo("2")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/datablocks/test-datablocks-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/datablocks/test-datablocks-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/datablocks/test-cache-datablocks-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/datablocks/test-cache-datablocks-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/datablocks/test-datablocks-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/datablocks/test-datablocks-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"datablocks":"DB-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImRhdGFibG9ja3MiOiJEQi1VSUQifX0=") - .body(jsonFrom("openrtb2/datablocks/test-auction-datablocks-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/datablocks/test-auction-datablocks-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/datablocks/test-auction-datablocks-response.json", - response, singletonList("datablocks")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/datablocks/test-auction-datablocks-response.json", response, + singletonList("datablocks")); } } diff --git a/src/test/java/org/prebid/server/it/DealsSimulationTest.java b/src/test/java/org/prebid/server/it/DealsSimulationTest.java new file mode 100644 index 00000000000..e79d90e20b3 --- /dev/null +++ b/src/test/java/org/prebid/server/it/DealsSimulationTest.java @@ -0,0 +1,268 @@ +package org.prebid.server.it; + +import com.github.tomakehurst.wiremock.client.VerificationException; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.github.tomakehurst.wiremock.matching.AnythingPattern; +import com.github.tomakehurst.wiremock.verification.LoggedRequest; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import org.json.JSONException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.LineItemService; +import org.skyscreamer.jsonassert.ArrayValueMatcher; +import org.skyscreamer.jsonassert.Customization; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.ValueMatcher; +import org.skyscreamer.jsonassert.comparator.CustomComparator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.WeekFields; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +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.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.awaitility.Awaitility.await; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RunWith(SpringRunner.class) +@TestPropertySource(locations = { + "test-application.properties", + "deals/test-deals-application.properties", + "deals/test-deals-simulation-application.properties"}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class DealsSimulationTest extends VertxTest { + + private static final int APP_PORT = 10080; + private static final int WIREMOCK_PORT = 8090; + + private static final RequestSpecification SPEC = IntegrationTest.spec(APP_PORT); + + @ClassRule + public static final WireMockClassRule WIRE_MOCK_RULE = new WireMockClassRule(options().port(WIREMOCK_PORT)); + + private static final ZonedDateTime NOW = ZonedDateTime.now( + Clock.fixed(Instant.parse("2019-10-10T00:00:00Z"), ZoneOffset.UTC)); + + private static final DateTimeFormatter UTC_MILLIS_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .toFormatter(); + + private static final String RUBICON = "rubicon"; + + @Autowired + private LineItemService lineItemService; + + @Autowired + private Clock clock; + + @BeforeClass + public static void setUpInner() throws IOException { + // given + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/planner-plan")) + .withQueryParam("instanceId", equalTo("localhost")) + .withQueryParam("region", equalTo("local")) + .withQueryParam("vendor", equalTo("local")) + .withBasicAuth("username", "password") + .withHeader("pg-trx-id", new AnythingPattern()) + .withHeader("pg-sim-timestamp", equalTo(UTC_MILLIS_FORMATTER.format(NOW))) + .willReturn(aResponse() + .withBody(IntegrationTest.jsonFrom("deals/simulation/test-planner-plan-response-1.json")))); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/planner-register")) + .withBasicAuth("username", "password") + .withHeader("pg-trx-id", new AnythingPattern()) + .withHeader("pg-sim-timestamp", equalTo(NOW.toString())) + .withRequestBody(equalToJson(IntegrationTest.jsonFrom( + "deals/simulation/test-planner-register-request-1.json"))) + .willReturn(aResponse())); + } + + @Test + public void openrtb2AuctionShouldRespondWithDealBids() throws IOException, JSONException, InterruptedException { + // given + given(SPEC) + .header("pg-sim-timestamp", NOW.plusSeconds(0).toString()) + .when() + .post("/pbs-admin/e2eAdmin/planner/fetchLineItems"); + + TimeUnit.SECONDS.sleep(1); // no way to check that planner response handling is complete + + given(SPEC) + .when() + .header("pg-sim-timestamp", NOW.plusSeconds(1).toString()) + .post("/pbs-admin/e2eAdmin/advancePlans"); + + awaitForLineItemMetadata(NOW.plusSeconds(1)); + + given(SPEC) + .when() + .body(IntegrationTest.jsonFrom("deals/simulation/test-bid-rates.json")) + .post("/pbs-admin/e2eAdmin/bidRate"); + + final Response beforePlansUpdateResponse = given(SPEC) + .header("Referer", "http://www.example.com") + .header("User-Agent", "userAgent") + .header("X-Forwarded-For", "185.199.110.153") + .header("pg-sim-timestamp", NOW.plusSeconds(2).toString()) + // this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT"}} + .cookie("uids", "eyJ1aWRzIjp7InJ1Ymljb24iOiJKNVZMQ1dRUC0yNi1DV0ZUIn19") + .body(IntegrationTest.jsonFrom("deals/simulation/test-auction-request.json")) + .post("/openrtb2/auction"); + + assertResponse("deals/simulation/test-auction-response-1.json", beforePlansUpdateResponse, + singletonList(RUBICON)); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/delivery-stats-progress")) + .withBasicAuth("username", "password") + .withHeader("pg-sim-timestamp", equalTo(UTC_MILLIS_FORMATTER.format(NOW.plusSeconds(3)))) + .withHeader("pg-trx-id", new AnythingPattern()) + .willReturn(aResponse())); + + given(SPEC) + .when() + .header("pg-sim-timestamp", NOW.plusSeconds(3).toString()) + .post("/pbs-admin/e2eAdmin/dealstats/report"); + + // update plans for now date = 2019-10-10T00:15:00Z - making lineItem1 inactive due to absence of active plan + given(SPEC) + .when() + .header("pg-sim-timestamp", NOW.plusMinutes(15).toString()) + .post("/pbs-admin/e2eAdmin/advancePlans"); + + final Response afterPlansUpdateResponse = given(SPEC) + .header("Referer", "http://www.example.com") + .header("User-Agent", "userAgent") + .header("X-Forwarded-For", "185.199.110.153") + .header("pg-sim-timestamp", NOW.plusMinutes(16).toString()) + // this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT"}} + .cookie("uids", "eyJ1aWRzIjp7InJ1Ymljb24iOiJKNVZMQ1dRUC0yNi1DV0ZUIn19") + .body(IntegrationTest.jsonFrom("deals/simulation/test-auction-request.json")) + .post("/openrtb2/auction"); + + assertResponse("deals/simulation/test-auction-response-2.json", afterPlansUpdateResponse, + singletonList(RUBICON)); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/delivery-stats-progress")) + .withBasicAuth("username", "password") + .withHeader("pg-sim-timestamp", equalTo(UTC_MILLIS_FORMATTER.format(NOW.plusMinutes(17)))) + .withHeader("pg-trx-id", new AnythingPattern()) + .willReturn(aResponse())); + + given(SPEC) + .when() + .header("pg-sim-timestamp", NOW.plusMinutes(17).toString()) + .post("/pbs-admin/e2eAdmin/dealstats/report"); + + assertDeliveryStatsProgressRequests( + "deals/simulation/test-delivery-stats-progress-request-1.json", + "deals/simulation/test-delivery-stats-progress-request-2.json"); + + given(SPEC) + .header("pg-sim-timestamp", NOW.toString()) + .when() + .post("/pbs-admin/e2eAdmin/planner/register"); + } + + private void awaitForLineItemMetadata(ZonedDateTime now) { + await().atMost(10, TimeUnit.SECONDS).pollInterval(100, TimeUnit.MILLISECONDS) + .until(() -> lineItemService.accountHasDeals("2001", now)); + } + + /** + * Timestamps in response are always generated anew. + * This comparator allows to just verify they are present and parsable. + */ + private static CustomComparator openrtbDeepDebugTimeComparator() { + final ValueMatcher timeValueMatcher = (actual, expected) -> { + try { + return mapper.readValue("\"" + actual.toString() + "\"", ZonedDateTime.class) != null; + } catch (IOException e) { + return false; + } + }; + + final ArrayValueMatcher arrayValueMatcher = new ArrayValueMatcher<>(new CustomComparator( + JSONCompareMode.NON_EXTENSIBLE, + new Customization("ext.debug.trace.deals[*].time", timeValueMatcher))); + + final List arrayValueMatchers = IntStream.range(1, 5) + .mapToObj(i -> new Customization("ext.debug.trace.lineitems.lineItem" + i, + new ArrayValueMatcher<>(new CustomComparator( + JSONCompareMode.NON_EXTENSIBLE, + new Customization("ext.debug.trace.lineitems.lineItem" + i + "[*].time", + timeValueMatcher))))) + .collect(Collectors.toList()); + + arrayValueMatchers.add(new Customization("ext.debug.trace.deals", arrayValueMatcher)); + + return new CustomComparator(JSONCompareMode.NON_EXTENSIBLE, + arrayValueMatchers.toArray(new Customization[arrayValueMatchers.size()])); + } + + private void assertResponse(String expectedResponsePath, Response response, List bidders) + throws IOException, JSONException { + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + expectedResponsePath, response, bidders)); + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + } + + public static void assertDeliveryStatsProgressRequests(String path1, String path2) + throws IOException, JSONException { + final String firstReportRequest = IntegrationTest.jsonFrom(path1); + final String secondReportRequest = IntegrationTest.jsonFrom(path2); + + await().atMost(20, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).until(() -> + verify(() -> WIRE_MOCK_RULE.verify(2, postRequestedFor(urlPathEqualTo("/delivery-stats-progress"))))); + + final List loggedRequests = + WIRE_MOCK_RULE.findAll(postRequestedFor(urlPathEqualTo("/delivery-stats-progress"))); + + JSONAssert.assertEquals(firstReportRequest, loggedRequests.get(0).getBodyAsString(), JSONCompareMode.LENIENT); + JSONAssert.assertEquals(secondReportRequest, loggedRequests.get(1).getBodyAsString(), JSONCompareMode.LENIENT); + } + + private String withTemporalFields(String auctionResponse) { + final ZonedDateTime dateTime = ZonedDateTime.now(clock); + + return auctionResponse + .replaceAll("\"?\\{\\{ userdow }}\"?", Integer.toString( + dateTime.getDayOfWeek().get(WeekFields.SUNDAY_START.dayOfWeek()))) + .replaceAll("\"?\\{\\{ userhour }}\"?", Integer.toString(dateTime.getHour())); + } + + private static boolean verify(Runnable verify) { + try { + verify.run(); + return true; + } catch (VerificationException e) { + return false; + } + } +} diff --git a/src/test/java/org/prebid/server/it/DealsTest.java b/src/test/java/org/prebid/server/it/DealsTest.java new file mode 100644 index 00000000000..0dcacc57602 --- /dev/null +++ b/src/test/java/org/prebid/server/it/DealsTest.java @@ -0,0 +1,322 @@ +package org.prebid.server.it; + +import com.github.tomakehurst.wiremock.client.VerificationException; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.github.tomakehurst.wiremock.matching.AnythingPattern; +import com.github.tomakehurst.wiremock.verification.LoggedRequest; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import org.apache.commons.collections4.CollectionUtils; +import org.json.JSONException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.LineItemService; +import org.prebid.server.deals.proto.report.DeliveryProgressReport; +import org.prebid.server.deals.proto.report.Event; +import org.prebid.server.deals.proto.report.LineItemStatus; +import org.skyscreamer.jsonassert.ArrayValueMatcher; +import org.skyscreamer.jsonassert.Customization; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompare; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.ValueMatcher; +import org.skyscreamer.jsonassert.comparator.CustomComparator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.time.Clock; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +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.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RunWith(SpringRunner.class) +@TestPropertySource(locations = {"test-application.properties", "deals/test-deals-application.properties"}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class DealsTest extends VertxTest { + + private static final int APP_PORT = 8070; + private static final int WIREMOCK_PORT = 8090; + + private static final RequestSpecification SPEC = IntegrationTest.spec(APP_PORT); + + @SuppressWarnings("unchecked") + @ClassRule + public static final WireMockClassRule WIRE_MOCK_RULE = new WireMockClassRule(options() + .port(WIREMOCK_PORT) + .extensions( + IntegrationTest.CacheResponseTransformer.class, + IntegrationTest.ResponseOrderTransformer.class)); + + private static final String RUBICON = "rubicon"; + + @Autowired + private LineItemService lineItemService; + + @Autowired + private Clock clock; + + @BeforeClass + public static void setUpInner() throws IOException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/delivery-stats-progress")) + .withBasicAuth("username", "password") + .withHeader("pg-trx-id", new AnythingPattern()) + .willReturn(aResponse())); + + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/planner-plan")) + .withQueryParam("instanceId", equalTo("localhost")) + .withQueryParam("region", equalTo("local")) + .withQueryParam("vendor", equalTo("local")) + .withBasicAuth("username", "password") + .withHeader("pg-trx-id", new AnythingPattern()) + .willReturn(aResponse().withBody(plannerResponseFrom("deals/test-planner-plan-response-1.json")))); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/planner-register")) + .withBasicAuth("username", "password") + .withHeader("pg-trx-id", new AnythingPattern()) + .withRequestBody(equalToJson(IntegrationTest.jsonFrom( + "deals/test-planner-register-request-1.json"), false, true)) + .willReturn(aResponse().withBody(IntegrationTest.jsonFrom( + "deals/test-planner-register-response.json")))); + + // pre-bid cache + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) + .withRequestBody(IntegrationTest.equalToBidCacheRequest( + IntegrationTest.jsonFrom("deals/test-cache-deals-request.json"))) + .willReturn(aResponse() + .withTransformers("cache-response-transformer") + .withTransformerParameter("matcherName", "deals/test-cache-matcher.json"))); + } + + @Test + public void openrtb2AuctionShouldRespondWithDealBids() throws IOException, JSONException { + // given + awaitForLineItemMetadata(); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/user-data-win-event")) + .withRequestBody(equalToJson(IntegrationTest.jsonFrom( + "deals/test-user-data-win-event-request-1.json"), false, true)) + .willReturn(aResponse())); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/user-data-details")) + .withRequestBody(equalToJson(IntegrationTest.jsonFrom( + "deals/test-user-data-details-request-1.json"), false, true)) + .willReturn(aResponse().withBody(IntegrationTest.jsonFrom( + "deals/test-user-data-details-response-1.json")))); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .withRequestBody(equalToJson(IntegrationTest.jsonFrom( + "deals/test-rubicon-bid-request-1.json"), false, true)) + .willReturn(aResponse().withBody(IntegrationTest.jsonFrom( + "deals/test-rubicon-bid-response-1.json")))); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .withRequestBody(equalToJson(IntegrationTest.jsonFrom( + "deals/test-rubicon-bid-request-2.json"), false, true)) + .willReturn(aResponse() + .withFixedDelay(300) + .withBody(IntegrationTest.jsonFrom("deals/test-rubicon-bid-response-2.json")))); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .withRequestBody(equalToJson(IntegrationTest.jsonFrom( + "deals/test-rubicon-bid-request-3.json"), false, true)) + .willReturn(aResponse().withBody(IntegrationTest.jsonFrom("deals/test-rubicon-bid-response-3.json")))); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .withRequestBody(equalToJson(IntegrationTest.jsonFrom( + "deals/test-rubicon-bid-request-4.json"), false, true)) + .willReturn(aResponse().withBody(IntegrationTest.jsonFrom("deals/test-rubicon-bid-response-4.json")))); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .withRequestBody(equalToJson(IntegrationTest.jsonFrom( + "deals/test-rubicon-bid-request-5.json"), false, true)) + .willReturn(aResponse() + .withFixedDelay(600) + .withBody(IntegrationTest.jsonFrom("deals/test-rubicon-bid-response-5.json")))); + + // when + final Response response = given(SPEC) + .header("Referer", "http://www.example.com") + .header("User-Agent", "userAgent") + .header("X-Forwarded-For", "185.199.110.153") + // this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT"}} + .cookie("uids", "eyJ1aWRzIjp7InJ1Ymljb24iOiJKNVZMQ1dRUC0yNi1DV0ZUIn19") + .body(IntegrationTest.jsonFrom("deals/test-auction-request.json")) + .post("/openrtb2/auction"); + + // then + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + "deals/test-auction-response.json", response, singletonList(RUBICON))); + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + + // when + final Response eventResponse = given(SPEC) + .queryParam("t", "win") + .queryParam("b", "bidId") + .queryParam("a", "14062") + .queryParam("l", "lineItem1") + .queryParam("f", "i") + // this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT"}} + .cookie("uids", "eyJ1aWRzIjp7InJ1Ymljb24iOiJKNVZMQ1dRUC0yNi1DV0ZUIn19") + .get("/event"); + + // then + assertThat(eventResponse.getStatusCode()).isEqualTo(200); + + await().atMost(1, TimeUnit.SECONDS).pollInterval(50, TimeUnit.MILLISECONDS).until(() -> + verify(() -> WIRE_MOCK_RULE.verify(postRequestedFor(urlPathEqualTo("/user-data-win-event"))))); + + // verify delivery stats report + await().atMost(10, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).until(() -> + verify(() -> WIRE_MOCK_RULE.verify(2, postRequestedFor(urlPathEqualTo("/delivery-stats-progress"))))); + + final String expectedRequestBody = IntegrationTest.jsonFrom( + "deals/test-delivery-stats-progress-request-1.json"); + + final List requestList = WIRE_MOCK_RULE.findAll( + postRequestedFor(urlPathEqualTo("/delivery-stats-progress"))); + + final DeliveryProgressReport report = chooseReportToCompare(requestList); + + JSONAssert.assertEquals(expectedRequestBody, mapper.writeValueAsString(report), JSONCompareMode.LENIENT); + } + + private static String plannerResponseFrom(String templatePath) throws IOException { + final ZonedDateTime now = ZonedDateTime.now().withFixedOffsetZone(); + + return IntegrationTest.jsonFrom(templatePath) + .replaceAll("\\{\\{ now }}", now.toString()) + .replaceAll("\\{\\{ lineItem.startTime }}", now.minusDays(5).toString()) + .replaceAll("\\{\\{ lineItem.endTime }}", now.plusDays(5).toString()) + .replaceAll("\\{\\{ plan.startTime }}", now.minusHours(1).toString()) + .replaceAll("\\{\\{ plan.endTime }}", now.plusHours(1).toString()); + } + + private String withTemporalFields(String auctionResponse) { + final ZonedDateTime dateTime = ZonedDateTime.now(clock); + + return auctionResponse + .replaceAll("\"?\\{\\{ userdow }}\"?", Integer.toString( + dateTime.getDayOfWeek().get(WeekFields.SUNDAY_START.dayOfWeek()))) + .replaceAll("\"?\\{\\{ userhour }}\"?", Integer.toString(dateTime.getHour())); + } + + private void awaitForLineItemMetadata() { + await().atMost(2, TimeUnit.SECONDS).pollInterval(100, TimeUnit.MILLISECONDS) + .until(() -> lineItemService.accountHasDeals("2001", ZonedDateTime.now(clock))); + } + + /** + * Timestamps in response are always generated anew. + * This comparator allows to just verify they are present and parsable. + */ + private static CustomComparator openrtbDeepDebugTimeComparator() { + final ValueMatcher timeValueMatcher = (actual, expected) -> { + try { + return mapper.readValue("\"" + actual.toString() + "\"", ZonedDateTime.class) != null; + } catch (IOException e) { + return false; + } + }; + + final ArrayValueMatcher arrayValueMatcher = new ArrayValueMatcher<>(new CustomComparator( + JSONCompareMode.NON_EXTENSIBLE, + new Customization("ext.debug.trace.deals[*].time", timeValueMatcher))); + + final ValueMatcher jsonStringValueMatcher = (actual, expected) -> { + try { + return !JSONCompare.compareJSON(actual.toString(), expected.toString(), JSONCompareMode.NON_EXTENSIBLE) + .failed(); + } catch (JSONException e) { + throw new RuntimeException("Unexpected json exception", e); + } + }; + + final ArrayValueMatcher cacheArrayValueMatcher = new ArrayValueMatcher<>(new CustomComparator( + JSONCompareMode.NON_EXTENSIBLE, + new Customization("ext.debug.httpcalls.cache[*].requestbody", jsonStringValueMatcher), + new Customization("ext.debug.httpcalls.cache[*].responsebody", jsonStringValueMatcher))); + + final List arrayValueMatchers = IntStream.range(1, 5) + .mapToObj(i -> new Customization("ext.debug.trace.lineitems.lineItem" + i, + new ArrayValueMatcher<>(new CustomComparator( + JSONCompareMode.NON_EXTENSIBLE, + new Customization("ext.debug.trace.lineitems.lineItem" + i + "[*].time", + timeValueMatcher))))) + .collect(Collectors.toList()); + + arrayValueMatchers.add(new Customization("ext.debug.trace.deals", arrayValueMatcher)); + arrayValueMatchers.add(new Customization("ext.debug.httpcalls.cache", cacheArrayValueMatcher)); + arrayValueMatchers.add(new Customization("**.requestheaders.x-prebid", (o1, o2) -> true)); + return new CustomComparator(JSONCompareMode.NON_EXTENSIBLE, arrayValueMatchers.toArray(new Customization[0])); + } + + private static boolean verify(Runnable verify) { + try { + verify.run(); + return true; + } catch (VerificationException e) { + return false; + } + } + + private static DeliveryProgressReport chooseReportToCompare(List requestList) + throws com.fasterxml.jackson.core.JsonProcessingException { + final DeliveryProgressReport firstReport = mapper.readValue(requestList.get(0).getBodyAsString(), + DeliveryProgressReport.class); + final DeliveryProgressReport secondReport = mapper.readValue(requestList.get(1).getBodyAsString(), + DeliveryProgressReport.class); + + // in a reason cron high dependent on time value, report with statistic should be chosen + final DeliveryProgressReport report = firstReport.getClientAuctions() != 0 ? firstReport : secondReport; + final LineItemStatus lineItem1 = firstReport.getLineItemStatus().stream() + .filter(lineItemStatus -> lineItemStatus.getLineItemId().equals("lineItem1")) + .findFirst().orElse(null); + + // if report does not contain win event for lineItem1 it is possible that it got by the second report + if (lineItem1 != null && CollectionUtils.isEmpty(lineItem1.getEvents())) { + final Set mergedEvents = lineItem1.getEvents(); + + firstReport.getLineItemStatus().stream() + .filter(lineItemStatus -> lineItemStatus.getLineItemId().equals("lineItem1")) + .map(LineItemStatus::getEvents) + .filter(CollectionUtils::isNotEmpty) + .findFirst() + .ifPresent(mergedEvents::addAll); + + secondReport.getLineItemStatus().stream() + .filter(lineItemStatus -> lineItemStatus.getLineItemId().equals("lineItem1")) + .map(LineItemStatus::getEvents) + .filter(CollectionUtils::isNotEmpty) + .findFirst() + .ifPresent(mergedEvents::addAll); + } + + return report; + } +} diff --git a/src/test/java/org/prebid/server/it/DecenteradsTest.java b/src/test/java/org/prebid/server/it/DecenteradsTest.java new file mode 100644 index 00000000000..4f89a0d22e5 --- /dev/null +++ b/src/test/java/org/prebid/server/it/DecenteradsTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class DecenteradsTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromDecenterads() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/decenterads-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/decenterads/test-decenterads-bid-request.json"))) + .willReturn(aResponse() + .withBody(jsonFrom("openrtb2/decenterads/test-decenterads-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/decenterads/test-auction-decenterads-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/decenterads/test-auction-decenterads-response.json", response, + singletonList("decenterads")); + } +} diff --git a/src/test/java/org/prebid/server/it/DeepintentTest.java b/src/test/java/org/prebid/server/it/DeepintentTest.java new file mode 100644 index 00000000000..d289602b19c --- /dev/null +++ b/src/test/java/org/prebid/server/it/DeepintentTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class DeepintentTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromDeepintent() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/deepintent-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/deepintent/test-deepintent-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/deepintent/test-deepintent-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/deepintent/test-auction-deepintent-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/deepintent/test-auction-deepintent-response.json", response, + singletonList("deepintent")); + } +} diff --git a/src/test/java/org/prebid/server/it/DmxTest.java b/src/test/java/org/prebid/server/it/DmxTest.java index d8d2c036dfa..e5f32f5cb99 100644 --- a/src/test/java/org/prebid/server/it/DmxTest.java +++ b/src/test/java/org/prebid/server/it/DmxTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,34 +21,15 @@ public class DmxTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromDmx() throws IOException, JSONException { // given - // DmxBidder bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/dmx-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) .withRequestBody(equalToJson(jsonFrom("openrtb2/dmx/test-dmx-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/dmx/test-dmx-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/dmx/test-cache-dmx-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/dmx/test-cache-dmx-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"dmx":"DM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImRteCI6IkRNLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/dmx/test-auction-dmx-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/dmx/test-auction-dmx-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/dmx/test-auction-dmx-response.json", - response, singletonList("dmx")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/dmx/test-auction-dmx-response.json", response, singletonList("dmx")); } } diff --git a/src/test/java/org/prebid/server/it/EmxdigitalTest.java b/src/test/java/org/prebid/server/it/EmxdigitalTest.java index a919a80e6be..f3a368dc0ba 100644 --- a/src/test/java/org/prebid/server/it/EmxdigitalTest.java +++ b/src/test/java/org/prebid/server/it/EmxdigitalTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -26,41 +22,17 @@ public class EmxdigitalTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromEmxdigital() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/emx_digital-exchange")) - .withQueryParam("t", equalTo("1000")) - .withQueryParam("ts", equalTo("2060541160")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("User-Agent", equalTo("Android Chrome/60")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Referer", equalTo("http://www.example.com")) - .withHeader("DNT", equalTo("2")) - .withHeader("Accept-Language", equalTo("en")) .withRequestBody(equalToJson(jsonFrom("openrtb2/emxdigital/test-emxdigital-bid-request.json"), - true, true)) + true, false)) .willReturn(aResponse().withBody(jsonFrom("openrtb2/emxdigital/test-emxdigital-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/emxdigital/test-cache-emxdigital-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/emxdigital/test-cache-emxdigital-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"emxdigital":"STR-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImVteGRpZ2l0YWwiOiJTVFItVUlEIn19") - .body(jsonFrom("openrtb2/emxdigital/test-auction-emxdigital-request.json")) - .post("/openrtb2/auction"); + final Response response = + responseFor("openrtb2/emxdigital/test-auction-emxdigital-request.json", Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/emxdigital/test-auction-emxdigital-response.json", + assertJsonEquals("openrtb2/emxdigital/test-auction-emxdigital-response.json", response, singletonList("emx_digital")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); } } diff --git a/src/test/java/org/prebid/server/it/EngagebdrTest.java b/src/test/java/org/prebid/server/it/EngagebdrTest.java index 6e0491cc608..9782d720fa7 100644 --- a/src/test/java/org/prebid/server/it/EngagebdrTest.java +++ b/src/test/java/org/prebid/server/it/EngagebdrTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,45 +21,18 @@ public class EngagebdrTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromEngagebdr() throws IOException, JSONException { // given - // engagebdr bid response for imp 021 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/engagebdr-exchange")) - .withQueryParam("zoneid", equalTo("99999")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/engagebdr/test-engagebdr-bid-request-1.json"))) + .withRequestBody(equalToJson(jsonFrom("openrtb2/engagebdr/test-engagebdr-bid-request.json"))) .willReturn(aResponse().withBody( - jsonFrom("openrtb2/engagebdr/test-engagebdr-bid-response-1.json")))); - - // engagebdr bid response for imp 022 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/engagebdr-exchange")) - .withQueryParam("zoneid", equalTo("88888")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/engagebdr/test-engagebdr-bid-request-2.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/engagebdr/test-engagebdr-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/engagebdr/test-cache-engagebdr-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/engagebdr/test-cache-engagebdr-response.json")))); + jsonFrom("openrtb2/engagebdr/test-engagebdr-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"engagebdr":"EG-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImVuZ2FnZWJkciI6IkVHLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/engagebdr/test-auction-engagebdr-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/engagebdr/test-auction-engagebdr-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/engagebdr/test-auction-engagebdr-response.json", - response, singletonList("engagebdr")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/engagebdr/test-auction-engagebdr-response.json", response, + singletonList("engagebdr")); } } diff --git a/src/test/java/org/prebid/server/it/EplanningTest.java b/src/test/java/org/prebid/server/it/EplanningTest.java index df239b9d3d2..654f94c091a 100644 --- a/src/test/java/org/prebid/server/it/EplanningTest.java +++ b/src/test/java/org/prebid/server/it/EplanningTest.java @@ -4,20 +4,14 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; -import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -26,42 +20,15 @@ public class EplanningTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromEplanning() throws IOException, JSONException { // given - // eplanning bid response for imp15 - WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/eplanning-exchange/12345/1/example.com/ROS")) - .withQueryParam("r", equalTo("pbs")) - .withQueryParam("ncb", equalTo("1")) - .withQueryParam("ur", equalTo("https://www.example.com")) - .withQueryParam("e", equalTo("testadunitcode:600x300")) - .withQueryParam("ip", equalTo("193.168.244.1")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("DNT", equalTo("2")) - .withHeader("Accept-Language", equalTo("en")) + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/eplanning-exchange/12345/1/www.example.com/ROS")) .willReturn(aResponse().withBody(jsonFrom("openrtb2/eplanning/test-eplanning-bid-response-1.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/eplanning/test-cache-eplanning-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/eplanning/test-cache-eplanning-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "https://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "https://www.example.com") - // this uids cookie value stands for {"uids":{""eplanning":"EP-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImVwbGFubmluZyI6IkVQLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/eplanning/test-auction-eplanning-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/eplanning/test-auction-eplanning-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/eplanning/test-auction-eplanning-response.json", - response, singletonList("eplanning")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/eplanning/test-auction-eplanning-response.json", response, + singletonList("eplanning")); } } diff --git a/src/test/java/org/prebid/server/it/EpomTest.java b/src/test/java/org/prebid/server/it/EpomTest.java new file mode 100644 index 00000000000..5d0cece3f04 --- /dev/null +++ b/src/test/java/org/prebid/server/it/EpomTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class EpomTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheMediaEpom() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/epom-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/epom/test-epom-bid-request-1.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/epom/test-epom-bid-response-1.json")))); + + // when + final Response response = responseFor("openrtb2/epom/test-auction-epom-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/epom/test-auction-epom-response.json", response, singletonList("epom")); + } +} diff --git a/src/test/java/org/prebid/server/it/EvolutionTest.java b/src/test/java/org/prebid/server/it/EvolutionTest.java new file mode 100644 index 00000000000..c839b37a5fa --- /dev/null +++ b/src/test/java/org/prebid/server/it/EvolutionTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class EvolutionTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromEvolution() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/evolution-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/evolution/test-evolution-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/evolution/test-evolution-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/evolution/test-auction-evolution-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/evolution/test-auction-evolution-response.json", response, + singletonList("e_volution")); + } +} diff --git a/src/test/java/org/prebid/server/it/FacebookTest.java b/src/test/java/org/prebid/server/it/FacebookTest.java index 614fa7b7428..090db856d2b 100644 --- a/src/test/java/org/prebid/server/it/FacebookTest.java +++ b/src/test/java/org/prebid/server/it/FacebookTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,46 +21,16 @@ public class FacebookTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromFacebook() throws IOException, JSONException { // given - // facebook bid response for impId001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/audienceNetwork-exchange")) - .withHeader("X-Fb-Pool-Routing-Token", equalTo("FB-UID")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/facebook/test-facebook-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/facebook/test-facebook-bid-response-1.json")))); - - // facebook bid response for impId002 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/audienceNetwork-exchange")) - .withHeader("X-Fb-Pool-Routing-Token", equalTo("FB-UID")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/facebook/test-facebook-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/facebook/test-facebook-bid-response-2.json")))); - - // facebook bid response for impId003 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/audienceNetwork-exchange")) - .withHeader("X-Fb-Pool-Routing-Token", equalTo("FB-UID")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/facebook/test-facebook-bid-request-3.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/facebook/test-facebook-bid-response-3.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/facebook/test-cache-facebook-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/facebook/test-cache-facebook-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/facebook/test-facebook-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/facebook/test-facebook-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"audienceNetwork":"FB-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImF1ZGllbmNlTmV0d29yayI6IkZCLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/facebook/test-auction-facebook-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/facebook/test-auction-facebook-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/facebook/test-auction-facebook-response.json", - response, singletonList("audienceNetwork")); - - final String actualStr = response.asString(); - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/facebook/test-auction-facebook-response.json", response, + singletonList("audienceNetwork")); } } diff --git a/src/test/java/org/prebid/server/it/GammaTest.java b/src/test/java/org/prebid/server/it/GammaTest.java index 0e04f389151..fc625eaaed8 100644 --- a/src/test/java/org/prebid/server/it/GammaTest.java +++ b/src/test/java/org/prebid/server/it/GammaTest.java @@ -4,21 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.absent; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.equalToIgnoreCase; -import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -28,47 +22,14 @@ public class GammaTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromGamma() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/gamma-exchange/")) - .withQueryParam("id", equalTo("id")) - .withQueryParam("zid", equalTo("zid")) - .withQueryParam("wid", equalTo("wid")) - .withQueryParam("bidid", equalTo("impId1")) - .withQueryParam("hb", equalTo("pbmobile")) - .withQueryParam("device_ip", equalTo("123.123.123.12")) - .withQueryParam("device_ua", equalTo("Android Chrome/60")) - .withQueryParam("device_ifa", equalTo("ifaId")) - .withHeader("Accept", equalTo("*/*")) - .withHeader("Connection", equalToIgnoreCase("keep-alive")) - .withHeader("Cache-Control", equalTo("no-cache")) - .withHeader("Accept-Encoding", equalTo("gzip, deflate")) - .withHeader("User-Agent", equalTo("Android Chrome/60")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withHeader("X-Forwarded-For", equalTo("123.123.123.12")) - .withHeader("Accept-Language", equalTo("fr")) - .withHeader("DNT", equalTo("2")) .withRequestBody(absent()) .willReturn(aResponse().withBody(jsonFrom("openrtb2/gamma/test-gamma-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/gamma/test-cache-gamma-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/gamma/test-cache-gamma-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"gamma":"STR-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImdhbW1hIjoiU1RSLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/gamma/test-auction-gamma-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/gamma/test-auction-gamma-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/gamma/test-auction-gamma-response.json", - response, singletonList("gamma")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/gamma/test-auction-gamma-response.json", response, singletonList("gamma")); } } diff --git a/src/test/java/org/prebid/server/it/GamoshiTest.java b/src/test/java/org/prebid/server/it/GamoshiTest.java index 7d14af354c9..01d494578e4 100644 --- a/src/test/java/org/prebid/server/it/GamoshiTest.java +++ b/src/test/java/org/prebid/server/it/GamoshiTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,40 +21,16 @@ public class GamoshiTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromGamoshi() throws IOException, JSONException { // given - // Gamoshi bid response for imp 001 and 002 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/gamoshi-exchange/r/1701/bidr")) - .withQueryParam("bidder", equalTo("prebid-server")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Accept-Language", equalTo("en")) - .withHeader("DNT", equalTo("2")) - .withHeader("x-openrtb-version", equalTo("2.4")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/gamoshi/test-gamoshi-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/gamoshi/test-gamoshi-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/gamoshi/test-cache-gamoshi-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/gamoshi/test-cache-gamoshi-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/gamoshi/test-gamoshi-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/gamoshi/test-gamoshi-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"gamoshi":"GM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImdhbW9zaGkiOiJHTS1VSUQifX0=") - .body(jsonFrom("openrtb2/gamoshi/test-auction-gamoshi-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/gamoshi/test-auction-gamoshi-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/gamoshi/test-auction-gamoshi-response.json", - response, singletonList("gamoshi")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/gamoshi/test-auction-gamoshi-response.json", response, + singletonList("gamoshi")); } } diff --git a/src/test/java/org/prebid/server/it/GridTest.java b/src/test/java/org/prebid/server/it/GridTest.java index c79783187b6..07ac0bc8c81 100644 --- a/src/test/java/org/prebid/server/it/GridTest.java +++ b/src/test/java/org/prebid/server/it/GridTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,32 +21,15 @@ public class GridTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromTheMediaGrid() throws IOException, JSONException { // given - // TheMediaGrid bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/grid-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/grid/test-grid-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/grid/test-grid-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/grid/test-cache-grid-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/grid/test-cache-grid-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/grid/test-grid-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/grid/test-grid-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"grid":"GRID-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImdyaWQiOiJHUklELVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/grid/test-auction-grid-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/grid/test-auction-grid-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/grid/test-auction-grid-response.json", - response, singletonList("grid")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/grid/test-auction-grid-response.json", response, singletonList("grid")); } } diff --git a/src/test/java/org/prebid/server/it/GumgumTest.java b/src/test/java/org/prebid/server/it/GumgumTest.java index 76e13f5aeaa..530bd5fd570 100644 --- a/src/test/java/org/prebid/server/it/GumgumTest.java +++ b/src/test/java/org/prebid/server/it/GumgumTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,32 +21,15 @@ public class GumgumTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromGumGum() throws IOException, JSONException { // given - // GumGum bid response for imp 001 and 002 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/gumgum-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/gumgum/test-gumgum-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/gumgum/test-gumgum-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/gumgum/test-cache-gumgum-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/gumgum/test-cache-gumgum-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/gumgum/test-gumgum-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/gumgum/test-gumgum-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"gum":"GUM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Imd1bSI6IkdVTS1VSUQifX0=") - .body(jsonFrom("openrtb2/gumgum/test-auction-gumgum-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/gumgum/test-auction-gumgum-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/gumgum/test-auction-gumgum-response.json", - response, singletonList("gumgum")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/gumgum/test-auction-gumgum-response.json", response, singletonList("gumgum")); } } diff --git a/src/test/java/org/prebid/server/it/ImprovedigitalTest.java b/src/test/java/org/prebid/server/it/ImprovedigitalTest.java index 5febb215a5b..a8e35f39fec 100644 --- a/src/test/java/org/prebid/server/it/ImprovedigitalTest.java +++ b/src/test/java/org/prebid/server/it/ImprovedigitalTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,36 +21,18 @@ public class ImprovedigitalTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromImproveDigital() throws IOException, JSONException { // given - // Improvedigital bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/improvedigital-exchange")) .withRequestBody(equalToJson( - jsonFrom("openrtb2/improvedigital/test-improvedigital-bid-request-1.json"))) + jsonFrom("openrtb2/improvedigital/test-improvedigital-bid-request.json"))) .willReturn(aResponse().withBody( - jsonFrom("openrtb2/improvedigital/test-improvedigital-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson( - jsonFrom("openrtb2/improvedigital/test-cache-improvedigital-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/improvedigital/test-cache-improvedigital-response.json")))); + jsonFrom("openrtb2/improvedigital/test-improvedigital-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"improvedigital":"ID-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImltcHJvdmVkaWdpdGFsIjoiSUQtVUlEIn19") - .body(jsonFrom("openrtb2/improvedigital/test-auction-improvedigital-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/improvedigital/test-auction-improvedigital-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/improvedigital/test-auction-improvedigital-response.json", - response, singletonList("improvedigital")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/improvedigital/test-auction-improvedigital-response.json", response, + singletonList("improvedigital")); } } diff --git a/src/test/java/org/prebid/server/it/InmobiTest.java b/src/test/java/org/prebid/server/it/InmobiTest.java index 9b38c25671c..7d7d274525f 100644 --- a/src/test/java/org/prebid/server/it/InmobiTest.java +++ b/src/test/java/org/prebid/server/it/InmobiTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,32 +22,14 @@ public class InmobiTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromInmobi() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/inmobi-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) .withRequestBody(equalToJson(jsonFrom("openrtb2/inmobi/test-inmobi-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/inmobi/test-inmobi-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/inmobi/test-cache-inmobi-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/inmobi/test-cache-inmobi-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"inmobi":"IM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImlubW9iaSI6IklNLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/inmobi/test-auction-inmobi-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/inmobi/test-auction-inmobi-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/inmobi/test-auction-inmobi-response.json", - response, singletonList("inmobi")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/inmobi/test-auction-inmobi-response.json", response, singletonList("inmobi")); } } diff --git a/src/test/java/org/prebid/server/it/IntegrationTest.java b/src/test/java/org/prebid/server/it/IntegrationTest.java index fbc2e0c110d..cbd7b1697e4 100644 --- a/src/test/java/org/prebid/server/it/IntegrationTest.java +++ b/src/test/java/org/prebid/server/it/IntegrationTest.java @@ -6,12 +6,16 @@ import com.github.tomakehurst.wiremock.extension.ResponseTransformer; import com.github.tomakehurst.wiremock.http.Request; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.github.tomakehurst.wiremock.matching.StringValuePattern; import io.restassured.builder.RequestSpecBuilder; import io.restassured.config.ObjectMapperConfig; import io.restassured.config.RestAssuredConfig; import io.restassured.internal.mapping.Jackson2Mapper; import io.restassured.response.Response; import io.restassured.specification.RequestSpecification; +import lombok.AllArgsConstructor; +import lombok.Value; +import org.apache.commons.lang3.StringUtils; import org.json.JSONException; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -21,43 +25,69 @@ import org.prebid.server.cache.proto.request.PutObject; import org.prebid.server.cache.proto.response.BidCacheResponse; import org.prebid.server.cache.proto.response.CacheObject; +import org.prebid.server.it.hooks.TestHooksConfiguration; +import org.prebid.server.it.util.BidCacheRequestPattern; +import org.prebid.server.model.Endpoint; import org.skyscreamer.jsonassert.ArrayValueMatcher; import org.skyscreamer.jsonassert.Customization; +import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompare; import org.skyscreamer.jsonassert.JSONCompareMode; import org.skyscreamer.jsonassert.ValueMatcher; import org.skyscreamer.jsonassert.comparator.CustomComparator; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; import org.springframework.test.context.TestPropertySource; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static io.restassured.RestAssured.given; import static java.lang.String.format; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) -@TestPropertySource("test-application.properties") +@TestPropertySource({"test-application.properties", "test-application-hooks.properties"}) +@Import(TestHooksConfiguration.class) public abstract class IntegrationTest extends VertxTest { private static final int APP_PORT = 8080; private static final int WIREMOCK_PORT = 8090; + private static final String ANY_SYMBOL_REGEX = ".*"; + private static final Pattern UTC_MILLIS_PATTERN = + Pattern.compile(".*([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z).*"); @SuppressWarnings("unchecked") @ClassRule public static final WireMockClassRule WIRE_MOCK_RULE = new WireMockClassRule(options() .port(WIREMOCK_PORT) .gzipDisabled(true) + .jettyStopTimeout(5000L) .extensions(IntegrationTest.CacheResponseTransformer.class)); @Rule public WireMockClassRule instanceRule = WIRE_MOCK_RULE; - static final RequestSpecification SPEC = spec(APP_PORT); + protected static final RequestSpecification SPEC = spec(APP_PORT); + private static final String HOST_AND_PORT = "localhost:" + WIREMOCK_PORT; + private static final String CACHE_PATH = "/cache"; + private static final String CACHE_ENDPOINT = "http://" + HOST_AND_PORT + CACHE_PATH; + private static final String USER_SERVICE_PATH = "/user-data-details"; + private static final String USER_SERVICE_ENDPOINT = "http://" + HOST_AND_PORT + USER_SERVICE_PATH; @BeforeClass public static void setUp() throws IOException { @@ -76,45 +106,67 @@ static RequestSpecification spec(int port) { .build(); } - static String jsonFrom(String file) throws IOException { - // workaround to clear formatting - return mapper.writeValueAsString(mapper.readTree(IntegrationTest.class.getResourceAsStream(file))); + protected static Response responseFor(String file, Endpoint endpoint) throws IOException { + return given(SPEC) + .header("Referer", "http://www.example.com") + .header("X-Forwarded-For", "193.168.244.1") + .header("User-Agent", "userAgent") + .header("Origin", "http://www.example.com") + .body(jsonFrom(file)) + .post(endpoint.value()); } - static String legacyAuctionResponseFrom(String templatePath, Response response, List bidders) - throws IOException { - - return auctionResponseFrom(templatePath, response, - "bidder_status.find { it.bidder == '%s' }.response_time_ms", bidders); + protected static String jsonFrom(String file) throws IOException { + // workaround to clear formatting + return mapper.writeValueAsString(mapper.readTree(IntegrationTest.class.getResourceAsStream(file))); } - static String openrtbAuctionResponseFrom(String templatePath, Response response, List bidders) + protected static String openrtbAuctionResponseFrom(String templatePath, Response response, List bidders) throws IOException { - return auctionResponseFrom(templatePath, response, "ext.responsetimemillis.%s", bidders); + return auctionResponseFrom(templatePath, response, "ext.responsetimemillis.%s", + "ext.debug.httpcalls.userservice[0].requestbody", bidders); } private static String auctionResponseFrom(String templatePath, Response response, String responseTimePath, - List bidders) throws IOException { - final String hostAndPort = "localhost:" + WIREMOCK_PORT; - final String cachePath = "/cache"; - final String cacheEndpoint = "http://" + hostAndPort + cachePath; + String responseUserTimePath, List bidders) throws IOException { - String result = jsonFrom(templatePath) - .replaceAll("\\{\\{ cache.endpoint }}", cacheEndpoint) - .replaceAll("\\{\\{ cache.resource_url }}", cacheEndpoint + "?uuid=") - .replaceAll("\\{\\{ cache.host }}", hostAndPort) - .replaceAll("\\{\\{ cache.path }}", cachePath); + String result = replaceStaticInfo(jsonFrom(templatePath)); for (final String bidder : bidders) { result = result.replaceAll("\\{\\{ " + bidder + "\\.exchange_uri }}", - "http://" + hostAndPort + "/" + bidder + "-exchange"); + "http://" + HOST_AND_PORT + "/" + bidder + "-exchange"); result = setResponseTime(response, result, bidder, responseTimePath); } + if (StringUtils.isNotBlank(responseUserTimePath)) { + result = setUserServiceTimestamp(response, result, responseUserTimePath); + } return result; } + private static String setUserServiceTimestamp(Response response, String expectedResponseJson, + String responseUserTimePath) { + final Object val; + try { + val = response.path(responseUserTimePath); + } catch (Exception e) { + return expectedResponseJson; + } + + if (val == null) { + return expectedResponseJson; + } + final String userRequest = val.toString(); + final Matcher m = UTC_MILLIS_PATTERN.matcher(userRequest); + String userRequestDate; + if (m.find()) { + userRequestDate = m.group(1); + return expectedResponseJson.replaceAll("\\{\\{ userservice_time }}", userRequestDate); + } + return expectedResponseJson; + } + private static String setResponseTime(Response response, String expectedResponseJson, String bidder, String responseTimePath) { final Object val = response.path(format(responseTimePath, bidder)); @@ -125,7 +177,7 @@ private static String setResponseTime(Response response, String expectedResponse } final Object cacheVal = response.path("ext.responsetimemillis.cache"); - final Integer cacheResponseTime = val instanceof Integer ? (Integer) cacheVal : null; + final Integer cacheResponseTime = cacheVal instanceof Integer ? (Integer) cacheVal : null; if (cacheResponseTime != null) { expectedResponseJson = expectedResponseJson.replaceAll("\"\\{\\{ cache\\.response_time_ms }}\"", cacheResponseTime.toString()); @@ -146,7 +198,7 @@ private static String cacheResponseFromRequestJson( final List responseCacheObjects = new ArrayList<>(); for (PutObject putItem : puts) { final String id = putItem.getType().equals("json") - ? putItem.getValue().get("id").textValue() + "@" + putItem.getValue().get("price") + ? putItem.getValue().get("id").textValue() + "@" + resolvePriceForJsonMediaType(putItem) : putItem.getValue().textValue(); final String uuid = jsonNodeMatcher.get(id).textValue(); @@ -158,11 +210,17 @@ private static String cacheResponseFromRequestJson( } } + private static String resolvePriceForJsonMediaType(PutObject putItem) { + final JsonNode extObject = putItem.getValue().get("ext"); + final JsonNode origBidCpm = extObject != null ? extObject.get("origbidcpm") : null; + return origBidCpm != null ? origBidCpm.toString() : putItem.getValue().get("price").toString(); + } + /** * Cache debug fields "requestbody" and "responsebody" are escaped JSON strings. - * This comparator allows to compare them with actual values as usual JSON objects. + * This customization allows to compare them with actual values as usual JSON objects. */ - static CustomComparator openrtbCacheDebugComparator() { + static Customization openrtbCacheDebugCustomization() { final ValueMatcher jsonStringValueMatcher = (actual, expected) -> { try { return !JSONCompare.compareJSON(actual.toString(), expected.toString(), JSONCompareMode.NON_EXTENSIBLE) @@ -177,9 +235,54 @@ static CustomComparator openrtbCacheDebugComparator() { new Customization("ext.debug.httpcalls.cache[*].requestbody", jsonStringValueMatcher), new Customization("ext.debug.httpcalls.cache[*].responsebody", jsonStringValueMatcher))); - return new CustomComparator( - JSONCompareMode.NON_EXTENSIBLE, - new Customization("ext.debug.httpcalls.cache", arrayValueMatcher)); + return new Customization("ext.debug.httpcalls.cache", arrayValueMatcher); + } + + static Customization headersDebugCustomization() { + return new Customization("**.requestheaders.x-prebid", (o1, o2) -> true); + } + + protected static void assertJsonEquals(String file, + Response response, + List bidders, + Customization... customizations) throws IOException, JSONException { + final List fullCustomizations = new ArrayList<>(Arrays.asList(customizations)); + fullCustomizations.add(new Customization("ext.prebid.auctiontimestamp", (o1, o2) -> true)); + fullCustomizations.add(new Customization("ext.responsetimemillis.cache", (o1, o2) -> true)); + String expectedRequest = replaceStaticInfo(jsonFrom(file)); + for (String bidder : bidders) { + expectedRequest = replaceBidderRelatedStaticInfo(expectedRequest, bidder); + fullCustomizations.add(new Customization( + String.format("ext.responsetimemillis.%s", bidder), (o1, o2) -> true)); + } + + JSONAssert.assertEquals(expectedRequest, response.asString(), + new CustomComparator(JSONCompareMode.NON_EXTENSIBLE, + fullCustomizations.toArray(new Customization[0]))); + } + + private static String replaceStaticInfo(String json) { + + return json.replaceAll("\\{\\{ cache.endpoint }}", CACHE_ENDPOINT) + .replaceAll("\\{\\{ cache.resource_url }}", CACHE_ENDPOINT + "?uuid=") + .replaceAll("\\{\\{ cache.host }}", HOST_AND_PORT) + .replaceAll("\\{\\{ cache.path }}", CACHE_PATH) + .replaceAll("\\{\\{ userservice_uri }}", USER_SERVICE_ENDPOINT) + .replaceAll("\\{\\{ event.url }}", "http://localhost:8080/event?"); + } + + private static String replaceBidderRelatedStaticInfo(String json, String bidder) { + + return json.replaceAll("\\{\\{ " + bidder + "\\.exchange_uri }}", + "http://" + HOST_AND_PORT + "/" + bidder + "-exchange"); + } + + static BidCacheRequestPattern equalToBidCacheRequest(String json) { + return new BidCacheRequestPattern(json); + } + + protected static StringValuePattern notEmpty() { + return matching(ANY_SYMBOL_REGEX); } public static class CacheResponseTransformer extends ResponseTransformer { @@ -191,7 +294,8 @@ public com.github.tomakehurst.wiremock.http.Response transform( final String newResponse; try { - newResponse = cacheResponseFromRequestJson(request.getBodyAsString(), + newResponse = cacheResponseFromRequestJson( + request.getBodyAsString(), parameters.getString("matcherName")); } catch (IOException e) { return com.github.tomakehurst.wiremock.http.Response.response() @@ -212,4 +316,88 @@ public boolean applyGlobally() { return false; } } + + static final String LINE_ITEM_RESPONSE_ORDER = "lineItemResponseOrder"; + static final String ID_TO_EXECUTION_PARAMETERS = "idToExecutionParameters"; + + public static class ResponseOrderTransformer extends ResponseTransformer { + + private static final String LINE_ITEM_PATH = "/imp/0/ext/rp/target/line_item"; + + private final Lock lock = new ReentrantLock(); + private final Condition lockCondition = lock.newCondition(); + + @Override + @SuppressWarnings("unchecked") + public com.github.tomakehurst.wiremock.http.Response transform( + Request request, + com.github.tomakehurst.wiremock.http.Response response, + FileSource files, + Parameters parameters) { + + final Queue lineItemResponseOrder = + (Queue) parameters.get(LINE_ITEM_RESPONSE_ORDER); + final Map idToParameters = + (Map) parameters.get(ID_TO_EXECUTION_PARAMETERS); + + String requestDealId; + try { + requestDealId = readStringValue(mapper.readTree(request.getBodyAsString()), LINE_ITEM_PATH); + } catch (IOException e) { + throw new RuntimeException("Request should contain imp/ext/rp/target/line_item for deals request"); + } + + final BidRequestExecutionParameters requestParameters = idToParameters.get(requestDealId); + + waitForTurn(lineItemResponseOrder, requestParameters.getDealId(), requestParameters.getDelay()); + + return com.github.tomakehurst.wiremock.http.Response.response() + .body(requestParameters.getBody()) + .status(requestParameters.getStatus()) + .build(); + } + + private void waitForTurn(Queue dealsResponseOrder, String id, Long delay) { + lock.lock(); + try { + while (!dealsResponseOrder.peek().equals(id)) { + lockCondition.await(); + } + TimeUnit.MILLISECONDS.sleep(delay); + dealsResponseOrder.poll(); + lockCondition.signalAll(); + } catch (InterruptedException e) { + throw new RuntimeException(format("Failed on waiting to return bid request for lineItem id = %s", + id)); + } finally { + lock.unlock(); + } + } + + private String readStringValue(JsonNode jsonNode, String path) { + return jsonNode.at(path).asText(); + } + + @Override + public String getName() { + return "response-order-transformer"; + } + + @Override + public boolean applyGlobally() { + return false; + } + } + + @Value + @AllArgsConstructor(staticName = "of") + static class BidRequestExecutionParameters { + String dealId; + + String body; + + Integer status; + + Long delay; + } } diff --git a/src/test/java/org/prebid/server/it/InteractiveoffersTest.java b/src/test/java/org/prebid/server/it/InteractiveoffersTest.java new file mode 100644 index 00000000000..914f9869662 --- /dev/null +++ b/src/test/java/org/prebid/server/it/InteractiveoffersTest.java @@ -0,0 +1,38 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class InteractiveoffersTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromInteractiveoffers() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/interactiveoffers-exchange")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/interactiveoffers/test-interactiveoffers-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/interactiveoffers/test-interactiveoffers-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/interactiveoffers/test-auction-interactiveoffers-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json", response, + singletonList("interactiveoffers")); + } +} diff --git a/src/test/java/org/prebid/server/it/InvibesTest.java b/src/test/java/org/prebid/server/it/InvibesTest.java new file mode 100644 index 00000000000..6295cbb8e0e --- /dev/null +++ b/src/test/java/org/prebid/server/it/InvibesTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class InvibesTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromInvibes() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/invibes-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/invibes/test-invibes-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/invibes/test-invibes-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/invibes/test-auction-invibes-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/invibes/test-auction-invibes-response.json", response, singletonList("invibes")); + } +} diff --git a/src/test/java/org/prebid/server/it/IxTest.java b/src/test/java/org/prebid/server/it/IxTest.java index 9d965edeb16..77487501a0b 100644 --- a/src/test/java/org/prebid/server/it/IxTest.java +++ b/src/test/java/org/prebid/server/it/IxTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,87 +13,22 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) public class IxTest extends IntegrationTest { - private static final String IX = "ix"; - @Test public void openrtb2AuctionShouldRespondWithBidsFromIx() throws IOException, JSONException { // given - // ix bid response for imp 6 with 300x250 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/ix-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/ix/test-ix-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/ix/test-ix-bid-response-1.json")))); - - // ix bid response for imp 6 with 600x480 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/ix-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/ix/test-ix-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/ix/test-ix-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/ix/test-cache-ix-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/ix/test-cache-ix-response.json")))); - - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"ix":"IE-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Iml4IjoiSUUtVUlEIn19") - .body(jsonFrom("openrtb2/ix/test-auction-ix-request.json")) - .post("/openrtb2/auction"); - - // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/ix/test-auction-ix-response.json", - response, singletonList(IX)); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); - } - - @Test - public void auctionShouldRespondWithBidsFromIx() throws IOException { - // given - // ix bid response for ad unit 7 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/ix-exchange")) - .withRequestBody(equalToJson(jsonFrom("auction/ix/test-ix-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/ix/test-ix-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("auction/ix/test-cache-ix-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/ix/test-cache-ix-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/ix/test-ix-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/ix/test-ix-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - //this uids cookie value stands for {"uids":{"ix":"IE-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Iml4IjoiSUUtVUlEIn19") - .queryParam("debug", "1") - .body(jsonFrom("auction/ix/test-auction-ix-request.json")) - .post("/auction"); + final Response response = responseFor("openrtb2/ix/test-auction-ix-request.json", Endpoint.openrtb2_auction); // then - assertThat(response.header("Cache-Control")).isEqualTo("no-cache, no-store, must-revalidate"); - assertThat(response.header("Pragma")).isEqualTo("no-cache"); - assertThat(response.header("Expires")).isEqualTo("0"); - assertThat(response.header("Access-Control-Allow-Credentials")).isEqualTo("true"); - assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("http://www.example.com"); - - final String expectedAuctionResponse = legacyAuctionResponseFrom( - "auction/ix/test-auction-ix-response.json", - response, singletonList(IX)); - assertThat(response.asString()).isEqualTo(expectedAuctionResponse); + assertJsonEquals("openrtb2/ix/test-auction-ix-response.json", response, singletonList("ix")); } } diff --git a/src/test/java/org/prebid/server/it/JixieTest.java b/src/test/java/org/prebid/server/it/JixieTest.java new file mode 100644 index 00000000000..97081f71d99 --- /dev/null +++ b/src/test/java/org/prebid/server/it/JixieTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class JixieTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromJixie() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/jixie-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/jixie/test-jixie-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/jixie/test-jixie-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/jixie/test-auction-jixie-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/jixie/test-auction-jixie-response.json", response, singletonList("jixie")); + } +} diff --git a/src/test/java/org/prebid/server/it/KayzenTest.java b/src/test/java/org/prebid/server/it/KayzenTest.java new file mode 100644 index 00000000000..36d42dd8934 --- /dev/null +++ b/src/test/java/org/prebid/server/it/KayzenTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class KayzenTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromKayzen() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/kayzen-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/kayzen/test-kayzen-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/kayzen/test-kayzen-bid-response.json")))); + // when + final Response response = responseFor("openrtb2/kayzen/test-auction-kayzen-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/kayzen/test-auction-kayzen-response.json", response, + singletonList("kayzen")); + } +} diff --git a/src/test/java/org/prebid/server/it/KidozTest.java b/src/test/java/org/prebid/server/it/KidozTest.java index 660b3ea4629..e1d586c3d25 100644 --- a/src/test/java/org/prebid/server/it/KidozTest.java +++ b/src/test/java/org/prebid/server/it/KidozTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,43 +21,15 @@ public class KidozTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromKidoz() throws IOException, JSONException { // given - // Kidoz bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/kidoz-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/kidoz/test-kidoz-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/kidoz/test-kidoz-bid-response-1.json")))); - - // Kidoz bid response for imp 002 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/kidoz-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/kidoz/test-kidoz-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/kidoz/test-kidoz-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/kidoz/test-cache-kidoz-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/kidoz/test-cache-kidoz-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/kidoz/test-kidoz-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/kidoz/test-kidoz-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7ImtpZG96IjoiS1otVUlEIn19") - .body(jsonFrom("openrtb2/kidoz/test-auction-kidoz-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/kidoz/test-auction-kidoz-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/kidoz/test-auction-kidoz-response.json", - response, singletonList("kidoz")); - - String actualStr = response.asString(); - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/kidoz/test-auction-kidoz-response.json", response, singletonList("kidoz")); } } diff --git a/src/test/java/org/prebid/server/it/KrushmediaTest.java b/src/test/java/org/prebid/server/it/KrushmediaTest.java new file mode 100644 index 00000000000..d50c507a9c8 --- /dev/null +++ b/src/test/java/org/prebid/server/it/KrushmediaTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class KrushmediaTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromKrushmedia() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/krushmedia-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/krushmedia/test-krushmedia-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/krushmedia/test-krushmedia-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/krushmedia/test-auction-krushmedia-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/krushmedia/test-auction-krushmedia-response.json", response, + singletonList("krushmedia")); + } +} diff --git a/src/test/java/org/prebid/server/it/KubientTest.java b/src/test/java/org/prebid/server/it/KubientTest.java index 600bd6b79be..2cab843fc67 100644 --- a/src/test/java/org/prebid/server/it/KubientTest.java +++ b/src/test/java/org/prebid/server/it/KubientTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,33 +21,16 @@ public class KubientTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromKubient() throws IOException, JSONException { // given - // Kubient bid response for imp 001 and 002 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/kubient-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/kubient/test-kubient-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/kubient/test-kubient-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/kubient/test-cache-kubient-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/kubient/test-cache-kubient-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/kubient/test-kubient-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/kubient/test-kubient-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"kub":"KUM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Imt1YiI6IktVTS1VSUQifX0=") - .body(jsonFrom("openrtb2/kubient/test-auction-kubient-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/kubient/test-auction-kubient-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/kubient/test-auction-kubient-response.json", - response, singletonList("kubient")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/kubient/test-auction-kubient-response.json", response, singletonList("kubient")); } } diff --git a/src/test/java/org/prebid/server/it/LifestreetTest.java b/src/test/java/org/prebid/server/it/LifestreetTest.java deleted file mode 100644 index 33e85f51cce..00000000000 --- a/src/test/java/org/prebid/server/it/LifestreetTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.prebid.server.it; - -import io.restassured.response.Response; -import org.json.JSONException; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; -import org.springframework.test.context.junit4.SpringRunner; - -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 io.restassured.RestAssured.given; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -public class LifestreetTest extends IntegrationTest { - - private static final String LIFESTREET = "lifestreet"; - - @Test - public void openrtb2AuctionShouldRespondWithBidsFromLifestreet() throws IOException, JSONException { - // given - // lifestreet bid response for imp 7 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/lifestreet-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/lifestreet/test-lifestreet-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/lifestreet/test-lifestreet-bid-response-1.json")))); - - // lifestreet bid response for imp 71 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/lifestreet-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/lifestreet/test-lifestreet-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/lifestreet/test-lifestreet-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/lifestreet/test-cache-lifestreet-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/lifestreet/test-cache-lifestreet-response.json")))); - - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"lifestreet":"LS-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImxpZmVzdHJlZXQiOiJMUy1VSUQifX0=") - .body(jsonFrom("openrtb2/lifestreet/test-auction-lifestreet-request.json")) - .post("/openrtb2/auction"); - - // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/lifestreet/test-auction-lifestreet-response.json", - response, singletonList(LIFESTREET)); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); - } - - @Test - public void auctionShouldRespondWithBidsFromLifestreet() throws IOException { - // given - // lifestreet bid response for ad unit 8 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/lifestreet-exchange")) - .withRequestBody(equalToJson(jsonFrom("auction/lifestreet/test-lifestreet-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/lifestreet/test-lifestreet-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("auction/lifestreet/test-cache-lifestreet-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/lifestreet/test-cache-lifestreet-response.json")))); - - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - //this uids cookie value stands for {"uids":{"lifestreet":"LS-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImxpZmVzdHJlZXQiOiJMUy1VSUQifX0=") - .queryParam("debug", "1") - .body(jsonFrom("auction/lifestreet/test-auction-lifestreet-request.json")) - .post("/auction"); - - // then - assertThat(response.header("Cache-Control")).isEqualTo("no-cache, no-store, must-revalidate"); - assertThat(response.header("Pragma")).isEqualTo("no-cache"); - assertThat(response.header("Expires")).isEqualTo("0"); - assertThat(response.header("Access-Control-Allow-Credentials")).isEqualTo("true"); - assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("http://www.example.com"); - - final String expectedAuctionResponse = legacyAuctionResponseFrom( - "auction/lifestreet/test-auction-lifestreet-response.json", - response, singletonList(LIFESTREET)); - assertThat(response.asString()).isEqualTo(expectedAuctionResponse); - } -} diff --git a/src/test/java/org/prebid/server/it/LockerdomeTest.java b/src/test/java/org/prebid/server/it/LockerdomeTest.java index 16575a0458e..c744e963e85 100644 --- a/src/test/java/org/prebid/server/it/LockerdomeTest.java +++ b/src/test/java/org/prebid/server/it/LockerdomeTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,33 +21,16 @@ public class LockerdomeTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromLockerDome() throws IOException, JSONException { // given - // LockerDome bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/lockerdome-exchange")) - .withHeader("x-openrtb-version", equalTo("2.5")) .withRequestBody(equalToJson(jsonFrom("openrtb2/lockerdome/test-lockerdome-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/lockerdome/test-lockerdome-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/lockerdome/test-cache-lockerdome-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/lockerdome/test-cache-lockerdome-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"lockerdome":"LD-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImxvY2tlcmRvbWUiOiJMRC1VSUQifX0=") - .body(jsonFrom("openrtb2/lockerdome/test-auction-lockerdome-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/lockerdome/test-auction-lockerdome-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/lockerdome/test-auction-lockerdome-response.json", - response, singletonList("lockerdome")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/lockerdome/test-auction-lockerdome-response.json", response, + singletonList("lockerdome")); } } diff --git a/src/test/java/org/prebid/server/it/LogicadTest.java b/src/test/java/org/prebid/server/it/LogicadTest.java index f1ae373aa22..221390fd412 100644 --- a/src/test/java/org/prebid/server/it/LogicadTest.java +++ b/src/test/java/org/prebid/server/it/LogicadTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,36 +21,15 @@ public class LogicadTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromLogicad() throws IOException, JSONException { // given - // Logicad bid response for imp WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/logicad-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) .withRequestBody(equalToJson(jsonFrom("openrtb2/logicad/test-logicad-bid-request.json"))) .willReturn(aResponse().withBody( jsonFrom("openrtb2/logicad/test-logicad-bid-response.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/logicad/test-cache-logicad-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/logicad/test-cache-logicad-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"logicad":"LC-UID"}} - .cookie("uids", "eyJ1aWRzIjp7ImxvZ2ljYWQiOiJMQy1VSUQifX0=") - .body(jsonFrom("openrtb2/logicad/test-auction-logicad-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/logicad/test-auction-logicad-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/logicad/test-auction-logicad-response.json", - response, singletonList("logicad")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/logicad/test-auction-logicad-response.json", response, singletonList("logicad")); } } diff --git a/src/test/java/org/prebid/server/it/LoopmeTest.java b/src/test/java/org/prebid/server/it/LoopmeTest.java new file mode 100644 index 00000000000..b54561c6e47 --- /dev/null +++ b/src/test/java/org/prebid/server/it/LoopmeTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class LoopmeTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromLoopme() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/loopme-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/loopme/test-loopme-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/loopme/test-loopme-bid-response.json")))); + + // when + final Response response = + responseFor("openrtb2/loopme/test-auction-loopme-request.json", Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/loopme/test-auction-loopme-response.json", response, singletonList("loopme")); + } +} diff --git a/src/test/java/org/prebid/server/it/LunamediaTest.java b/src/test/java/org/prebid/server/it/LunamediaTest.java index a3102404584..b257ab97c9d 100644 --- a/src/test/java/org/prebid/server/it/LunamediaTest.java +++ b/src/test/java/org/prebid/server/it/LunamediaTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,37 +21,16 @@ public class LunamediaTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromLunamedia() throws IOException, JSONException { // given - // Lunamedia bid response for imp WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/lunamedia-exchange")) - .withQueryParam("pubid", equalTo("19f1b372c7548ec1fe734d2c9f8dc688")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("x-openrtb-version", equalTo("2.5")) .withRequestBody(equalToJson(jsonFrom("openrtb2/lunamedia/test-lunamedia-bid-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/lunamedia/test-lunamedia-bid-response.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/lunamedia/test-cache-lunamedia-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/lunamedia/test-cache-lunamedia-response.json")))); + .willReturn(aResponse().withBody(jsonFrom("openrtb2/lunamedia/test-lunamedia-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7Imx1bmFtZWRpYSI6IkxNLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/lunamedia/test-auction-lunamedia-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/lunamedia/test-auction-lunamedia-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/lunamedia/test-auction-lunamedia-response.json", - response, singletonList("lunamedia")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/lunamedia/test-auction-lunamedia-response.json", response, + singletonList("lunamedia")); } } diff --git a/src/test/java/org/prebid/server/it/MadvertiseTest.java b/src/test/java/org/prebid/server/it/MadvertiseTest.java new file mode 100644 index 00000000000..bce8a65ffe7 --- /dev/null +++ b/src/test/java/org/prebid/server/it/MadvertiseTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class MadvertiseTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMadvertise() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/madvertise-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/madvertise/test-madvertise-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/madvertise/test-madvertise-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/madvertise/test-auction-madvertise-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/madvertise/test-auction-madvertise-response.json", response, + singletonList("madvertise")); + } +} diff --git a/src/test/java/org/prebid/server/it/MarsmediaTest.java b/src/test/java/org/prebid/server/it/MarsmediaTest.java index 2cfaa0245ff..a32a2848844 100644 --- a/src/test/java/org/prebid/server/it/MarsmediaTest.java +++ b/src/test/java/org/prebid/server/it/MarsmediaTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,36 +21,16 @@ public class MarsmediaTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromMarsmedia() throws IOException, JSONException { // given - // Marsmedia bid response for imp 001 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/marsmedia-exchange&zone=zone_1")) - //.withQueryParam("zone", equalTo("zone_1")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withHeader("DNT", equalTo("2")) - .withHeader("Accept-Language", equalTo("en")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/marsmedia/test-marsmedia-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/marsmedia/test-marsmedia-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/marsmedia/test-cache-marsmedia-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/marsmedia/test-cache-marsmedia-response.json")))); + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/marsmedia-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/marsmedia/test-marsmedia-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/marsmedia/test-marsmedia-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"marsmedia":"MM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Im1hcnNtZWRpYSI6Ik1NLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/marsmedia/test-auction-marsmedia-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/marsmedia/test-auction-marsmedia-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/marsmedia/test-auction-marsmedia-response.json", - response, singletonList("marsmedia")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/marsmedia/test-auction-marsmedia-response.json", response, + singletonList("marsmedia")); } } diff --git a/src/test/java/org/prebid/server/it/MedianetTest.java b/src/test/java/org/prebid/server/it/MedianetTest.java new file mode 100644 index 00000000000..296c65a5d52 --- /dev/null +++ b/src/test/java/org/prebid/server/it/MedianetTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class MedianetTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheMedianet() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/medianet-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/medianet/test-medianet-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/medianet/test-medianet-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/medianet/test-auction-medianet-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/medianet/test-auction-medianet-response.json", response, + singletonList("medianet")); + } +} diff --git a/src/test/java/org/prebid/server/it/MgidTest.java b/src/test/java/org/prebid/server/it/MgidTest.java index 45258c55dbe..5f8b5812816 100644 --- a/src/test/java/org/prebid/server/it/MgidTest.java +++ b/src/test/java/org/prebid/server/it/MgidTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -27,27 +25,11 @@ public void openrtb2AuctionShouldRespondWithBidsFromTheMgid() throws IOException .withRequestBody(equalToJson(jsonFrom("openrtb2/mgid/test-mgid-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/mgid/test-mgid-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/mgid/test-cache-mgid-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/mgid/test-cache-mgid-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"mgid":"MGID-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Im1naWQiOiJNR0lELVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/mgid/test-auction-mgid-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/mgid/test-auction-mgid-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/mgid/test-auction-mgid-response.json", - response, singletonList("mgid")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/mgid/test-auction-mgid-response.json", response, singletonList("mgid")); } } diff --git a/src/test/java/org/prebid/server/it/MobfoxpbTest.java b/src/test/java/org/prebid/server/it/MobfoxpbTest.java new file mode 100644 index 00000000000..b2b3e0eb2cd --- /dev/null +++ b/src/test/java/org/prebid/server/it/MobfoxpbTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class MobfoxpbTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMobfoxpb() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/mobfoxpb-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/mobfoxpb/test-mobfoxpb-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/mobfoxpb/test-mobfoxpb-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/mobfoxpb/test-auction-mobfoxpb-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json", response, + singletonList("mobfoxpb")); + } +} diff --git a/src/test/java/org/prebid/server/it/MobilefuseTest.java b/src/test/java/org/prebid/server/it/MobilefuseTest.java index 6f05d5c42c2..8aa6fc95b9b 100644 --- a/src/test/java/org/prebid/server/it/MobilefuseTest.java +++ b/src/test/java/org/prebid/server/it/MobilefuseTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,34 +21,16 @@ public class MobilefuseTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromMobilefuse() throws IOException, JSONException { // given - // MobilefuseBidder bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/mobilefuse-exchange/1111&tagid_src=ext")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) .withRequestBody(equalToJson(jsonFrom("openrtb2/mobilefuse/test-mobilefuse-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/mobilefuse/test-mobilefuse-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/mobilefuse/test-cache-mobilefuse-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/mobilefuse/test-cache-mobilefuse-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"mobilefuse":"MF-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Im1vYmlsZWZ1c2UiOiJNRi1VSUQifX0=") - .body(jsonFrom("openrtb2/mobilefuse/test-auction-mobilefuse-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/mobilefuse/test-auction-mobilefuse-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/mobilefuse/test-auction-mobilefuse-response.json", - response, singletonList("mobilefuse")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/mobilefuse/test-auction-mobilefuse-response.json", response, + singletonList("mobilefuse")); } } diff --git a/src/test/java/org/prebid/server/it/NanointeractiveTest.java b/src/test/java/org/prebid/server/it/NanointeractiveTest.java index 1b17d5e4934..e8229e43a37 100644 --- a/src/test/java/org/prebid/server/it/NanointeractiveTest.java +++ b/src/test/java/org/prebid/server/it/NanointeractiveTest.java @@ -4,66 +4,35 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) public class NanointeractiveTest extends IntegrationTest { - private static final String NANOINTERACTIVE = "nanointeractive"; - @Test public void openrtb2AuctionShouldRespondWithBidsFromNanointeractive() throws IOException, JSONException { // given - // nanointeractive bid response for imp WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/nanointeractive-exchange/")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Cookie", equalTo("Nano=NI-UID")) .withRequestBody(equalToJson( - jsonFrom("openrtb2/nanointeractive/test-nanointeractive-bid-request-1.json"))) + jsonFrom("openrtb2/nanointeractive/test-nanointeractive-bid-request.json"))) .willReturn(aResponse().withBody( - jsonFrom("openrtb2/nanointeractive/test-nanointeractive-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson( - jsonFrom("openrtb2/nanointeractive/test-cache-nanointeractive-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/nanointeractive/test-cache-nanointeractive-response.json")))); + jsonFrom("openrtb2/nanointeractive/test-nanointeractive-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Cookie", "Nano=NI-UID") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7Im5hbm9pbnRlcmFjdGl2ZSI6Ik5JLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/nanointeractive/test-auction-nanointeractive-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/nanointeractive/test-auction-nanointeractive-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/nanointeractive/test-auction-nanointeractive-response.json", - response, singletonList(NANOINTERACTIVE)); - - String actualStr = response.asString(); - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/nanointeractive/test-auction-nanointeractive-response.json", response, + singletonList("nanointeractive")); } } diff --git a/src/test/java/org/prebid/server/it/NinthdecimalTest.java b/src/test/java/org/prebid/server/it/NinthdecimalTest.java index a064afd2e31..9f64046e369 100644 --- a/src/test/java/org/prebid/server/it/NinthdecimalTest.java +++ b/src/test/java/org/prebid/server/it/NinthdecimalTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,38 +21,17 @@ public class NinthdecimalTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromNinthdecimal() throws IOException, JSONException { // given - // ninthdecimal bid response for imp WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/ninthdecimal-exchange")) - .withQueryParam("pubid", equalTo("19f1b372c7548ec1fe734d2c9f8dc688")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=UTF-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("x-openrtb-version", equalTo("2.5")) .withRequestBody(equalToJson(jsonFrom("openrtb2/ninthdecimal/test-ninthdecimal-bid-request.json"))) .willReturn(aResponse().withBody( jsonFrom("openrtb2/ninthdecimal/test-ninthdecimal-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/ninthdecimal/test-cache-ninthdecimal-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/ninthdecimal/test-cache-ninthdecimal-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"ninthdecimal":"ND-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Im5pbnRoZGVjaW1hbCI6Ik5ELVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/ninthdecimal/test-auction-ninthdecimal-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/ninthdecimal/test-auction-ninthdecimal-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/ninthdecimal/test-auction-ninthdecimal-response.json", - response, singletonList("ninthdecimal")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/ninthdecimal/test-auction-ninthdecimal-response.json", response, + singletonList("ninthdecimal")); } } diff --git a/src/test/java/org/prebid/server/it/NobidTest.java b/src/test/java/org/prebid/server/it/NobidTest.java new file mode 100644 index 00000000000..e114efa8f8f --- /dev/null +++ b/src/test/java/org/prebid/server/it/NobidTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class NobidTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromNobid() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/nobid-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/nobid/test-nobid-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/nobid/test-nobid-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/nobid/test-auction-nobid-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/nobid/test-auction-nobid-response.json", response, singletonList("nobid")); + } +} diff --git a/src/test/java/org/prebid/server/it/OnetagTest.java b/src/test/java/org/prebid/server/it/OnetagTest.java new file mode 100644 index 00000000000..90ff435f81c --- /dev/null +++ b/src/test/java/org/prebid/server/it/OnetagTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class OnetagTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOnetag() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/onetag-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/onetag/test-onetag-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/onetag/test-onetag-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/onetag/test-auction-onetag-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/onetag/test-auction-onetag-response.json", response, singletonList("onetag")); + } +} diff --git a/src/test/java/org/prebid/server/it/OpenxTest.java b/src/test/java/org/prebid/server/it/OpenxTest.java index 3da4d82290c..2b628f4c0f4 100644 --- a/src/test/java/org/prebid/server/it/OpenxTest.java +++ b/src/test/java/org/prebid/server/it/OpenxTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,43 +21,15 @@ public class OpenxTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromOpenx() throws IOException, JSONException { // given - // openx bid response for imp 011 and 02 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/openx-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/openx/test-openx-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/openx/test-openx-bid-response-1.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/openx/test-openx-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/openx/test-openx-bid-response.json")))); - // openx bid response for imp 03 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/openx-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/openx/test-openx-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/openx/test-openx-bid-response-2.json")))); - - // openx bid response for imp 04 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/openx-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/openx/test-openx-bid-request-3.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/openx/test-openx-bid-response-3.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/openx/test-cache-openx-request.json"), true, false)) - .willReturn(aResponse() - .withTransformers("cache-response-transformer") - .withTransformerParameter("matcherName", "openrtb2/openx/test-cache-matcher-openx.json") - )); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"openx":"OX-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Im9wZW54IjoiT1gtVUlEIn19") - .body(jsonFrom("openrtb2/openx/test-auction-openx-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/openx/test-auction-openx-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/openx/test-auction-openx-response.json", response, singletonList("openx")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/openx/test-auction-openx-response.json", response, singletonList("openx")); } } diff --git a/src/test/java/org/prebid/server/it/OrbidderTest.java b/src/test/java/org/prebid/server/it/OrbidderTest.java index 638078fdeb7..70dfd2b89d2 100644 --- a/src/test/java/org/prebid/server/it/OrbidderTest.java +++ b/src/test/java/org/prebid/server/it/OrbidderTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,34 +21,16 @@ public class OrbidderTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromOrbidder() throws IOException, JSONException { // given - // OrbidderBidder bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/orbidder-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) .withRequestBody(equalToJson(jsonFrom("openrtb2/orbidder/test-orbidder-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/orbidder/test-orbidder-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/orbidder/test-cache-orbidder-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/orbidder/test-cache-orbidder-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - //this uids cookie value stands for {"uids":{"orbidder":"OB-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Im9yYmlkZGVyIjoiT0ItVUlEIn19") - .body(jsonFrom("openrtb2/orbidder/test-auction-orbidder-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/orbidder/test-auction-orbidder-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/orbidder/test-auction-orbidder-response.json", - response, singletonList("orbidder")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/orbidder/test-auction-orbidder-response.json", response, + singletonList("orbidder")); } } diff --git a/src/test/java/org/prebid/server/it/OutbrainTest.java b/src/test/java/org/prebid/server/it/OutbrainTest.java new file mode 100644 index 00000000000..0b2baf6f66a --- /dev/null +++ b/src/test/java/org/prebid/server/it/OutbrainTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class OutbrainTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOutbrain() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/outbrain-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/outbrain/test-outbrain-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/outbrain/test-outbrain-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/outbrain/test-auction-outbrain-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/outbrain/test-auction-outbrain-response.json", response, + singletonList("outbrain")); + } +} diff --git a/src/test/java/org/prebid/server/it/PangleTest.java b/src/test/java/org/prebid/server/it/PangleTest.java new file mode 100644 index 00000000000..4155095c07d --- /dev/null +++ b/src/test/java/org/prebid/server/it/PangleTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class PangleTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromPangle() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/pangle-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/pangle/test-pangle-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/pangle/test-pangle-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/pangle/test-auction-pangle-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/pangle/test-auction-pangle-response.json", response, + singletonList("pangle")); + } +} diff --git a/src/test/java/org/prebid/server/it/PrematureReturnTest.java b/src/test/java/org/prebid/server/it/PrematureReturnTest.java new file mode 100644 index 00000000000..5729f7c9b9c --- /dev/null +++ b/src/test/java/org/prebid/server/it/PrematureReturnTest.java @@ -0,0 +1,441 @@ +package org.prebid.server.it; + +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.github.tomakehurst.wiremock.matching.AnythingPattern; +import io.restassured.specification.RequestSpecification; +import org.json.JSONException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.LineItemService; +import org.skyscreamer.jsonassert.ArrayValueMatcher; +import org.skyscreamer.jsonassert.Customization; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.ValueMatcher; +import org.skyscreamer.jsonassert.comparator.CustomComparator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.time.Clock; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +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.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.awaitility.Awaitility.await; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RunWith(SpringRunner.class) +@TestPropertySource(locations = {"test-application.properties", "deals/test-deals-application.properties"}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class PrematureReturnTest extends VertxTest { + + private static final int APP_PORT = 8070; + private static final int WIREMOCK_PORT = 8090; + + private static final RequestSpecification SPEC = IntegrationTest.spec(APP_PORT); + + @SuppressWarnings("unchecked") + @ClassRule + public static final WireMockClassRule WIRE_MOCK_RULE = new WireMockClassRule( + options().port(WIREMOCK_PORT).extensions(IntegrationTest.ResponseOrderTransformer.class)); + + private static final String RUBICON = "rubicon"; + + @Autowired + private LineItemService lineItemService; + + @Autowired + private Clock clock; + + @BeforeClass + public static void setUpInner() throws IOException { + // given + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/planner-plan")) + .withQueryParam("instanceId", equalTo("localhost")) + .withQueryParam("region", equalTo("local")) + .withQueryParam("vendor", equalTo("local")) + .withBasicAuth("username", "password") + .withHeader("pg-trx-id", new AnythingPattern()) + .willReturn(aResponse() + .withBody(plannerResponseFrom("deals/premature/test-planner-plan-response.json")))); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/planner-register")) + .withBasicAuth("username", "password") + .withHeader("pg-trx-id", new AnythingPattern()) + .withRequestBody(equalToJson(IntegrationTest.jsonFrom("deals/test-planner-register-request-1.json"), + false, true)) + .willReturn(aResponse())); + + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/user-data-details")) + .withRequestBody(equalToJson(IntegrationTest + .jsonFrom("deals/test-user-data-details-request-1.json"), false, true)) + .willReturn(aResponse().withBody(IntegrationTest + .jsonFrom("deals/test-user-data-details-response-1.json")))); + } + + @Test + public void openrtb2AuctionWhenAllThreeBidsReturnsInOrderWithMinimalDelay() throws IOException, JSONException { + // given + awaitForLineItemMetadata(); + + final Queue lineItemResponseOrder = new LinkedList<>(); + lineItemResponseOrder.add("extLineItem1"); + lineItemResponseOrder.add("extLineItem2"); + lineItemResponseOrder.add("extLineItem3"); + + final Map idToExecutionParameters = new HashMap<>(); + idToExecutionParameters.put("extLineItem1", IntegrationTest.BidRequestExecutionParameters.of("extLineItem1", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-1.json"), 200, 0L)); + idToExecutionParameters.put("extLineItem2", IntegrationTest.BidRequestExecutionParameters.of("extLineItem2", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-2.json"), 200, 20L)); + idToExecutionParameters.put("extLineItem3", IntegrationTest.BidRequestExecutionParameters.of("extLineItem3", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-3.json"), 200, 20L)); + + stubExchange(lineItemResponseOrder, idToExecutionParameters); + + // when + final io.restassured.response.Response response = givenResponse(); + + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + "deals/premature/responses/test-auction-in-order-response.json", response, singletonList(RUBICON))); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + } + + @Test + public void openrtb2AuctionWhenAllThreeBidsReturnsInReverseOrderWithMinimalDelay() throws IOException, + JSONException { + // given + awaitForLineItemMetadata(); + + final Queue lineItemResponseOrder = new LinkedList<>(); + lineItemResponseOrder.add("extLineItem3"); + lineItemResponseOrder.add("extLineItem2"); + lineItemResponseOrder.add("extLineItem1"); + + final Map idToExecutionParameters = new HashMap<>(); + idToExecutionParameters.put("extLineItem1", IntegrationTest.BidRequestExecutionParameters.of("extLineItem1", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-1.json"), 200, 20L)); + idToExecutionParameters.put("extLineItem2", IntegrationTest.BidRequestExecutionParameters.of("extLineItem2", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-2.json"), 200, 20L)); + idToExecutionParameters.put("extLineItem3", IntegrationTest.BidRequestExecutionParameters.of("extLineItem3", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-3.json"), 200, 0L)); + + stubExchange(lineItemResponseOrder, idToExecutionParameters); + + // when + final io.restassured.response.Response response = givenResponse(); + + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + "deals/premature/responses/test-auction-in-reverse-order-response.json", response, + singletonList(RUBICON))); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + } + + @Test + public void openrtb2AuctionWhenAllThreeBidsReturnsInOrderWithHighDelay() throws IOException, JSONException { + // given + awaitForLineItemMetadata(); + + final Queue lineItemResponseOrder = new LinkedList<>(); + lineItemResponseOrder.add("extLineItem1"); + lineItemResponseOrder.add("extLineItem2"); + lineItemResponseOrder.add("extLineItem3"); + + final Map idToExecutionParameters = new HashMap<>(); + idToExecutionParameters.put("extLineItem1", IntegrationTest.BidRequestExecutionParameters.of("extLineItem1", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-1.json"), 200, 0L)); + idToExecutionParameters.put("extLineItem2", IntegrationTest.BidRequestExecutionParameters.of("extLineItem2", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-2.json"), 200, 200L)); + idToExecutionParameters.put("extLineItem3", IntegrationTest.BidRequestExecutionParameters.of("extLineItem3", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-3.json"), 200, 200L)); + + stubExchange(lineItemResponseOrder, idToExecutionParameters); + + // when + final io.restassured.response.Response response = givenResponse(); + + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + "deals/premature/responses/test-auction-in-order-response.json", response, singletonList(RUBICON))); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + } + + @Test + public void openrtb2AuctionWhenAllThreeBidsReturnsInReverseOrderWithHighDelay() throws IOException, JSONException { + // given + awaitForLineItemMetadata(); + + final Queue lineItemResponseOrder = new LinkedList<>(); + lineItemResponseOrder.add("extLineItem3"); + lineItemResponseOrder.add("extLineItem2"); + lineItemResponseOrder.add("extLineItem1"); + + final Map idToExecutionParameters = new HashMap<>(); + idToExecutionParameters.put("extLineItem1", IntegrationTest.BidRequestExecutionParameters.of("extLineItem1", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-1.json"), 200, 200L)); + idToExecutionParameters.put("extLineItem2", IntegrationTest.BidRequestExecutionParameters.of("extLineItem2", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-2.json"), 200, 200L)); + idToExecutionParameters.put("extLineItem3", IntegrationTest.BidRequestExecutionParameters.of("extLineItem3", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-3.json"), 200, 0L)); + + stubExchange(lineItemResponseOrder, idToExecutionParameters); + + // when + final io.restassured.response.Response response = givenResponse(); + + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + "deals/premature/responses/test-auction-in-reverse-order-response.json", response, + singletonList(RUBICON))); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + } + + @Test + public void openrtb2AuctionWhenOnlyFirstBidComesBack() throws IOException, JSONException { + // given + awaitForLineItemMetadata(); + + final Queue lineItemResponseOrder = new LinkedList<>(); + lineItemResponseOrder.add("extLineItem1"); + lineItemResponseOrder.add("extLineItem2"); + lineItemResponseOrder.add("extLineItem3"); + + final Map idToExecutionParameters = new HashMap<>(); + idToExecutionParameters.put("extLineItem1", IntegrationTest.BidRequestExecutionParameters.of("extLineItem1", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-1.json"), 200, 0L)); + idToExecutionParameters.put("extLineItem2", IntegrationTest.BidRequestExecutionParameters.of("extLineItem2", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-no-bid-response-2.json"), 200, 20L)); + idToExecutionParameters.put("extLineItem3", IntegrationTest.BidRequestExecutionParameters.of("extLineItem3", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-no-bid-response-3.json"), 200, 20L)); + + stubExchange(lineItemResponseOrder, idToExecutionParameters); + + // when + final io.restassured.response.Response response = givenResponse(); + + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + "deals/premature/responses/test-auction-first-bid-only-response.json", response, + singletonList(RUBICON))); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + } + + @Test + public void openrtb2AuctionWhenOnlySecondBidComesBack() throws IOException, JSONException { + // given + awaitForLineItemMetadata(); + + final Queue lineItemResponseOrder = new LinkedList<>(); + lineItemResponseOrder.add("extLineItem1"); + lineItemResponseOrder.add("extLineItem2"); + lineItemResponseOrder.add("extLineItem3"); + + final Map idToExecutionParameters = new HashMap<>(); + idToExecutionParameters.put("extLineItem1", IntegrationTest.BidRequestExecutionParameters.of("extLineItem1", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-no-bid-response-1.json"), 200, 0L)); + idToExecutionParameters.put("extLineItem2", IntegrationTest.BidRequestExecutionParameters.of("extLineItem2", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-2.json"), 200, 20L)); + idToExecutionParameters.put("extLineItem3", IntegrationTest.BidRequestExecutionParameters.of("extLineItem3", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-no-bid-response-3.json"), 200, 20L)); + + stubExchange(lineItemResponseOrder, idToExecutionParameters); + + // when + final io.restassured.response.Response response = givenResponse(); + + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + "deals/premature/responses/test-auction-second-bid-only-response.json", response, + singletonList(RUBICON))); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + } + + @Test + public void openrtb2AuctionWhenOnlyThirdBidComesBack() throws IOException, JSONException { + // given + awaitForLineItemMetadata(); + + final Queue lineItemResponseOrder = new LinkedList<>(); + lineItemResponseOrder.add("extLineItem1"); + lineItemResponseOrder.add("extLineItem2"); + lineItemResponseOrder.add("extLineItem3"); + + final Map idToExecutionParameters = new HashMap<>(); + idToExecutionParameters.put("extLineItem1", IntegrationTest.BidRequestExecutionParameters.of("extLineItem1", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-no-bid-response-1.json"), 200, 0L)); + idToExecutionParameters.put("extLineItem2", IntegrationTest.BidRequestExecutionParameters.of("extLineItem2", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-no-bid-response-2.json"), 200, 20L)); + idToExecutionParameters.put("extLineItem3", IntegrationTest.BidRequestExecutionParameters.of("extLineItem3", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-3.json"), 200, 20L)); + + stubExchange(lineItemResponseOrder, idToExecutionParameters); + + // when + final io.restassured.response.Response response = givenResponse(); + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + "deals/premature/responses/test-auction-third-bid-only-response.json", response, + singletonList(RUBICON))); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + } + + @Test + public void openrtb2AuctionWhenFirstAndSecondBidsComesBackReverseWithHighDelay() throws IOException, JSONException { + // given + awaitForLineItemMetadata(); + + final Queue lineItemResponseOrder = new LinkedList<>(); + lineItemResponseOrder.add("extLineItem2"); + lineItemResponseOrder.add("extLineItem1"); + lineItemResponseOrder.add("extLineItem3"); + + final Map idToExecutionParameters = new HashMap<>(); + idToExecutionParameters.put("extLineItem1", IntegrationTest.BidRequestExecutionParameters.of("extLineItem1", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-1.json"), 200, 200L)); + idToExecutionParameters.put("extLineItem2", IntegrationTest.BidRequestExecutionParameters.of("extLineItem2", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-2.json"), 200, 0L)); + idToExecutionParameters.put("extLineItem3", IntegrationTest.BidRequestExecutionParameters.of("extLineItem3", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-no-bid-response-3.json"), 200, 200L)); + + stubExchange(lineItemResponseOrder, idToExecutionParameters); + + // when + final io.restassured.response.Response response = givenResponse(); + + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + "deals/premature/responses/test-auction-first-and-second-response.json", response, + singletonList(RUBICON))); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + } + + @Test + public void openrtb2AuctionWhenSecondAndThirdBidsComesBackReverseWithHighDelay() throws IOException, JSONException { + // given + awaitForLineItemMetadata(); + + final Queue lineItemResponseOrder = new LinkedList<>(); + lineItemResponseOrder.add("extLineItem3"); + lineItemResponseOrder.add("extLineItem2"); + lineItemResponseOrder.add("extLineItem1"); + + final Map idToExecutionParameters = new HashMap<>(); + idToExecutionParameters.put("extLineItem1", IntegrationTest.BidRequestExecutionParameters.of("extLineItem1", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-no-bid-response-1.json"), 200, 200L)); + idToExecutionParameters.put("extLineItem2", IntegrationTest.BidRequestExecutionParameters.of("extLineItem2", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-2.json"), 200, 200L)); + idToExecutionParameters.put("extLineItem3", IntegrationTest.BidRequestExecutionParameters.of("extLineItem3", + IntegrationTest.jsonFrom("deals/premature/test-rubicon-bid-response-3.json"), 200, 0L)); + + stubExchange(lineItemResponseOrder, idToExecutionParameters); + + // when + final io.restassured.response.Response response = givenResponse(); + + final String expectedAuctionResponse = withTemporalFields(IntegrationTest.openrtbAuctionResponseFrom( + "deals/premature/responses/test-auction-third-and-second-response.json", response, + singletonList(RUBICON))); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbDeepDebugTimeComparator()); + } + + private io.restassured.response.Response givenResponse() throws IOException { + return given(SPEC) + .header("Referer", "http://www.example.com") + .header("User-Agent", "userAgent") + .header("X-Forwarded-For", "185.199.110.153") + // this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT"}} + .cookie("uids", "eyJ1aWRzIjp7InJ1Ymljb24iOiJKNVZMQ1dRUC0yNi1DV0ZUIn19") + .body(IntegrationTest.jsonFrom("deals/premature/test-auction-request.json")) + .post("/openrtb2/auction"); + } + + private void stubExchange(Queue lineItemResponseOrder, + Map idToExecutionParameters) { + WIRE_MOCK_RULE.stubFor(post(urlMatching("/rubicon-exchange.*")) + .willReturn(aResponse() + .withTransformers("response-order-transformer") + .withTransformerParameter(IntegrationTest.LINE_ITEM_RESPONSE_ORDER, lineItemResponseOrder) + .withTransformerParameter(IntegrationTest.ID_TO_EXECUTION_PARAMETERS, + idToExecutionParameters))); + } + + private void awaitForLineItemMetadata() { + await().atMost(10, TimeUnit.SECONDS).pollInterval(100, TimeUnit.MILLISECONDS) + .until(() -> lineItemService.accountHasDeals("2001", ZonedDateTime.now(clock))); + } + + private static String plannerResponseFrom(String templatePath) throws IOException { + final ZonedDateTime now = ZonedDateTime.now().withFixedOffsetZone(); + + return IntegrationTest.jsonFrom(templatePath) + .replaceAll("\\{\\{ now }}", now.toString()) + .replaceAll("\\{\\{ lineItem.startTime }}", now.minusDays(5).toString()) + .replaceAll("\\{\\{ lineItem.endTime }}", now.plusDays(5).toString()) + .replaceAll("\\{\\{ plan.startTime }}", now.minusHours(1).toString()) + .replaceAll("\\{\\{ plan.endTime }}", now.plusHours(1).toString()); + } + + private static CustomComparator openrtbDeepDebugTimeComparator() { + final ValueMatcher timeValueMatcher = (actual, expected) -> { + try { + return mapper.readValue("\"" + actual.toString() + "\"", ZonedDateTime.class) != null; + } catch (IOException e) { + return false; + } + }; + + final ArrayValueMatcher arrayValueMatcher = new ArrayValueMatcher<>(new CustomComparator( + JSONCompareMode.NON_EXTENSIBLE, + new Customization("ext.debug.trace.deals[*].time", timeValueMatcher))); + + final List arrayValueMatchers = IntStream.range(1, 5) + .mapToObj(i -> new Customization("ext.debug.trace.lineitems.lineItem" + i, + new ArrayValueMatcher<>(new CustomComparator( + JSONCompareMode.NON_EXTENSIBLE, + new Customization("ext.debug.trace.lineitems.lineItem" + i + "[*].time", + timeValueMatcher))))) + .collect(Collectors.toList()); + + arrayValueMatchers.add(new Customization("ext.debug.trace.deals", arrayValueMatcher)); + arrayValueMatchers.add(new Customization("**.requestheaders.x-prebid", (o1, o2) -> true)); + + return new CustomComparator(JSONCompareMode.NON_EXTENSIBLE, + arrayValueMatchers.toArray(new Customization[0])); + } + + private String withTemporalFields(String auctionResponse) { + final ZonedDateTime dateTime = ZonedDateTime.now(clock); + + return auctionResponse + .replaceAll("\"?\\{\\{ userdow }}\"?", Integer.toString( + dateTime.getDayOfWeek().get(WeekFields.SUNDAY_START.dayOfWeek()))) + .replaceAll("\"?\\{\\{ userhour }}\"?", Integer.toString(dateTime.getHour())); + } +} diff --git a/src/test/java/org/prebid/server/it/PubmaticTest.java b/src/test/java/org/prebid/server/it/PubmaticTest.java index 390cffc1da8..98e9077fe79 100644 --- a/src/test/java/org/prebid/server/it/PubmaticTest.java +++ b/src/test/java/org/prebid/server/it/PubmaticTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,86 +13,24 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) public class PubmaticTest extends IntegrationTest { - private static final String PUBMATIC = "pubmatic"; - @Test public void openrtb2AuctionShouldRespondWithBidsFromPubmatic() throws IOException, JSONException { // given - // pubmatic bid response for imp 9 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/pubmatic-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/pubmatic/test-pubmatic-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/pubmatic/test-pubmatic-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/pubmatic/test-cache-pubmatic-request.json"), true, - false)) - .willReturn(aResponse() - .withTransformers("cache-response-transformer") - .withTransformerParameter("matcherName", - "openrtb2/pubmatic/test-cache-matcher-pubmatic.json"))); - - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"pubmatic":"PM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InB1Ym1hdGljIjoiUE0tVUlEIn19") - .body(jsonFrom("openrtb2/pubmatic/test-auction-pubmatic-request.json")) - .post("/openrtb2/auction"); - - // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/pubmatic/test-auction-pubmatic-response.json", - response, singletonList(PUBMATIC)); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); - } - - @Test - public void auctionShouldRespondWithBidsFromPubmatic() throws IOException { - // given - // pubmatic bid response for ad unit 9 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/pubmatic-exchange")) - .withRequestBody(equalToJson(jsonFrom("auction/pubmatic/test-pubmatic-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/pubmatic/test-pubmatic-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("auction/pubmatic/test-cache-pubmatic-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/pubmatic/test-cache-pubmatic-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/pubmatic/test-pubmatic-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/pubmatic/test-pubmatic-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - //this uids cookie value stands for {"uids":{"pubmatic":"PM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InB1Ym1hdGljIjoiUE0tVUlEIn19") - .queryParam("debug", "1") - .body(jsonFrom("auction/pubmatic/test-auction-pubmatic-request.json")) - .post("/auction"); + final Response response = responseFor("openrtb2/pubmatic/test-auction-pubmatic-request.json", + Endpoint.openrtb2_auction); // then - assertThat(response.header("Cache-Control")).isEqualTo("no-cache, no-store, must-revalidate"); - assertThat(response.header("Pragma")).isEqualTo("no-cache"); - assertThat(response.header("Expires")).isEqualTo("0"); - assertThat(response.header("Access-Control-Allow-Credentials")).isEqualTo("true"); - assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("http://www.example.com"); - - final String expectedAuctionResponse = legacyAuctionResponseFrom( - "auction/pubmatic/test-auction-pubmatic-response.json", - response, singletonList(PUBMATIC)); - assertThat(response.asString()).isEqualTo(expectedAuctionResponse); + assertJsonEquals("openrtb2/pubmatic/test-auction-pubmatic-response.json", response, + singletonList("pubmatic")); } } diff --git a/src/test/java/org/prebid/server/it/PubnativeTest.java b/src/test/java/org/prebid/server/it/PubnativeTest.java index 2f1b8a3b1aa..cfa7af78bce 100644 --- a/src/test/java/org/prebid/server/it/PubnativeTest.java +++ b/src/test/java/org/prebid/server/it/PubnativeTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,48 +21,16 @@ public class PubnativeTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromThePubnative() throws IOException, JSONException { // given - // Pubnative bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/pubnative-exchange")) - .withQueryParam("zoneid", equalTo("1")) - .withQueryParam("apptoken", equalTo("4fd53a12b78af4b39835de9e449c3082")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/pubnative/test-pubnative-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/pubnative/test-pubnative-bid-response-1.json")))); - - // Pubnative bid response for imp 002 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/pubnative-exchange")) - .withQueryParam("zoneid", equalTo("2")) - .withQueryParam("apptoken", equalTo("4fd53a12b78af4b39835de9e449c")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/pubnative/test-pubnative-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/pubnative/test-pubnative-bid-response-2.json")))); - - // Pubnative bid response for imp 003 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/pubnative-exchange")) - .withQueryParam("zoneid", equalTo("3")) - .withQueryParam("apptoken", equalTo("4fd53a12b78af4b39835de9e")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/pubnative/test-pubnative-bid-request-3.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/pubnative/test-pubnative-bid-response-3.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/pubnative/test-cache-pubnative-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/pubnative/test-cache-pubnative-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/pubnative/test-pubnative-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/pubnative/test-pubnative-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"pubnative":"PN-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InB1Ym5hdGl2ZSI6IlBOLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/pubnative/test-auction-pubnative-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/pubnative/test-auction-pubnative-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/pubnative/test-auction-pubnative-response.json", - response, singletonList("pubnative")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/pubnative/test-auction-pubnative-response.json", response, + singletonList("pubnative")); } } diff --git a/src/test/java/org/prebid/server/it/PulsepointTest.java b/src/test/java/org/prebid/server/it/PulsepointTest.java index 99baee3c5c6..5e813f50744 100644 --- a/src/test/java/org/prebid/server/it/PulsepointTest.java +++ b/src/test/java/org/prebid/server/it/PulsepointTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,82 +13,24 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) public class PulsepointTest extends IntegrationTest { - private static final String PULSEPOINT = "pulsepoint"; - @Test public void openrtb2AuctionShouldRespondWithBidsFromPulsepoint() throws IOException, JSONException { // given - // pulsepoint bid response for imp 8 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/pulsepoint-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/pulsepoint/test-pulsepoint-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/pulsepoint/test-pulsepoint-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/pulsepoint/test-cache-pulsepoint-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/pulsepoint/test-cache-pulsepoint-response.json")))); - - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"pulsepoint":"PP-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InB1bHNlcG9pbnQiOiJQUC1VSUQifX0=") - .body(jsonFrom("openrtb2/pulsepoint/test-auction-pulsepoint-request.json")) - .post("/openrtb2/auction"); - - // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/pulsepoint/test-auction-pulsepoint-response.json", - response, singletonList(PULSEPOINT)); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); - } - - @Test - public void auctionShouldRespondWithBidsFromPulsepoint() throws IOException { - // given - // pulsepoint bid response for ad unit 6 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/pulsepoint-exchange")) - .withRequestBody(equalToJson(jsonFrom("auction/pulsepoint/test-pulsepoint-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/pulsepoint/test-pulsepoint-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("auction/pulsepoint/test-cache-pulsepoint-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/pulsepoint/test-cache-pulsepoint-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/pulsepoint/test-pulsepoint-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/pulsepoint/test-pulsepoint-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - //this uids cookie value stands for {"uids":{"pulsepoint":"PP-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InB1bHNlcG9pbnQiOiJQUC1VSUQifX0=") - .queryParam("debug", "1") - .body(jsonFrom("auction/pulsepoint/test-auction-pulsepoint-request.json")) - .post("/auction"); + final Response response = responseFor("openrtb2/pulsepoint/test-auction-pulsepoint-request.json", + Endpoint.openrtb2_auction); // then - assertThat(response.header("Cache-Control")).isEqualTo("no-cache, no-store, must-revalidate"); - assertThat(response.header("Pragma")).isEqualTo("no-cache"); - assertThat(response.header("Expires")).isEqualTo("0"); - assertThat(response.header("Access-Control-Allow-Credentials")).isEqualTo("true"); - assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("http://www.example.com"); - - final String expectedAuctionResponse = legacyAuctionResponseFrom( - "auction/pulsepoint/test-auction-pulsepoint-response.json", - response, singletonList(PULSEPOINT)); - assertThat(response.asString()).isEqualTo(expectedAuctionResponse); + assertJsonEquals("openrtb2/pulsepoint/test-auction-pulsepoint-response.json", response, + singletonList("pulsepoint")); } } diff --git a/src/test/java/org/prebid/server/it/RevcontentTest.java b/src/test/java/org/prebid/server/it/RevcontentTest.java new file mode 100644 index 00000000000..c281011f6de --- /dev/null +++ b/src/test/java/org/prebid/server/it/RevcontentTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class RevcontentTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheRevcontentTest() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/revcontent-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/revcontent/test-revcontent-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/revcontent/test-revcontent-bid-response.json")))); + // when + final Response response = responseFor("openrtb2/revcontent/test-auction-revcontent-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/revcontent/test-auction-revcontent-response.json", response, + singletonList("revcontent")); + } +} diff --git a/src/test/java/org/prebid/server/it/RhythmoneTest.java b/src/test/java/org/prebid/server/it/RhythmoneTest.java index e983632ae1e..b5dcfb54d23 100644 --- a/src/test/java/org/prebid/server/it/RhythmoneTest.java +++ b/src/test/java/org/prebid/server/it/RhythmoneTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,35 +20,17 @@ public class RhythmoneTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromRhythmone() throws IOException, JSONException { - // given - // rhythmone bid response for imp002 + // given002 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rhythmone-exchange/72721/0/mvo")) - .withQueryParam("z", equalTo("1r")) - .withQueryParam("s2s", equalTo("true")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/rhythmone/test-rhythmone-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/rhythmone/test-rhythmone-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/rhythmone/test-cache-rhythmone-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/rhythmone/test-cache-rhythmone-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/rhythmone/test-rhythmone-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/rhythmone/test-rhythmone-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"rhythmone":"RO-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InJoeXRobW9uZSI6IlJPLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/rhythmone/test-auction-rhythmone-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/rhythmone/test-auction-rhythmone-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/rhythmone/test-auction-rhythmone-response.json", - response, singletonList("rhythmone")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/rhythmone/test-auction-rhythmone-response.json", response, + singletonList("rhythmone")); } } diff --git a/src/test/java/org/prebid/server/it/RtbhouseTest.java b/src/test/java/org/prebid/server/it/RtbhouseTest.java index 2b90218d1b4..96792060359 100644 --- a/src/test/java/org/prebid/server/it/RtbhouseTest.java +++ b/src/test/java/org/prebid/server/it/RtbhouseTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,30 +22,15 @@ public class RtbhouseTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromTheRtbHouse() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rtbhouse-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/rtbhouse/test-rtbhouse-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/rtbhouse/test-rtbhouse-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/rtbhouse/test-cache-rtbhouse-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/rtbhouse/test-cache-rtbhouse-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/rtbhouse/test-rtbhouse-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/rtbhouse/test-rtbhouse-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"rtbhouse":"RTBH-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InJ0YmhvdXNlIjoiUlRCSC1VSUQifX0=") - .body(jsonFrom("openrtb2/rtbhouse/test-auction-rtbhouse-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/rtbhouse/test-auction-rtbhouse-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/rtbhouse/test-auction-rtbhouse-response.json", - response, singletonList("rtbhouse")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/rtbhouse/test-auction-rtbhouse-response.json", response, + singletonList("rtbhouse")); } } diff --git a/src/test/java/org/prebid/server/it/RubiconTest.java b/src/test/java/org/prebid/server/it/RubiconTest.java new file mode 100644 index 00000000000..b8632f6e568 --- /dev/null +++ b/src/test/java/org/prebid/server/it/RubiconTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class RubiconTest extends IntegrationTest { + + @Test + public void testOpenrtb2AuctionCoreFunctionality() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/rubicon/test-rubicon-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/rubicon/test-rubicon-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/rubicon/test-auction-rubicon-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/rubicon/test-auction-rubicon-response.json", response, singletonList("rubicon")); + } +} diff --git a/src/test/java/org/prebid/server/it/SaLunamediaTest.java b/src/test/java/org/prebid/server/it/SaLunamediaTest.java new file mode 100644 index 00000000000..1a48efb3c42 --- /dev/null +++ b/src/test/java/org/prebid/server/it/SaLunamediaTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class SaLunamediaTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromSaLunamedia() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/salunamedia-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/salunamedia/test-salunamedia-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/salunamedia/test-salunamedia-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/salunamedia/test-auction-salunamedia-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/salunamedia/test-auction-salunamedia-response.json", response, + singletonList("sa_lunamedia")); + } +} + diff --git a/src/test/java/org/prebid/server/it/SharethroughTest.java b/src/test/java/org/prebid/server/it/SharethroughTest.java index 9c5774437a6..63dad901a30 100644 --- a/src/test/java/org/prebid/server/it/SharethroughTest.java +++ b/src/test/java/org/prebid/server/it/SharethroughTest.java @@ -4,85 +4,36 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.prebid.server.util.HttpUtil; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; +import org.skyscreamer.jsonassert.Customization; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) public class SharethroughTest extends IntegrationTest { - private static final Date TEST_TIME = new Date(1604455678999L); // hardcoded value in bidder - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); - private static final String TEST_FORMATTED_TIME = DATE_FORMAT.format(TEST_TIME); - private static final String DEADLINE_FORMATTED_TIME = DATE_FORMAT.format(new Date(TEST_TIME.getTime() + 3000L)); - @Test public void openrtb2AuctionShouldRespondWithBidsFromSharethrough() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/sharethrough-exchange")) - .withQueryParam("placement_key", equalTo("abc123")) - .withQueryParam("bidId", equalTo("bid")) - .withQueryParam("consent_required", equalTo("false")) - .withQueryParam("consent_string", equalTo("consentValue")) - .withQueryParam("us_privacy", equalTo("1NYN")) - .withQueryParam("instant_play_capable", equalTo("true")) - .withQueryParam("stayInIframe", equalTo("true")) - .withQueryParam("height", equalTo("50")) - .withQueryParam("width", equalTo("50")) - .withQueryParam("supplyId", equalTo("FGMrCMMc")) - .withQueryParam("adRequestAt", equalTo(TEST_FORMATTED_TIME)) - .withQueryParam("ttduid", equalTo("id")) - .withQueryParam("stxuid", equalTo("STR-UID")) - .withQueryParam("strVersion", equalTo("8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("User-Agent", equalTo("Android Chrome/60")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Origin", equalTo("http://www.example.com")) - .withHeader("Referer", equalTo("http://www.example.com")) - .withRequestBody(equalTo(jsonFrom("openrtb2/sharethrough/test-sharethrough-request.json") - .replace("{{ DEADLINE_FORMATTED_TIME }}", DEADLINE_FORMATTED_TIME))) - .willReturn( - aResponse().withBody(jsonFrom("openrtb2/sharethrough/test-sharethrough-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/sharethrough/test-cache-sharethrough-request.json"))) + .withRequestBody(equalToJson(jsonFrom("openrtb2/sharethrough/test-sharethrough-request.json"))) .willReturn( - aResponse().withBody(jsonFrom("openrtb2/sharethrough/test-cache-sharethrough-response.json")))); + aResponse().withBody(jsonFrom("openrtb2/sharethrough/test-sharethrough-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"sharethrough":"STR-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InNoYXJldGhyb3VnaCI6IlNUUi1VSUQifX0=") - .body(jsonFrom("openrtb2/sharethrough/test-auction-sharethrough-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/sharethrough/test-auction-sharethrough-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/sharethrough/test-auction-sharethrough-response.json", - response, singletonList("sharethrough")) - .replace("{{ TEST_FORMATTED_TIME }}", HttpUtil.encodeUrl(TEST_FORMATTED_TIME)) - .replace("{{ DEADLINE_FORMATTED_TIME }}", DEADLINE_FORMATTED_TIME); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/sharethrough/test-auction-sharethrough-response.json", response, + singletonList("sharethrough"), + new Customization("seatbid[group=0].bid[adid=arid].adm", (o1, o2) -> true)); } } diff --git a/src/test/java/org/prebid/server/it/SilvermobTest.java b/src/test/java/org/prebid/server/it/SilvermobTest.java new file mode 100644 index 00000000000..de36c0b8097 --- /dev/null +++ b/src/test/java/org/prebid/server/it/SilvermobTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class SilvermobTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromSilvermob() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/silvermob-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/silvermob/test-silvermob-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/silvermob/test-silvermob-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/silvermob/test-auction-silvermob-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/silvermob/test-auction-silvermob-response.json", response, + singletonList("silvermob")); + } +} diff --git a/src/test/java/org/prebid/server/it/SmaatoTest.java b/src/test/java/org/prebid/server/it/SmaatoTest.java index 0ed182b2aac..3ac89339430 100644 --- a/src/test/java/org/prebid/server/it/SmaatoTest.java +++ b/src/test/java/org/prebid/server/it/SmaatoTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,37 +21,17 @@ public class SmaatoTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSmaato() throws IOException, JSONException { // given - // SmaatoBidder bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smaato-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) .withRequestBody(equalToJson(jsonFrom("openrtb2/smaato/test-smaato-bid-request.json"))) .willReturn(aResponse() .withHeader("X-SMT-ADTYPE", "Img") .withBody(jsonFrom("openrtb2/smaato/test-smaato-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/smaato/test-cache-smaato-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/smaato/test-cache-smaato-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"smaato":"SM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InNtYWF0byI6IlNNLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/smaato/test-auction-smaato-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/smaato/test-auction-smaato-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/smaato/test-auction-smaato-response.json", - response, singletonList("smaato")); - - final String actualStr = response.asString(); - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/smaato/test-auction-smaato-response.json", response, singletonList("smaato")); } } diff --git a/src/test/java/org/prebid/server/it/SmartadserverTest.java b/src/test/java/org/prebid/server/it/SmartadserverTest.java index dcfc58f6b23..2b5ca36861d 100644 --- a/src/test/java/org/prebid/server/it/SmartadserverTest.java +++ b/src/test/java/org/prebid/server/it/SmartadserverTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,34 +21,17 @@ public class SmartadserverTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSmartadserver() throws IOException, JSONException { // given - // Smartadserver bid response for imp 001 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smartadserver-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/smartadserver/test-smartadserver-bid-request-1.json"))) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smartadserver-exchange/api/bid")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/smartadserver/test-smartadserver-bid-request.json"))) .willReturn(aResponse() - .withBody(jsonFrom("openrtb2/smartadserver/test-smartadserver-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/smartadserver/test-cache-smartadserver-request.json"))) - .willReturn(aResponse() - .withBody(jsonFrom("openrtb2/smartadserver/test-cache-smartadserver-response.json")))); + .withBody(jsonFrom("openrtb2/smartadserver/test-smartadserver-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"smartadserver":"SA-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InNtYXJ0YWRzZXJ2ZXIiOiJTQS1VSUQifX0=") - .body(jsonFrom("openrtb2/smartadserver/test-auction-smartadserver-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/smartadserver/test-auction-smartadserver-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/smartadserver/test-auction-smartadserver-response.json", - response, singletonList("smartadserver")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/smartadserver/test-auction-smartadserver-response.json", response, + singletonList("smartadserver")); } } diff --git a/src/test/java/org/prebid/server/it/SmartrtbTest.java b/src/test/java/org/prebid/server/it/SmartrtbTest.java index 1c3a29d0b04..dcb1a349360 100644 --- a/src/test/java/org/prebid/server/it/SmartrtbTest.java +++ b/src/test/java/org/prebid/server/it/SmartrtbTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,35 +21,16 @@ public class SmartrtbTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSmartrtb() throws IOException, JSONException { // given - // Smartrtb bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smartrtb-exchange/1234")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withHeader("x-openrtb-version", equalTo("2.5")) .withRequestBody(equalToJson(jsonFrom("openrtb2/smartrtb/test-smartrtb-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/smartrtb/test-smartrtb-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/smartrtb/test-cache-smartrtb-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/smartrtb/test-cache-smartrtb-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7ImdhbW9zaGkiOiJHTS1VSUQifX0=") - .body(jsonFrom("openrtb2/smartrtb/test-auction-smartrtb-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/smartrtb/test-auction-smartrtb-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/smartrtb/test-auction-smartrtb-response.json", - response, singletonList("smartrtb")); - - String actualStr = response.asString(); - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/smartrtb/test-auction-smartrtb-response.json", response, + singletonList("smartrtb")); } } diff --git a/src/test/java/org/prebid/server/it/SmartyAdsTest.java b/src/test/java/org/prebid/server/it/SmartyAdsTest.java new file mode 100644 index 00000000000..17406dbfc33 --- /dev/null +++ b/src/test/java/org/prebid/server/it/SmartyAdsTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class SmartyAdsTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromSmartyAds() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smartyads-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/smartyads/test-smartyads-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/smartyads/test-smartyads-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/smartyads/test-auction-smartyads-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/smartyads/test-auction-smartyads-response.json", response, + singletonList("smartyads")); + } +} diff --git a/src/test/java/org/prebid/server/it/SmileWantedTest.java b/src/test/java/org/prebid/server/it/SmileWantedTest.java new file mode 100644 index 00000000000..a43803449a3 --- /dev/null +++ b/src/test/java/org/prebid/server/it/SmileWantedTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class SmileWantedTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromSmileWanted() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smilewanted-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/smilewanted/test-smilewanted-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom( + "openrtb2/smilewanted/test-smilewanted-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/smilewanted/test-auction-smilewanted-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/smilewanted/test-auction-smilewanted-response.json", response, + singletonList("smilewanted")); + } +} diff --git a/src/test/java/org/prebid/server/it/SomoaudienceTest.java b/src/test/java/org/prebid/server/it/SomoaudienceTest.java index dff938a72df..9293279afdb 100644 --- a/src/test/java/org/prebid/server/it/SomoaudienceTest.java +++ b/src/test/java/org/prebid/server/it/SomoaudienceTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,73 +21,17 @@ public class SomoaudienceTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSomoaudience() throws IOException, JSONException { // given - // somoaudience bid response for imp 16 & 17 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/somoaudience-exchange")) - .withQueryParam("s", equalTo("placementId02")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Accept-Language", equalTo("en")) - .withHeader("DNT", equalTo("2")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/somoaudience/test-somoaudience-bid-request-1.json"))) + .withRequestBody(equalToJson(jsonFrom("openrtb2/somoaudience/test-somoaudience-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom( - "openrtb2/somoaudience/test-somoaudience-bid-response-1.json")))); - - // somoaudience bid response for imp 18 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/somoaudience-exchange")) - .withQueryParam("s", equalTo("placementId03")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Accept-Language", equalTo("en")) - .withHeader("DNT", equalTo("2")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/somoaudience/test-somoaudience-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom( - "openrtb2/somoaudience/test-somoaudience-bid-response-2.json")))); - - // somoaudience bid response for imp 19 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/somoaudience-exchange")) - .withQueryParam("s", equalTo("placementId04")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("Accept-Language", equalTo("en")) - .withHeader("DNT", equalTo("2")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/somoaudience/test-somoaudience-bid-request-3.json"))) - .willReturn(aResponse().withBody(jsonFrom( - "openrtb2/somoaudience/test-somoaudience-bid-response-3.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/somoaudience/test-cache-somoaudience-request.json"), - true, false)) - .willReturn(aResponse() - .withTransformers("cache-response-transformer") - .withTransformerParameter("matcherName", - "openrtb2/somoaudience/test-cache-matcher-somoaudience.json"))); + "openrtb2/somoaudience/test-somoaudience-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"somoaudience":"SM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InNvbW9hdWRpZW5jZSI6IlNNLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/somoaudience/test-auction-somoaudience-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/somoaudience/test-auction-somoaudience-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/somoaudience/test-auction-somoaudience-response.json", - response, singletonList("somoaudience")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/somoaudience/test-auction-somoaudience-response.json", response, + singletonList("somoaudience")); } } diff --git a/src/test/java/org/prebid/server/it/SonobiTest.java b/src/test/java/org/prebid/server/it/SonobiTest.java index 1201c492fde..fc589cc0eb0 100644 --- a/src/test/java/org/prebid/server/it/SonobiTest.java +++ b/src/test/java/org/prebid/server/it/SonobiTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,37 +21,16 @@ public class SonobiTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSonobi() throws IOException, JSONException { // given - // Sonobi bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/sonobi-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/sonobi/test-sonobi-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/sonobi/test-sonobi-bid-response-1.json")))); - - // Sonobi bid response for imp 002 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/sonobi-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/sonobi/test-sonobi-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/sonobi/test-sonobi-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/sonobi/test-cache-sonobi-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/sonobi/test-cache-sonobi-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/sonobi/test-sonobi-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/sonobi/test-sonobi-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"sonobi":"SB-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InNvbm9iaSI6IlNCLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/sonobi/test-auction-sonobi-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/sonobi/test-auction-sonobi-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/sonobi/test-auction-sonobi-response.json", - response, singletonList("sonobi")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/sonobi/test-auction-sonobi-response.json", response, + singletonList("sonobi")); } } diff --git a/src/test/java/org/prebid/server/it/SovrnTest.java b/src/test/java/org/prebid/server/it/SovrnTest.java index b9b0d11cccc..0626e4de33f 100644 --- a/src/test/java/org/prebid/server/it/SovrnTest.java +++ b/src/test/java/org/prebid/server/it/SovrnTest.java @@ -4,21 +4,16 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) public class SovrnTest extends IntegrationTest { @@ -28,84 +23,15 @@ public class SovrnTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSovrn() throws IOException, JSONException { // given - // sovrn bid response for imp 13 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/sovrn-exchange")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("DNT", equalTo("2")) - .withHeader("Accept-Language", equalTo("en")) - .withCookie("ljt_reader", equalTo("990011")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/sovrn/test-sovrn-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/sovrn/test-sovrn-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/sovrn/test-cache-sovrn-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/sovrn/test-cache-sovrn-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/sovrn/test-sovrn-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/sovrn/test-sovrn-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"sovrn":"990011"}} - .cookie("uids", "eyJ1aWRzIjp7InNvdnJuIjoiOTkwMDExIn19") - .body(jsonFrom("openrtb2/sovrn/test-auction-sovrn-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/sovrn/test-auction-sovrn-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/sovrn/test-auction-sovrn-response.json", - response, singletonList(SOVRN)); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); - } - - @Test - public void auctionShouldRespondWithBidsFromSovrn() throws IOException { - // given - // sovrn bid response for ad unit 11 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/sovrn-exchange")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("DNT", equalTo("10")) - .withHeader("Accept-Language", equalTo("en")) - .withCookie("ljt_reader", equalTo("990011")) - .withRequestBody(equalToJson(jsonFrom("auction/sovrn/test-sovrn-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/sovrn/test-sovrn-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("auction/sovrn/test-cache-sovrn-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("auction/sovrn/test-cache-sovrn-response.json")))); - - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - //this uids cookie value stands for {"uids":{"sovrn":"990011"}} - .cookie("uids", "eyJ1aWRzIjp7InNvdnJuIjoiOTkwMDExIn19") - .queryParam("debug", "1") - .body(jsonFrom("auction/sovrn/test-auction-sovrn-request.json")) - .post("/auction"); - - // then - assertThat(response.header("Cache-Control")).isEqualTo("no-cache, no-store, must-revalidate"); - assertThat(response.header("Pragma")).isEqualTo("no-cache"); - assertThat(response.header("Expires")).isEqualTo("0"); - assertThat(response.header("Access-Control-Allow-Credentials")).isEqualTo("true"); - assertThat(response.header("Access-Control-Allow-Origin")).isEqualTo("http://www.example.com"); - - final String expectedAuctionResponse = legacyAuctionResponseFrom( - "auction/sovrn/test-auction-sovrn-response.json", - response, singletonList(SOVRN)); - assertThat(response.asString()).isEqualTo(expectedAuctionResponse); + assertJsonEquals("openrtb2/sovrn/test-auction-sovrn-response.json", response, singletonList("sovrn")); } } diff --git a/src/test/java/org/prebid/server/it/SynacormediaTest.java b/src/test/java/org/prebid/server/it/SynacormediaTest.java index 28d38262843..ee480eb15dc 100644 --- a/src/test/java/org/prebid/server/it/SynacormediaTest.java +++ b/src/test/java/org/prebid/server/it/SynacormediaTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,36 +21,17 @@ public class SynacormediaTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSynacorMedia() throws IOException, JSONException { // given - // SynacorMedia bid response for imp 001 and imp 002 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/synacormedia-exchange/228")) - .withRequestBody(equalToJson( - jsonFrom("openrtb2/synacormedia/test-synacormedia-bid-request.json"))) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/synacormedia-exchange/seat_id")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/synacormedia/test-synacormedia-bid-request.json"))) .willReturn(aResponse().withBody( jsonFrom("openrtb2/synacormedia/test-synacormedia-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson( - jsonFrom("openrtb2/synacormedia/test-cache-synacormedia-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/synacormedia/test-cache-synacormedia-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"synacormedia":"SCM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InN5bmFjb3JtZWRpYSI6IlNDTS1VSUQifX0=") - .body(jsonFrom("openrtb2/synacormedia/test-auction-synacormedia-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/synacormedia/test-auction-synacormedia-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/synacormedia/test-auction-synacormedia-response.json", - response, singletonList("synacormedia")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/synacormedia/test-auction-synacormedia-response.json", response, + singletonList("synacormedia")); } } diff --git a/src/test/java/org/prebid/server/it/TappxTest.java b/src/test/java/org/prebid/server/it/TappxTest.java index fe12b651953..490ab0f3e4c 100644 --- a/src/test/java/org/prebid/server/it/TappxTest.java +++ b/src/test/java/org/prebid/server/it/TappxTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,38 +21,16 @@ public class TappxTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromTappx() throws IOException, JSONException { // given - // tappx bid response for imp 12 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/tappx-exchange")) - .withQueryParam("tappxkey", equalTo("pub-12345-android-9876")) - .withQueryParam("v", equalTo("1.1")) - .withQueryParam("type_cnn", equalTo("prebid")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("Accept", equalTo("application/json")) .withRequestBody(equalToJson(jsonFrom("openrtb2/tappx/test-tappx-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/tappx/test-tappx-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/tappx/test-cache-tappx-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/tappx/test-cache-tappx-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"tappx":"TX-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InRhcHB4IjoiVFgtVUlEIn19") - .body(jsonFrom("openrtb2/tappx/test-auction-tappx-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/tappx/test-auction-tappx-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/tappx/test-auction-tappx-response.json", - response, singletonList("tappx")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/tappx/test-auction-tappx-response.json", response, singletonList("tappx")); } } diff --git a/src/test/java/org/prebid/server/it/TelariaTest.java b/src/test/java/org/prebid/server/it/TelariaTest.java index 311907f95d9..ef798772b37 100644 --- a/src/test/java/org/prebid/server/it/TelariaTest.java +++ b/src/test/java/org/prebid/server/it/TelariaTest.java @@ -4,19 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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.equalToIgnoreCase; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -25,47 +21,15 @@ public class TelariaTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromTelaria() throws IOException, JSONException { // given - // ValueImpression bid response WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/telaria-exchange/")) - - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("User-Agent", equalTo("userAgent")) - .withHeader("X-Forwarded-For", equalTo("193.168.244.1")) - .withHeader("x-openrtb-version", equalTo("2.5")) - .withHeader("Accept-Encoding", equalTo("gzip")) - .withHeader("Accept-Language", equalTo("en")) - .withHeader("Content-Length", equalTo("677")) - .withHeader("DNT", equalTo("2")) - .withHeader("Host", equalTo("localhost:8090")) - .withRequestBody( - equalToJson(jsonFrom("openrtb2/telaria/test-telaria-bid-request-1.json"))) - .willReturn(aResponse() - .withBody(jsonFrom("openrtb2/telaria/test-telaria-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody( - equalToJson(jsonFrom("openrtb2/telaria/test-cache-telaria-request.json"))) - .willReturn(aResponse() - .withBody(jsonFrom("openrtb2/telaria/test-cache-telaria-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/telaria/test-telaria-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/telaria/test-telaria-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7InRlbGFyaWEiOiJUTC1VSUQifX0=") - .body(jsonFrom("openrtb2/telaria/test-auction-telaria-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/telaria/test-auction-telaria-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/telaria/test-auction-telaria-response.json", - response, singletonList("telaria")); - - String actualStr = response.asString(); - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/telaria/test-auction-telaria-response.json", response, singletonList("telaria")); } } diff --git a/src/test/java/org/prebid/server/it/TripleliftNativeTest.java b/src/test/java/org/prebid/server/it/TripleliftNativeTest.java index 40de0046252..7570c4133a9 100644 --- a/src/test/java/org/prebid/server/it/TripleliftNativeTest.java +++ b/src/test/java/org/prebid/server/it/TripleliftNativeTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -29,30 +27,13 @@ public void openrtb2AuctionShouldRespondWithBidsFromTriplelift() throws IOExcept .willReturn(aResponse().withBody( jsonFrom("openrtb2/tripleliftnative/test-triplelift-native-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson( - jsonFrom("openrtb2/tripleliftnative/test-cache-triplelift-native-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/tripleliftnative/test-cache-triplelift-native-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"triplelift_native":"T"}} - .cookie("uids", "eyJ1aWRzIjp7InRyaXBsZWxpZnRfbmF0aXZlIjoiVCJ9fQ==") - .body(jsonFrom("openrtb2/tripleliftnative/test-auction-triplelift-native-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/tripleliftnative/test-auction-triplelift-native-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/tripleliftnative/test-auction-triplelift-native-response.json", - response, singletonList("triplelift_native")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/tripleliftnative/test-auction-triplelift-native-response.json", response, + singletonList("triplelift_native")); } } diff --git a/src/test/java/org/prebid/server/it/TripleliftTest.java b/src/test/java/org/prebid/server/it/TripleliftTest.java index f10a63e3787..2aa6b646a93 100644 --- a/src/test/java/org/prebid/server/it/TripleliftTest.java +++ b/src/test/java/org/prebid/server/it/TripleliftTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -27,28 +25,13 @@ public void openrtb2AuctionShouldRespondWithBidsFromTriplelift() throws IOExcept .withRequestBody(equalToJson(jsonFrom("openrtb2/triplelift/test-triplelift-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/triplelift/test-triplelift-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/triplelift/test-cache-triplelift-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/triplelift/test-cache-triplelift-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"triplelift":"TL-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InRyaXBsZWxpZnQiOiJUTC1VSUQifX0=") - .body(jsonFrom("openrtb2/triplelift/test-auction-triplelift-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/triplelift/test-auction-triplelift-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/triplelift/test-auction-triplelift-response.json", - response, singletonList("triplelift")); - final String actualStr = response.asString(); - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/triplelift/test-auction-triplelift-response.json", response, + singletonList("triplelift")); } } diff --git a/src/test/java/org/prebid/server/it/TtxTest.java b/src/test/java/org/prebid/server/it/TtxTest.java index a4dbf3754f5..0e9a2d8aa0f 100644 --- a/src/test/java/org/prebid/server/it/TtxTest.java +++ b/src/test/java/org/prebid/server/it/TtxTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,32 +21,15 @@ public class TtxTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFrom33Across() throws IOException, JSONException { // given - // 33Across bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/ttx-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/ttx/test-ttx-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/ttx/test-ttx-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/ttx/test-cache-ttx-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/ttx/test-cache-ttx-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/ttx/test-ttx-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/ttx/test-ttx-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"33across":"TTX-UID"}} - .cookie("uids", "eyJ1aWRzIjp7IjMzYWNyb3NzIjoiVFRYLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/ttx/test-auction-ttx-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/ttx/test-auction-ttx-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/ttx/test-auction-ttx-response.json", - response, singletonList("ttx")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/ttx/test-auction-ttx-response.json", response, singletonList("ttx")); } } diff --git a/src/test/java/org/prebid/server/it/UcfunnelTest.java b/src/test/java/org/prebid/server/it/UcfunnelTest.java index 771016a6557..1d443e9b915 100644 --- a/src/test/java/org/prebid/server/it/UcfunnelTest.java +++ b/src/test/java/org/prebid/server/it/UcfunnelTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,33 +21,15 @@ public class UcfunnelTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromUcfunnel() throws IOException, JSONException { // given - // Ucfunnel bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/ucfunnel-exchange/par-2EDDB423AA24474188B843EE4842932/request")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) .withRequestBody(equalToJson(jsonFrom("openrtb2/ucfunnel/test-ucfunnel-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/ucfunnel/test-ucfunnel-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/ucfunnel/test-cache-ucfunnel-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/ucfunnel/test-cache-ucfunnel-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7InVjZnVubmVsIjoiVUYtVUlEIn19") - .body(jsonFrom("openrtb2/ucfunnel/test-auction-ucfunnel-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/ucfunnel/test-auction-ucfunnel-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/ucfunnel/test-auction-ucfunnel-response.json", - response, singletonList("ucfunnel")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/ucfunnel/test-auction-ucfunnel-response.json", response, singletonList("ucfunnel")); } } diff --git a/src/test/java/org/prebid/server/it/UnicornTest.java b/src/test/java/org/prebid/server/it/UnicornTest.java new file mode 100644 index 00000000000..0e899529668 --- /dev/null +++ b/src/test/java/org/prebid/server/it/UnicornTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +@RunWith(SpringRunner.class) +public class UnicornTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromUnicorn() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/unicorn-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/unicorn/test-unicorn-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/unicorn/test-unicorn-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/unicorn/test-auction-unicorn-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/unicorn/test-auction-unicorn-response.json", response, singletonList("unicorn")); + } +} diff --git a/src/test/java/org/prebid/server/it/UnrulyTest.java b/src/test/java/org/prebid/server/it/UnrulyTest.java index 3598d888414..b4eb3109c93 100644 --- a/src/test/java/org/prebid/server/it/UnrulyTest.java +++ b/src/test/java/org/prebid/server/it/UnrulyTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,37 +21,15 @@ public class UnrulyTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromUnruly() throws IOException, JSONException { // given - // Unruly bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/unruly-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/unruly/test-unruly-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/unruly/test-unruly-bid-response-1.json")))); - - // Unruly bid response for imp 002 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/unruly-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/unruly/test-unruly-bid-request-2.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/unruly/test-unruly-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/unruly/test-cache-unruly-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/unruly/test-cache-unruly-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/unruly/test-unruly-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/unruly/test-unruly-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"unruly":"UR-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InVucnVseSI6IlVSLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/unruly/test-auction-unruly-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/unruly/test-auction-unruly-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/unruly/test-auction-unruly-response.json", - response, singletonList("unruly")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/unruly/test-auction-unruly-response.json", response, singletonList("unruly")); } } diff --git a/src/test/java/org/prebid/server/it/ValueImpressionTest.java b/src/test/java/org/prebid/server/it/ValueImpressionTest.java index 64ecccb46b6..bad146d9de6 100644 --- a/src/test/java/org/prebid/server/it/ValueImpressionTest.java +++ b/src/test/java/org/prebid/server/it/ValueImpressionTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,37 +21,18 @@ public class ValueImpressionTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromValueImpression() throws IOException, JSONException { // given - // ValueImpression bid response WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/valueimpression-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) .withRequestBody( - equalToJson(jsonFrom("openrtb2/valueimpression/test-valueimpression-bid-request-1.json"))) + equalToJson(jsonFrom("openrtb2/valueimpression/test-valueimpression-bid-request.json"))) .willReturn(aResponse() - .withBody(jsonFrom("openrtb2/valueimpression/test-valueimpression-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody( - equalToJson(jsonFrom("openrtb2/valueimpression/test-cache-valueimpression-request.json"))) - .willReturn(aResponse() - .withBody(jsonFrom("openrtb2/valueimpression/test-cache-valueimpression-response.json")))); + .withBody(jsonFrom("openrtb2/valueimpression/test-valueimpression-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7InZhbHVlaW1wcmVzc2lvbiI6IlZJLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/valueimpression/test-auction-valueimpression-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/valueimpression/test-auction-valueimpression-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/valueimpression/test-auction-valueimpression-response.json", - response, singletonList("valueimpression")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/valueimpression/test-auction-valueimpression-response.json", response, + singletonList("valueimpression")); } } diff --git a/src/test/java/org/prebid/server/it/VerizonmediaTest.java b/src/test/java/org/prebid/server/it/VerizonmediaTest.java index 70f3ff099ec..3ecd3073b4e 100644 --- a/src/test/java/org/prebid/server/it/VerizonmediaTest.java +++ b/src/test/java/org/prebid/server/it/VerizonmediaTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,36 +21,18 @@ public class VerizonmediaTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromVerizonmedia() throws IOException, JSONException { // given - // Verizonmedia bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/verizonmedia-exchange")) .withRequestBody(equalToJson( - jsonFrom("openrtb2/verizonmedia/test-verizonmedia-bid-request-1.json"))) + jsonFrom("openrtb2/verizonmedia/test-verizonmedia-bid-request.json"))) .willReturn(aResponse().withBody( - jsonFrom("openrtb2/verizonmedia/test-verizonmedia-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson( - jsonFrom("openrtb2/verizonmedia/test-cache-verizonmedia-request.json"))) - .willReturn(aResponse().withBody( - jsonFrom("openrtb2/verizonmedia/test-cache-verizonmedia-response.json")))); + jsonFrom("openrtb2/verizonmedia/test-verizonmedia-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"verizon":"VM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InZlcml6b24iOiJWTS1VSUQifX0=") - .body(jsonFrom("openrtb2/verizonmedia/test-auction-verizonmedia-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/verizonmedia/test-auction-verizonmedia-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/verizonmedia/test-auction-verizonmedia-response.json", - response, singletonList("verizonmedia")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/verizonmedia/test-auction-verizonmedia-response.json", response, + singletonList("verizonmedia")); } } diff --git a/src/test/java/org/prebid/server/it/VisxTest.java b/src/test/java/org/prebid/server/it/VisxTest.java index 47e3ab58bdd..6c9b10971d3 100644 --- a/src/test/java/org/prebid/server/it/VisxTest.java +++ b/src/test/java/org/prebid/server/it/VisxTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,32 +21,15 @@ public class VisxTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromVisx() throws IOException, JSONException { // given - // VisxTest bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/visx-exchange")) .withRequestBody(equalToJson(jsonFrom("openrtb2/visx/test-visx-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/visx/test-visx-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/visx/test-cache-visx-request.json"), true, false)) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/visx/test-cache-visx-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"visx":"VISX-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InZpc3giOiJWSVNYLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/visx/test-auction-visx-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/visx/test-auction-visx-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/visx/test-auction-visx-response.json", - response, singletonList("visx")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/visx/test-auction-visx-response.json", response, singletonList("visx")); } } diff --git a/src/test/java/org/prebid/server/it/VrtcalTest.java b/src/test/java/org/prebid/server/it/VrtcalTest.java index c4a2ab8975e..8cf8acbe3a8 100644 --- a/src/test/java/org/prebid/server/it/VrtcalTest.java +++ b/src/test/java/org/prebid/server/it/VrtcalTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,32 +21,15 @@ public class VrtcalTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromVrtcal() throws IOException, JSONException { // given - // Vrtcal bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/vrtcal-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/vrtcal/test-vrtcal-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/vrtcal/test-vrtcal-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/vrtcal/test-cache-vrtcal-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/vrtcal/test-cache-vrtcal-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/vrtcal/test-vrtcal-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/vrtcal/test-vrtcal-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"vrtcal":"VR-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InZydGNhbCI6IlZSLVVJRCJ9fQ==") - .body(jsonFrom("openrtb2/vrtcal/test-auction-vrtcal-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/vrtcal/test-auction-vrtcal-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/vrtcal/test-auction-vrtcal-response.json", - response, singletonList("vrtcal")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/vrtcal/test-auction-vrtcal-response.json", response, singletonList("vrtcal")); } } diff --git a/src/test/java/org/prebid/server/it/YeahmobiTest.java b/src/test/java/org/prebid/server/it/YeahmobiTest.java index f130829b4a3..c714bf26ab8 100644 --- a/src/test/java/org/prebid/server/it/YeahmobiTest.java +++ b/src/test/java/org/prebid/server/it/YeahmobiTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,34 +21,16 @@ public class YeahmobiTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromYeahmobi() throws IOException, JSONException { // given - // YeahmobiBidder bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/yeahmobi-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) .withRequestBody(equalToJson(jsonFrom("openrtb2/yeahmobi/test-yeahmobi-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/yeahmobi/test-yeahmobi-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/yeahmobi/test-cache-yeahmobi-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/yeahmobi/test-cache-yeahmobi-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"yeahmobi":"YM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InllYWhtb2JpIjoiWU0tVUlEIn19") - .body(jsonFrom("openrtb2/yeahmobi/test-auction-yeahmobi-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/yeahmobi/test-auction-yeahmobi-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/yeahmobi/test-auction-yeahmobi-response.json", - response, singletonList("yeahmobi")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/yeahmobi/test-auction-yeahmobi-response.json", response, + singletonList("yeahmobi")); } } diff --git a/src/test/java/org/prebid/server/it/YieldlabTest.java b/src/test/java/org/prebid/server/it/YieldlabTest.java index 922a4ba29d7..0316d84cbe9 100644 --- a/src/test/java/org/prebid/server/it/YieldlabTest.java +++ b/src/test/java/org/prebid/server/it/YieldlabTest.java @@ -5,13 +5,16 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; +import org.prebid.server.model.Endpoint; +import org.skyscreamer.jsonassert.ArrayValueMatcher; +import org.skyscreamer.jsonassert.Customization; import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.comparator.CustomComparator; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; -import static io.restassured.RestAssured.given; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -21,51 +24,17 @@ public class YieldlabTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromYieldlab() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(WireMock.get(WireMock.urlPathEqualTo("/yieldlab-exchange/12345")) - .withQueryParam("content", WireMock.equalTo("json")) - .withQueryParam("pvid", WireMock.equalTo("true")) - //harcoded value of ts to pass test - .withQueryParam("ts", WireMock.equalTo("200000")) - .withQueryParam("t", WireMock.equalTo("key1=value1&key2=value2")) - .withQueryParam("ids", WireMock.equalTo("YL-UID")) - .withQueryParam("yl_rtb_ifa", WireMock.equalTo("ifaId")) - .withQueryParam("yl_rtb_devicetype", WireMock.equalTo("4")) - .withQueryParam("yl_rtb_connectiontype", WireMock.equalTo("6")) - .withQueryParam("lat", WireMock.equalTo("51.49949")) - .withQueryParam("lon", WireMock.equalTo("-0.128953")) - .withQueryParam("gdpr", WireMock.equalTo("0")) - .withQueryParam("consent", WireMock.equalTo("consentValue")) - .withHeader("Content-Type", WireMock.equalToIgnoreCase("application/json;charset=utf-8")) - .withHeader("Accept", WireMock.equalTo("application/json")) - .withHeader("User-Agent", WireMock.equalTo("userAgent")) - .withHeader("X-Forwarded-For", WireMock.equalTo("193.168.244.1")) - .withHeader("Cookie", WireMock.equalTo("id=YL-UID")) - .willReturn(WireMock.aResponse() - .withBody(jsonFrom("openrtb2/yieldlab/test-yieldlab-bid-response.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/cache")) - .withRequestBody(WireMock - .equalToJson(jsonFrom("openrtb2/yieldlab/test-cache-yieldlab-request.json"))) - .willReturn(WireMock.aResponse() - .withBody(jsonFrom("openrtb2/yieldlab/test-cache-yieldlab-response.json")))); + .willReturn(aResponse().withBody(jsonFrom("openrtb2/yieldlab/test-yieldlab-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"yieldlab":"YL-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InlpZWxkbGFiIjoiWUwtVUlEIn19") - .body(jsonFrom("openrtb2/yieldlab/test-auction-yieldlab-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/yieldlab/test-auction-yieldlab-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/yieldlab/test-auction-yieldlab-response.json", - response, singletonList("yieldlab")); - - final String actualStr = response.asString(); - JSONAssert.assertEquals(expectedAuctionResponse, actualStr, JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/yieldlab/test-auction-yieldlab-response.json", response, + singletonList("yieldlab"), new Customization("seatbid[*].bid", + new ArrayValueMatcher<>(new CustomComparator(JSONCompareMode.NON_EXTENSIBLE, + new Customization("**.adm", (o1, o2) -> true), + new Customization("**.crid", (o1, o2) -> true))))); } } diff --git a/src/test/java/org/prebid/server/it/YieldmoTest.java b/src/test/java/org/prebid/server/it/YieldmoTest.java index 9718ec40452..ecea5b6b1eb 100644 --- a/src/test/java/org/prebid/server/it/YieldmoTest.java +++ b/src/test/java/org/prebid/server/it/YieldmoTest.java @@ -4,8 +4,7 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; @@ -14,7 +13,6 @@ 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -23,32 +21,15 @@ public class YieldmoTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromYieldmo() throws IOException, JSONException { // given - // Yieldmo bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/yieldmo-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/yieldmo/test-yieldmo-bid-request-1.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/yieldmo/test-yieldmo-bid-response-1.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/yieldmo/test-cache-yieldmo-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/yieldmo/test-cache-yieldmo-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/yieldmo/test-yieldmo-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/yieldmo/test-yieldmo-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"yieldmo":"YM-UID"}} - .cookie("uids", "eyJ1aWRzIjp7InlpZWxkbW8iOiJZTS1VSUQifX0=") - .body(jsonFrom("openrtb2/yieldmo/test-auction-yieldmo-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/yieldmo/test-auction-yieldmo-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/yieldmo/test-auction-yieldmo-response.json", - response, singletonList("yieldmo")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/yieldmo/test-auction-yieldmo-response.json", response, singletonList("yieldmo")); } } diff --git a/src/test/java/org/prebid/server/it/YieldoneTest.java b/src/test/java/org/prebid/server/it/YieldoneTest.java index bdfcb173938..7fd5b0f1e34 100644 --- a/src/test/java/org/prebid/server/it/YieldoneTest.java +++ b/src/test/java/org/prebid/server/it/YieldoneTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,33 +21,15 @@ public class YieldoneTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromYieldone() throws IOException, JSONException { // given - // Yieldone bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/yieldone-exchange")) - .withHeader("Accept", equalTo("application/json")) - .withHeader("Content-Type", equalTo("application/json;charset=UTF-8")) .withRequestBody(equalToJson(jsonFrom("openrtb2/yieldone/test-yieldone-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/yieldone/test-yieldone-bid-response.json")))); - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/yieldone/test-cache-yieldone-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/yieldone/test-cache-yieldone-response.json")))); - // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - .cookie("uids", "eyJ1aWRzIjp7InlpZWxkb25lIjoiWUQtVUlEIn19") - .body(jsonFrom("openrtb2/yieldone/test-auction-yieldone-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/yieldone/test-auction-yieldone-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/yieldone/test-auction-yieldone-response.json", - response, singletonList("yieldone")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/yieldone/test-auction-yieldone-response.json", response, singletonList("yieldone")); } } diff --git a/src/test/java/org/prebid/server/it/ZeroclickfraudTest.java b/src/test/java/org/prebid/server/it/ZeroclickfraudTest.java index dd707b6b9c4..3360e96b9b0 100644 --- a/src/test/java/org/prebid/server/it/ZeroclickfraudTest.java +++ b/src/test/java/org/prebid/server/it/ZeroclickfraudTest.java @@ -4,18 +4,15 @@ import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import org.prebid.server.model.Endpoint; import org.springframework.test.context.junit4.SpringRunner; 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 io.restassured.RestAssured.given; import static java.util.Collections.singletonList; @RunWith(SpringRunner.class) @@ -24,45 +21,18 @@ public class ZeroclickfraudTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromZeroclickfraud() throws IOException, JSONException { // given - // Zeroclickfraud bid response for imp 001 WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/zeroclickfraud-exchange")) - .withQueryParam("sid", equalTo("1")) .withRequestBody( - equalToJson(jsonFrom("openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-1.json"))) + equalToJson(jsonFrom("openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request.json"))) .willReturn(aResponse() - .withBody(jsonFrom("openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response-1.json")))); - - // Zeroclickfraud bid response for imp 002 - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/zeroclickfraud-exchange")) - .withQueryParam("sid", equalTo("2")) - .withRequestBody( - equalToJson(jsonFrom("openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-2.json"))) - .willReturn(aResponse() - .withBody(jsonFrom("openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response-2.json")))); - - // pre-bid cache - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) - .withRequestBody( - equalToJson(jsonFrom("openrtb2/zeroclickfraud/test-cache-zeroclickfraud-request.json"))) - .willReturn(aResponse() - .withBody(jsonFrom("openrtb2/zeroclickfraud/test-cache-zeroclickfraud-response.json")))); + .withBody(jsonFrom("openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response.json")))); // when - final Response response = given(SPEC) - .header("Referer", "http://www.example.com") - .header("X-Forwarded-For", "193.168.244.1") - .header("User-Agent", "userAgent") - .header("Origin", "http://www.example.com") - // this uids cookie value stands for {"uids":{"zeroclickfraud":"ZF-UID"}} - .cookie("uids", "eyJ1aWRzIjp7Inplcm9jbGlja2ZyYXVkIjoiWkYtVUlEIn19") - .body(jsonFrom("openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json")) - .post("/openrtb2/auction"); + final Response response = responseFor("openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json", + Endpoint.openrtb2_auction); // then - final String expectedAuctionResponse = openrtbAuctionResponseFrom( - "openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json", - response, singletonList("zeroclickfraud")); - - JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.NON_EXTENSIBLE); + assertJsonEquals("openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json", response, + singletonList("zeroclickfraud")); } } diff --git a/src/test/java/org/prebid/server/it/hooks/HooksTest.java b/src/test/java/org/prebid/server/it/hooks/HooksTest.java new file mode 100644 index 00000000000..e191f4207d4 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/HooksTest.java @@ -0,0 +1,147 @@ +package org.prebid.server.it.hooks; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.it.IntegrationTest; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.test.context.junit4.SpringRunner; + +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.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.hamcrest.Matchers.empty; + +@RunWith(SpringRunner.class) +public class HooksTest extends IntegrationTest { + + private static final String RUBICON = "rubicon"; + + @Test + public void openrtb2AuctionShouldRunHooksAtEachStage() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .withRequestBody(equalToJson(jsonFrom("hooks/sample-module/test-rubicon-bid-request-1.json"))) + .willReturn(aResponse().withBody(jsonFrom("hooks/sample-module/test-rubicon-bid-response-1.json")))); + + // when + final Response response = given(SPEC) + .queryParam("sample-it-module-update", "headers,body") + .header("User-Agent", "userAgent") + .body(jsonFrom("hooks/sample-module/test-auction-sample-module-request.json")) + .post("/openrtb2/auction"); + + // then + final String expectedAuctionResponse = openrtbAuctionResponseFrom( + "hooks/sample-module/test-auction-sample-module-response.json", response, singletonList(RUBICON)); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT); + } + + @Test + public void openrtb2AuctionShouldBeRejectedByEntrypointHook() throws IOException { + given(SPEC) + .queryParam("sample-it-module-reject", "true") + .header("User-Agent", "userAgent") + .body(jsonFrom("hooks/sample-module/test-auction-sample-module-request.json")) + .post("/openrtb2/auction") + .then() + .statusCode(200) + .body("seatbid", empty()); + } + + @Test + public void openrtb2AuctionShouldBeRejectedByRawAuctionRequestHook() throws IOException { + given(SPEC) + .header("User-Agent", "userAgent") + .body(jsonFrom("hooks/reject/test-auction-raw-auction-request-reject-request.json")) + .post("/openrtb2/auction") + .then() + .statusCode(200) + .body("seatbid", empty()); + } + + @Test + public void openrtb2AuctionShouldBeRejectedByProcessedAuctionRequestHook() throws IOException { + given(SPEC) + .header("User-Agent", "userAgent") + .body(jsonFrom("hooks/reject/test-auction-processed-auction-request-reject-request.json")) + .post("/openrtb2/auction") + .then() + .statusCode(200) + .body("seatbid", empty()); + } + + @Test + public void openrtb2AuctionShouldRejectRubiconBidderByBidderRequestHook() throws IOException, JSONException { + // when + final Response response = given(SPEC) + .header("User-Agent", "userAgent") + .body(jsonFrom("hooks/reject/test-auction-bidder-request-reject-request.json")) + .post("/openrtb2/auction"); + + // then + final String expectedAuctionResponse = openrtbAuctionResponseFrom( + "hooks/reject/test-auction-bidder-request-reject-response.json", response, singletonList(RUBICON)); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT); + + WIRE_MOCK_RULE.verify(0, postRequestedFor(urlPathEqualTo("/rubicon-exchange"))); + } + + @Test + public void openrtb2AuctionShouldRejectRubiconBidderByRawBidderResponseHook() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .willReturn(aResponse().withBody(jsonFrom("hooks/reject/test-rubicon-bid-response-1.json")))); + + // when + final Response response = given(SPEC) + .header("User-Agent", "userAgent") + .body(jsonFrom("hooks/reject/test-auction-raw-bidder-response-reject-request.json")) + .post("/openrtb2/auction"); + + // then + final String expectedAuctionResponse = openrtbAuctionResponseFrom( + "hooks/reject/test-auction-raw-bidder-response-reject-response.json", response, singletonList(RUBICON)); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT); + + WIRE_MOCK_RULE.verify(1, postRequestedFor(urlPathEqualTo("/rubicon-exchange")) + .withRequestBody(equalToJson(jsonFrom("hooks/reject/test-rubicon-bid-request-1.json")))); + } + + @Test + public void openrtb2AuctionShouldRejectRubiconBidderByProcessedBidderResponseHook() + throws IOException, JSONException { + + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .willReturn(aResponse().withBody(jsonFrom("hooks/reject/test-rubicon-bid-response-1.json")))); + + // when + final Response response = given(SPEC) + .header("User-Agent", "userAgent") + .body(jsonFrom("hooks/reject/test-auction-processed-bidder-response-reject-request.json")) + .post("/openrtb2/auction"); + + // then + final String expectedAuctionResponse = openrtbAuctionResponseFrom( + "hooks/reject/test-auction-processed-bidder-response-reject-response.json", + response, + singletonList(RUBICON)); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT); + + WIRE_MOCK_RULE.verify(1, postRequestedFor(urlPathEqualTo("/rubicon-exchange")) + .withRequestBody(equalToJson(jsonFrom("hooks/reject/test-rubicon-bid-request-1.json")))); + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java new file mode 100644 index 00000000000..0fccbdbbdac --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java @@ -0,0 +1,50 @@ +package org.prebid.server.it.hooks; + +import com.iab.openrtb.response.BidResponse; +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionResponseHook; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; + +import java.util.stream.Collectors; + +public class SampleItAuctionResponseHook implements AuctionResponseHook { + + @Override + public Future> call( + AuctionResponsePayload auctionResponsePayload, AuctionInvocationContext invocationContext) { + + final BidResponse originalBidResponse = auctionResponsePayload.bidResponse(); + + final BidResponse updatedBidResponse = updateBidResponse(originalBidResponse); + + return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + AuctionResponsePayloadImpl.of(payload.bidResponse().toBuilder() + .seatbid(updatedBidResponse.getSeatbid()) + .build()))); + } + + @Override + public String code() { + return "auction-response"; + } + + private BidResponse updateBidResponse(BidResponse originalBidResponse) { + return originalBidResponse.toBuilder() + .seatbid(originalBidResponse.getSeatbid().stream() + .map(seatBid -> seatBid.toBuilder() + .bid(seatBid.getBid().stream() + .map(bid -> bid.toBuilder() + .adm(bid.getAdm() + + "") + .build()) + .collect(Collectors.toList())) + .build()) + .collect(Collectors.toList())) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java new file mode 100644 index 00000000000..c18f10bab56 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java @@ -0,0 +1,48 @@ +package org.prebid.server.it.hooks; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderRequestHook; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; + +import java.util.List; +import java.util.stream.Collectors; + +public class SampleItBidderRequestHook implements BidderRequestHook { + + @Override + public Future> call( + BidderRequestPayload bidderRequestPayload, BidderInvocationContext invocationContext) { + + final BidRequest originalBidRequest = bidderRequestPayload.bidRequest(); + + final BidRequest updatedBidRequest = updateBidRequest(originalBidRequest, invocationContext); + + return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + BidderRequestPayloadImpl.of(payload.bidRequest().toBuilder() + .imp(updatedBidRequest.getImp()) + .build()))); + } + + @Override + public String code() { + return "bidder-request"; + } + + private BidRequest updateBidRequest( + BidRequest originalBidRequest, BidderInvocationContext bidderInvocationContext) { + + final List updatedImps = originalBidRequest.getImp().stream() + .map(imp -> imp.toBuilder().tagid("tagid-from-bidder-request-hook").build()) + .collect(Collectors.toList()); + + return originalBidRequest.toBuilder() + .imp(updatedImps) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java new file mode 100644 index 00000000000..36827bd39df --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java @@ -0,0 +1,59 @@ +package org.prebid.server.it.hooks; + +import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; +import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.model.CaseInsensitiveMultiMap; + +public class SampleItEntrypointHook implements EntrypointHook { + + @Override + public Future> call( + EntrypointPayload entrypointPayload, InvocationContext invocationContext) { + + final boolean rejectFlag = Boolean.parseBoolean(entrypointPayload.queryParams().get("sample-it-module-reject")); + if (rejectFlag) { + return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by sample entrypoint hook")); + } + + return maybeUpdate(entrypointPayload); + } + + private Future> maybeUpdate(EntrypointPayload entrypointPayload) { + final String updateSelector = entrypointPayload.queryParams().get("sample-it-module-update"); + + final CaseInsensitiveMultiMap updatedHeaders = StringUtils.contains(updateSelector, "headers") + ? updateHeaders(entrypointPayload.headers()) + : entrypointPayload.headers(); + + final String updatedBody = StringUtils.contains(updateSelector, "body") + ? updateBody(entrypointPayload.body()) + : entrypointPayload.body(); + + return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + payload.queryParams(), + updatedHeaders, + updatedBody))); + } + + private static CaseInsensitiveMultiMap updateHeaders(CaseInsensitiveMultiMap headers) { + return CaseInsensitiveMultiMap.builder() + .addAll(headers) + .add("X-Forwarded-For", "222.111.222.111") + .build(); + } + + private static String updateBody(String body) { + return body.replaceAll("\"language\":\"en\"", "\"language\":\"fr\""); + } + + @Override + public String code() { + return "entrypoint"; + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItModule.java b/src/test/java/org/prebid/server/it/hooks/SampleItModule.java new file mode 100644 index 00000000000..e2806f8c87f --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItModule.java @@ -0,0 +1,41 @@ +package org.prebid.server.it.hooks; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.Module; +import org.prebid.server.json.JacksonMapper; + +import java.util.Arrays; +import java.util.Collection; + +public class SampleItModule implements Module { + + static final String MODULE_EXT = "sample-it-module"; + + private final JacksonMapper mapper; + + public SampleItModule(JacksonMapper mapper) { + this.mapper = mapper; + } + + @Override + public Collection> hooks() { + return Arrays.asList( + new SampleItEntrypointHook(), + new SampleItRawAuctionRequestHook(mapper), + new SampleItProcessedAuctionRequestHook(mapper), + new SampleItBidderRequestHook(), + new SampleItRawBidderResponseHook(), + new SampleItProcessedBidderResponseHook(), + new SampleItAuctionResponseHook(), + new SampleItRejectingRawAuctionRequestHook(), + new SampleItRejectingProcessedAuctionRequestHook(), + new SampleItRejectingBidderRequestHook(), + new SampleItRejectingRawBidderResponseHook(), + new SampleItRejectingProcessedBidderResponseHook()); + } + + @Override + public String code() { + return "sample-it-module"; + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..a285235f420 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java @@ -0,0 +1,66 @@ +package org.prebid.server.it.hooks; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; + +public class SampleItProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + private final JacksonMapper mapper; + + public SampleItProcessedAuctionRequestHook(JacksonMapper mapper) { + this.mapper = mapper; + } + + @Override + public Future> call( + AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { + + final BidRequest originalBidRequest = auctionRequestPayload.bidRequest(); + + final BidRequest updatedBidRequest = updateBidRequest(originalBidRequest); + + return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + AuctionRequestPayloadImpl.of(payload.bidRequest().toBuilder() + .ext(updatedBidRequest.getExt()) + .build()))); + } + + @Override + public String code() { + return "processed-auction-request"; + } + + private BidRequest updateBidRequest(BidRequest originalBidRequest) { + final ExtRequest originalExt = originalBidRequest.getExt(); + + final JsonNode moduleExt = originalExt != null ? originalExt.getProperty(SampleItModule.MODULE_EXT) : null; + final ObjectNode updatedModuleExt = + moduleExt != null ? moduleExt.deepCopy() : mapper.mapper().createObjectNode(); + updatedModuleExt.put(code() + "-trace", "I've been here"); + + final ExtRequest updatedExt = copyExt(originalExt); + updatedExt.addProperty(SampleItModule.MODULE_EXT, updatedModuleExt); + + return originalBidRequest.toBuilder() + .ext(updatedExt) + .build(); + } + + private ExtRequest copyExt(ExtRequest originalExt) { + final ExtRequest updatedExt = originalExt != null ? ExtRequest.of(originalExt.getPrebid()) : ExtRequest.empty(); + if (originalExt != null) { + updatedExt.addProperties(originalExt.getProperties()); + } + return updatedExt; + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java new file mode 100644 index 00000000000..210bb840ad3 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java @@ -0,0 +1,46 @@ +package org.prebid.server.it.hooks; + +import io.vertx.core.Future; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; + +import java.util.List; +import java.util.stream.Collectors; + +public class SampleItProcessedBidderResponseHook implements ProcessedBidderResponseHook { + + @Override + public Future> call( + BidderResponsePayload bidderResponsePayload, BidderInvocationContext invocationContext) { + + final List originalBids = bidderResponsePayload.bids(); + + final List updatedBids = updateBids(originalBids); + + return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + BidderResponsePayloadImpl.of(updatedBids))); + } + + @Override + public String code() { + return "processed-bidder-response"; + } + + private List updateBids(List originalBids) { + return originalBids.stream() + .map(bidderBid -> BidderBid.of( + bidderBid.getBid().toBuilder() + .adm(bidderBid.getBid().getAdm() + + "" + + "") + .build(), + bidderBid.getType(), + bidderBid.getBidCurrency())) + .collect(Collectors.toList()); + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java new file mode 100644 index 00000000000..c1054166dc1 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java @@ -0,0 +1,92 @@ +package org.prebid.server.it.hooks; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.v1.analytics.ResultImpl; +import org.prebid.server.hooks.v1.analytics.TagsImpl; +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.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; + +import java.util.Arrays; +import java.util.Collections; + +public class SampleItRawAuctionRequestHook implements RawAuctionRequestHook { + + private final JacksonMapper mapper; + + public SampleItRawAuctionRequestHook(JacksonMapper mapper) { + this.mapper = mapper; + } + + @Override + public Future> call( + AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { + + final BidRequest originalBidRequest = auctionRequestPayload.bidRequest(); + + final BidRequest updatedBidRequest = updateBidRequest(originalBidRequest); + + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(payload -> + AuctionRequestPayloadImpl.of(payload.bidRequest().toBuilder() + .ext(updatedBidRequest.getExt()) + .build())) + .debugMessages(Arrays.asList( + "raw auction request debug message 1", + "raw auction request debug message 1")) + .analyticsTags(TagsImpl.of(Collections.singletonList(ActivityImpl.of( + "device-id", + "success", + Collections.singletonList(ResultImpl.of( + "success", + mapper.mapper().createObjectNode().put("some-field", "some-value"), + AppliedToImpl.builder() + .impIds(Collections.singletonList("impId1")) + .request(true) + .build())))))) + .build()); + } + + @Override + public String code() { + return "raw-auction-request"; + } + + private BidRequest updateBidRequest(BidRequest originalBidRequest) { + final ExtRequest originalExt = originalBidRequest.getExt(); + + final JsonNode moduleExt = originalExt != null ? originalExt.getProperty(SampleItModule.MODULE_EXT) : null; + final ObjectNode updatedModuleExt = + moduleExt != null ? moduleExt.deepCopy() : mapper.mapper().createObjectNode(); + updatedModuleExt.put(code() + "-trace", "I've been here"); + + final ExtRequest updatedExt = copyExt(originalExt); + updatedExt.addProperty(SampleItModule.MODULE_EXT, updatedModuleExt); + + return originalBidRequest.toBuilder() + .ext(updatedExt) + .build(); + } + + private ExtRequest copyExt(ExtRequest originalExt) { + final ExtRequest updatedExt = originalExt != null ? ExtRequest.of(originalExt.getPrebid()) : ExtRequest.empty(); + if (originalExt != null) { + updatedExt.addProperties(originalExt.getProperties()); + } + return updatedExt; + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java new file mode 100644 index 00000000000..79d1cd52936 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java @@ -0,0 +1,46 @@ +package org.prebid.server.it.hooks; + +import io.vertx.core.Future; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; + +import java.util.List; +import java.util.stream.Collectors; + +public class SampleItRawBidderResponseHook implements RawBidderResponseHook { + + @Override + public Future> call( + BidderResponsePayload bidderResponsePayload, BidderInvocationContext invocationContext) { + + final List originalBids = bidderResponsePayload.bids(); + + final List updatedBids = updateBids(originalBids); + + return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + BidderResponsePayloadImpl.of(updatedBids))); + } + + @Override + public String code() { + return "raw-bidder-response"; + } + + private List updateBids(List originalBids) { + return originalBids.stream() + .map(bidderBid -> BidderBid.of( + bidderBid.getBid().toBuilder() + .adm(bidderBid.getBid().getAdm() + + "" + + "") + .build(), + bidderBid.getType(), + bidderBid.getBidCurrency())) + .collect(Collectors.toList()); + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java new file mode 100644 index 00000000000..bd90a974936 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java @@ -0,0 +1,23 @@ +package org.prebid.server.it.hooks; + +import io.vertx.core.Future; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderRequestHook; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; + +public class SampleItRejectingBidderRequestHook implements BidderRequestHook { + + @Override + public Future> call( + BidderRequestPayload bidderRequestPayload, BidderInvocationContext invocationContext) { + + return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting bidder request hook")); + } + + @Override + public String code() { + return "rejecting-bidder-request"; + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..b5feb3aaef9 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java @@ -0,0 +1,24 @@ +package org.prebid.server.it.hooks; + +import io.vertx.core.Future; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; + +public class SampleItRejectingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + @Override + public Future> call( + AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { + + return Future.succeededFuture(InvocationResultImpl.rejected( + "Rejected by rejecting processed auction request hook")); + } + + @Override + public String code() { + return "rejecting-processed-auction-request"; + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java new file mode 100644 index 00000000000..a6f1438402c --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java @@ -0,0 +1,24 @@ +package org.prebid.server.it.hooks; + +import io.vertx.core.Future; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; + +public class SampleItRejectingProcessedBidderResponseHook implements ProcessedBidderResponseHook { + + @Override + public Future> call( + BidderResponsePayload bidderResponsePayload, BidderInvocationContext invocationContext) { + + return Future.succeededFuture(InvocationResultImpl.rejected( + "Rejected by rejecting processed bidder response hook")); + } + + @Override + public String code() { + return "rejecting-processed-bidder-response"; + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java new file mode 100644 index 00000000000..5532962afc2 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java @@ -0,0 +1,23 @@ +package org.prebid.server.it.hooks; + +import io.vertx.core.Future; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; + +public class SampleItRejectingRawAuctionRequestHook implements RawAuctionRequestHook { + + @Override + public Future> call( + AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { + + return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting raw auction request hook")); + } + + @Override + public String code() { + return "rejecting-raw-auction-request"; + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java new file mode 100644 index 00000000000..0eeeee4375c --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java @@ -0,0 +1,23 @@ +package org.prebid.server.it.hooks; + +import io.vertx.core.Future; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; + +public class SampleItRejectingRawBidderResponseHook implements RawBidderResponseHook { + + @Override + public Future> call( + BidderResponsePayload bidderResponsePayload, BidderInvocationContext invocationContext) { + + return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting raw bidder response hook")); + } + + @Override + public String code() { + return "rejecting-raw-bidder-response"; + } +} diff --git a/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java b/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java new file mode 100644 index 00000000000..a08845f9c19 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java @@ -0,0 +1,15 @@ +package org.prebid.server.it.hooks; + +import org.prebid.server.hooks.v1.Module; +import org.prebid.server.json.JacksonMapper; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestHooksConfiguration { + + @Bean + Module sampleItModule(JacksonMapper mapper) { + return new SampleItModule(mapper); + } +} diff --git a/src/test/java/org/prebid/server/it/util/BidCacheRequestPattern.java b/src/test/java/org/prebid/server/it/util/BidCacheRequestPattern.java new file mode 100644 index 00000000000..c94393f98a0 --- /dev/null +++ b/src/test/java/org/prebid/server/it/util/BidCacheRequestPattern.java @@ -0,0 +1,59 @@ +package org.prebid.server.it.util; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.tomakehurst.wiremock.common.Json; +import com.github.tomakehurst.wiremock.matching.EqualToJsonPattern; +import com.github.tomakehurst.wiremock.matching.MatchResult; +import com.github.tomakehurst.wiremock.matching.StringValuePattern; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.cache.proto.request.BidCacheRequest; +import org.prebid.server.cache.proto.request.PutObject; + +import java.util.List; + +/** + * Class was created to compare complex Json object when ordering of inner array is not predetermined. + * The Wiremock equalToJson method that creates {@link EqualToJsonPattern} cannot compare such objects correctly + * even when using the unordered flag.. + */ +public class BidCacheRequestPattern extends StringValuePattern { + + private final BidCacheRequest expected; + + public BidCacheRequestPattern(@JsonProperty("equalToJson") String json) { + super(json); + expected = Json.read(json, BidCacheRequest.class); + } + + @Override + public String getExpected() { + return Json.prettyPrint(getValue()); + } + + @Override + public MatchResult match(String value) { + try { + final BidCacheRequest actual = Json.read(value, BidCacheRequest.class); + + return new MatchResult() { + @Override + public boolean isExactMatch() { + return getDistance() == 0; + } + + @Override + public double getDistance() { + final List actualPuts = actual.getPuts(); + final List expectedPuts = expected.getPuts(); + if (CollectionUtils.isEqualCollection(actualPuts, expectedPuts)) { + return 0; + } + + return CollectionUtils.disjunction(actualPuts, expectedPuts).size(); + } + }; + } catch (Exception e) { + return MatchResult.noMatch(); + } + } +} diff --git a/src/test/java/org/prebid/server/it/util/BidCacheRequestPatternTest.java b/src/test/java/org/prebid/server/it/util/BidCacheRequestPatternTest.java new file mode 100644 index 00000000000..876f050e4c4 --- /dev/null +++ b/src/test/java/org/prebid/server/it/util/BidCacheRequestPatternTest.java @@ -0,0 +1,75 @@ +package org.prebid.server.it.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.matching.MatchResult; +import org.junit.Test; +import org.prebid.server.json.ObjectMapperProvider; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BidCacheRequestPatternTest { + + private final ObjectMapper mapper = ObjectMapperProvider.mapper(); + + @Test + public void shouldMatchWhenPutsAreUnordered() throws IOException { + // given + final String original = jsonFrom("bidcacherequestpattern/original-bid-request.json"); + final String unordered = jsonFrom("bidcacherequestpattern/unordered-bid-request.json"); + + // when + final MatchResult result = new BidCacheRequestPattern(original).match(unordered); + + // then + assertThat(result.getDistance()).isEqualTo(0); + assertThat(result.isExactMatch()).isTrue(); + } + + @Test + public void shouldNotMatchWhenPutIsMissing() throws IOException { + // given + final String original = jsonFrom("bidcacherequestpattern/original-bid-request.json"); + final String missing = jsonFrom("bidcacherequestpattern/missing-put-bid-request.json"); + + // when + final MatchResult result = new BidCacheRequestPattern(original).match(missing); + + // then + assertThat(result.getDistance()).isEqualTo(1); + assertThat(result.isExactMatch()).isFalse(); + } + + @Test + public void shouldNotMatchWhenPutIsRedundant() throws IOException { + // given + final String original = jsonFrom("bidcacherequestpattern/original-bid-request.json"); + final String redundant = jsonFrom("bidcacherequestpattern/redundant-put-bid-request.json"); + + // when + final MatchResult result = new BidCacheRequestPattern(original).match(redundant); + + // then + assertThat(result.getDistance()).isEqualTo(1); + assertThat(result.isExactMatch()).isFalse(); + } + + @Test + public void shouldNotMatchWhenPutIsChanged() throws IOException { + // given + final String original = jsonFrom("bidcacherequestpattern/original-bid-request.json"); + final String changed = jsonFrom("bidcacherequestpattern/changed-put-bid-request.json"); + + // when + final MatchResult result = new BidCacheRequestPattern(original).match(changed); + + // then + assertThat(result.getDistance()).isEqualTo(3); + assertThat(result.isExactMatch()).isFalse(); + } + + private String jsonFrom(String file) throws IOException { + return mapper.writeValueAsString(mapper.readTree(BidCacheRequestPatternTest.class.getResourceAsStream(file))); + } +} diff --git a/src/test/java/org/prebid/server/json/IntegerFlagDeserializerTest.java b/src/test/java/org/prebid/server/json/IntegerFlagDeserializerTest.java new file mode 100644 index 00000000000..c0eacfe9174 --- /dev/null +++ b/src/test/java/org/prebid/server/json/IntegerFlagDeserializerTest.java @@ -0,0 +1,92 @@ +package org.prebid.server.json; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; + +public class IntegerFlagDeserializerTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + private IntegerFlagDeserializer integerFlagDeserializer; + + @Mock + private JsonParser jsonParser; + + @Mock + private DeserializationContext deserializationContext; + + @Before + public void setUp() { + integerFlagDeserializer = new IntegerFlagDeserializer(); + } + + @Test + public void deserializeShouldReturnOneWhenFieldIsBooleanAndHasValueTrue() throws IOException { + // given + given(jsonParser.getCurrentToken()).willReturn(JsonToken.VALUE_TRUE); + + // when + final Integer result = integerFlagDeserializer.deserialize(jsonParser, deserializationContext); + + // then + assertThat(result).isEqualTo(1); + } + + @Test + public void deserializeShouldReturnZeroWhenFieldIsBooleanAndHasValueFalse() throws IOException { + // given + given(jsonParser.getCurrentToken()).willReturn(JsonToken.VALUE_FALSE); + + // when + final Integer result = integerFlagDeserializer.deserialize(jsonParser, deserializationContext); + + // then + assertThat(result).isEqualTo(0); + } + + @Test + public void deserializeShouldReturnIntegerValueWhenFieldIsInt() throws IOException { + // given + given(jsonParser.getCurrentToken()).willReturn(JsonToken.VALUE_NUMBER_INT); + given(jsonParser.getValueAsInt()).willReturn(1); + + // when + final Integer result = integerFlagDeserializer.deserialize(jsonParser, deserializationContext); + + // then + assertThat(result).isEqualTo(1); + } + + @Test + public void deserializeShouldThrowJsonParsingExceptionWhenTypeIsNotBooleanOrInt() throws IOException { + // given + given(jsonParser.getCurrentToken()).willReturn(JsonToken.VALUE_STRING); + doThrow(MismatchedInputException.from(jsonParser, Integer.class, "errorMessage")) + .when(deserializationContext) + .reportWrongTokenException(eq(JsonToken.class), eq(JsonToken.VALUE_NUMBER_INT), anyString()); + + // when and then + assertThatThrownBy(() -> integerFlagDeserializer.deserialize(jsonParser, deserializationContext)) + .isInstanceOf(JsonProcessingException.class) + .hasMessage("errorMessage"); + } +} diff --git a/src/test/java/org/prebid/server/json/JsonMergerTest.java b/src/test/java/org/prebid/server/json/JsonMergerTest.java new file mode 100644 index 00000000000..1d4a7cd85cb --- /dev/null +++ b/src/test/java/org/prebid/server/json/JsonMergerTest.java @@ -0,0 +1,75 @@ +package org.prebid.server.json; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import org.junit.Before; +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigOrtb; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JsonMergerTest extends VertxTest { + + private JsonMerger target; + + @Before + public void setUp() { + target = new JsonMerger(jacksonMapper); + } + + @Test + public void mergeShouldReturnMergedObject() { + // given + final ObjectNode siteWithPage = mapper.valueToTree(Site.builder().page("testPage").build()); + final Publisher publisherWithId = Publisher.builder().id("testId").build(); + final ObjectNode appWithPublisherId = mapper.valueToTree(App.builder().publisher(publisherWithId).build()); + final ExtBidderConfigOrtb firstBidderConfigFpd = ExtBidderConfigOrtb.of(siteWithPage, appWithPublisherId, null); + + final ObjectNode siteWithDomain = mapper.valueToTree(Site.builder().domain("testDomain").build()); + final Publisher publisherWithIdAndDomain = Publisher.builder().id("shouldNotBe").domain("domain").build(); + final ObjectNode appWithUpdatedPublisher = mapper.valueToTree(App.builder() + .publisher(publisherWithIdAndDomain).build()); + final ExtBidderConfigOrtb secondBidderConfigFpd = + ExtBidderConfigOrtb.of(siteWithDomain, appWithUpdatedPublisher, null); + + // when + final ExtBidderConfigOrtb result = target.merge(firstBidderConfigFpd, secondBidderConfigFpd, + ExtBidderConfigOrtb.class); + + // then + final ObjectNode mergedSite = mapper.valueToTree(Site.builder().page("testPage").domain("testDomain").build()); + final Publisher mergedPublisher = Publisher.builder().id("testId").domain("domain").build(); + final ObjectNode mergedApp = mapper.valueToTree(App.builder().publisher(mergedPublisher).build()); + final ExtBidderConfigOrtb mergedConfigFpd = ExtBidderConfigOrtb.of(mergedSite, mergedApp, null); + + assertThat(result).isEqualTo(mergedConfigFpd); + } + + @Test + public void mergeShouldReturnOriginalObjectWhenMergedObjectIsNull() { + // given + final Site site = Site.builder().build(); + + // when + final Site result = target.merge(site, null, Site.class); + + // then + assertThat(result).isEqualTo(site); + } + + @Test + public void mergeShouldReturnMergedObjectWhenOriginalObjectIsNull() { + // given + final Site site = Site.builder().build(); + + // when + final Site result = target.merge(null, site, Site.class); + + // then + assertThat(result).isEqualTo(site); + } + +} diff --git a/src/test/java/org/prebid/server/log/ConditionalLoggerTest.java b/src/test/java/org/prebid/server/log/ConditionalLoggerTest.java index 943cf54f87f..7623c7e273c 100644 --- a/src/test/java/org/prebid/server/log/ConditionalLoggerTest.java +++ b/src/test/java/org/prebid/server/log/ConditionalLoggerTest.java @@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; @RunWith(VertxUnitRunner.class) public class ConditionalLoggerTest { @@ -44,6 +45,19 @@ public void tearDown(TestContext context) { vertx.close(context.asyncAssertSuccess()); } + @Test + public void infoWithKeyShouldCallLoggerWithExpectedCount() { + // when + for (int i = 0; i < 10; i++) { + conditionalLogger.infoWithKey("key", "Log Message1", 2); + conditionalLogger.infoWithKey("key", "Log Message2", 2); + } + + // then + verify(logger, times(10)).info("Log Message2"); + verifyNoMoreInteractions(logger); + } + @Test public void infoShouldCallLoggerWithExpectedCount() { // when diff --git a/src/test/java/org/prebid/server/log/CriteriaManagerTest.java b/src/test/java/org/prebid/server/log/CriteriaManagerTest.java new file mode 100644 index 00000000000..7be8a0c8af7 --- /dev/null +++ b/src/test/java/org/prebid/server/log/CriteriaManagerTest.java @@ -0,0 +1,77 @@ +package org.prebid.server.log; + +import io.vertx.core.Vertx; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.deals.model.LogCriteriaFilter; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +public class CriteriaManagerTest extends VertxTest { + + @Rule + public final MockitoRule rule = MockitoJUnit.rule(); + + @Mock + private CriteriaLogManager criteriaLogManager; + @Mock + private Vertx vertx; + + private CriteriaManager criteriaManager; + + @Before + public void setUp() { + criteriaManager = new CriteriaManager(criteriaLogManager, vertx); + } + + @Test + public void addCriteriaShouldThrowIllegalArgumentExceptionWhenLoggerLevelHasInvalidValue() { + assertThatIllegalArgumentException() + .isThrownBy(() -> criteriaManager.addCriteria("1001", "rubicon", "lineItemId1", "invalid", 800)) + .withMessage("Invalid LoggingLevel: invalid"); + } + + @Test + public void addCriteriaShouldAddVertxTimerWithLimitedDurationInMillis() { + // given and when + criteriaManager.addCriteria("1001", "rubicon", "lineItemId1", "error", 800000); + + // then + verify(vertx).setTimer(eq(300000L), any()); + } + + @Test + public void addCriteriaShouldAddVertxTimerWithDefaultDurationInMillis() { + // given and when + criteriaManager.addCriteria("1001", "rubicon", "lineItemId1", "error", 200000); + + // then + verify(vertx).setTimer(eq(200000L), any()); + } + + @Test + public void addCriteriaByFilterShouldAddVertxTimeWithLimitedDurationInMillis() { + // given and when + criteriaManager.addCriteria(LogCriteriaFilter.of("1001", "rubicon", "lineItemId1"), 800L); + + // then + verify(vertx).setTimer(eq(300000L), any()); + } + + @Test + public void addCriteriaByFilterShouldAddVertxTimeWithNotLimitedDurationInMillis() { + // given and when + criteriaManager.addCriteria(LogCriteriaFilter.of("1001", "rubicon", "lineItemId1"), 200L); + + // then + verify(vertx).setTimer(eq(200000L), any()); + } +} diff --git a/src/test/java/org/prebid/server/log/CriteriaTest.java b/src/test/java/org/prebid/server/log/CriteriaTest.java new file mode 100644 index 00000000000..e8374ad8280 --- /dev/null +++ b/src/test/java/org/prebid/server/log/CriteriaTest.java @@ -0,0 +1,121 @@ +package org.prebid.server.log; + +import io.vertx.core.logging.Logger; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class CriteriaTest { + + @Rule + public final MockitoRule rule = MockitoJUnit.rule(); + + @Mock + private Logger logger; + + @Test + public void logShouldChooseCriteriaLoggerLevelWhenAccountsMatchedAndElseFieldsAreNull() { + // given + final Criteria criteria = Criteria.create("account", null, null, Logger::error); + + // when + criteria.log(Criteria.builder().account("account").bidder("rubicon").build(), logger, "message", logger::info); + + // then + verify(logger).error(anyString()); + } + + @Test + public void logShouldChooseDefaultLogLevelWhenAccountsMatchedAndElseFieldsAreNotNullAndNotMatched() { + // given + final Criteria criteria = Criteria.create("account", "appnexus", null, Logger::error); + + // when + criteria.log(Criteria.builder().account("account").bidder("rubicon").build(), + logger, "message", logger::info); + + // then + verify(logger).info(anyString()); + } + + @Test + public void logShouldChooseCriteriaLogLevelAccountAndBidderMatchedLineItemIsNull() { + // given + final Criteria criteria = Criteria.create("account", "rubicon", null, Logger::error); + + // when + criteria.log(Criteria.builder().account("account").bidder("rubicon") + .lineItemId("lineItemId").build(), logger, "message", logger::info); + + // then + verify(logger).error(anyString()); + } + + @Test + public void logShouldChooseCriteriaLogLevelWhenAllMatched() { + // given + final Criteria criteria = Criteria.create("account", "rubicon", "lineItemId", Logger::error); + + // when + criteria.log(Criteria.builder().account("account").bidder("rubicon") + .lineItemId("lineItemId").build(), logger, "message", logger::info); + + // then + verify(logger).error(anyString()); + } + + @Test + public void logResponseShouldLogResponseWhenAllNotNullCriteriasPresent() { + // given + final Criteria criteria = Criteria.create("account", null, "lineItemId", Logger::error); + + // when + criteria.logResponse("Response has account and lineItemId", logger); + + // then + verify(logger).error(anyString()); + } + + @Test + public void logResponseShouldNotLogResponseWhenOneOfNutNullCriteriaMissing() { + // given + final Criteria criteria = Criteria.create("account", null, "lineItemId", Logger::error); + + // when + criteria.logResponse("Response has account", logger); + + // then + verifyZeroInteractions(logger); + } + + @Test + public void logResponseAndRequestShouldLogResponseWhenAllNotNullCriteriasPresent() { + // given + final Criteria criteria = Criteria.create("account", null, "lineItemId", Logger::error); + + // when + criteria.logResponseAndRequest("Response has account", "Request has lineItemId", logger); + + // then + verify(logger, times(2)).error(anyString()); + } + + @Test + public void logResponseAndRequestShouldNotLogResponseWhenOneOfNutNullCriteriaMissing() { + // given + final Criteria criteria = Criteria.create("account", null, "lineItemId", Logger::error); + + // when + criteria.logResponseAndRequest("Response has account", "Request", logger); + + // then + verifyZeroInteractions(logger); + } +} diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index 474130cc313..78a8e7dfffa 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -18,7 +18,9 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.metric.model.AccountMetricsVerbosityLevel; import java.util.EnumMap; @@ -29,7 +31,6 @@ import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -38,8 +39,9 @@ public class MetricsTest { private static final String RUBICON = "rubicon"; - private static final String INVALID_BIDDER = "invalid"; + private static final String CONVERSANT = "conversant"; private static final String ACCOUNT_ID = "accountId"; + private static final String ANALYTIC_CODE = "analyticCode"; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -47,8 +49,6 @@ public class MetricsTest { private MetricRegistry metricRegistry; @Mock private AccountMetricsVerbosity accountMetricsVerbosity; - @Mock - private BidderCatalog bidderCatalog; private Metrics metrics; @@ -56,9 +56,8 @@ public class MetricsTest { public void setUp() { metricRegistry = new MetricRegistry(); given(accountMetricsVerbosity.forAccount(anyString())).willReturn(AccountMetricsVerbosityLevel.detailed); - given(bidderCatalog.isValidName(any())).willReturn(true); - metrics = new Metrics(metricRegistry, CounterType.counter, accountMetricsVerbosity, bidderCatalog); + metrics = new Metrics(metricRegistry, CounterType.counter, accountMetricsVerbosity); } @Test @@ -82,7 +81,7 @@ public void forAccountShouldReturnAccountMetricsConfiguredWithAccount() { metrics.forAccount(ACCOUNT_ID).incCounter(MetricName.requests); // then - assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isOne(); } @Test @@ -102,7 +101,7 @@ public void forAdapterShouldReturnAdapterMetricsConfiguredWithAdapterType() { metrics.forAdapter(RUBICON).incCounter(MetricName.bids_received); // then - assertThat(metricRegistry.counter("adapter.rubicon.bids_received").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.bids_received").getCount()).isOne(); } @Test @@ -125,7 +124,7 @@ public void shouldReturnAdapterRequestTypeMetricsConfiguredWithAdapterType() { metrics.forAdapter(RUBICON).requestType(MetricName.openrtb2web).incCounter(MetricName.requests); // then - assertThat(metricRegistry.counter("adapter.rubicon.requests.type.openrtb2-web").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.requests.type.openrtb2-web").getCount()).isOne(); } @Test @@ -148,19 +147,20 @@ public void shouldReturnAdapterRequestMetricsConfiguredWithAdapterType() { metrics.forAdapter(RUBICON).request().incCounter(MetricName.gotbids); // then - assertThat(metricRegistry.counter("adapter.rubicon.requests.gotbids").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.requests.gotbids").getCount()).isOne(); } @Test public void shouldReturnSameAccountAdapterMetricsOnSuccessiveCalls() { - assertThat(metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON)) - .isSameAs(metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON)); + assertThat(metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON)) + .isSameAs(metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON)); } @Test public void shouldReturnAccountAdapterMetricsConfiguredWithCounterType() { verifyCreatesConfiguredCounterType(metrics -> metrics .forAccount(ACCOUNT_ID) + .adapter() .forAdapter(RUBICON) .incCounter(MetricName.bids_received)); } @@ -168,22 +168,23 @@ public void shouldReturnAccountAdapterMetricsConfiguredWithCounterType() { @Test public void shouldReturnAccountAdapterMetricsConfiguredWithAccountAndAdapterType() { // when - metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON).incCounter(MetricName.bids_received); + metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON).incCounter(MetricName.bids_received); // then - assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.bids_received").getCount()).isOne(); } @Test public void shouldReturnSameAccountAdapterRequestMetricsOnSuccessiveCalls() { - assertThat(metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON).request()) - .isSameAs(metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON).request()); + assertThat(metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON).request()) + .isSameAs(metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON).request()); } @Test public void shouldReturnAccountAdapterRequestMetricsConfiguredWithCounterType() { verifyCreatesConfiguredCounterType(metrics -> metrics .forAccount(ACCOUNT_ID) + .adapter() .forAdapter(RUBICON) .request() .incCounter(MetricName.gotbids)); @@ -192,10 +193,11 @@ public void shouldReturnAccountAdapterRequestMetricsConfiguredWithCounterType() @Test public void shouldReturnAccountAdapterRequestMetricsConfiguredWithAccountAndAdapterType() { // when - metrics.forAccount(ACCOUNT_ID).forAdapter(RUBICON).request().incCounter(MetricName.gotbids); + metrics.forAccount(ACCOUNT_ID).adapter().forAdapter(RUBICON).request().incCounter(MetricName.gotbids); // then - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.requests.gotbids").getCount()) + .isOne(); } @Test @@ -218,7 +220,7 @@ public void shouldReturnAccountRequestTypeMetricsConfiguredWithAccount() { metrics.forAccount(ACCOUNT_ID).requestType(MetricName.openrtb2web).incCounter(MetricName.requests); // then - assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isOne(); } @Test @@ -238,7 +240,7 @@ public void userSyncShouldReturnUserSyncMetricsConfiguredWithPrefix() { metrics.userSync().incCounter(MetricName.opt_outs); // then - assertThat(metricRegistry.counter("usersync.opt_outs").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.opt_outs").getCount()).isOne(); } @Test @@ -260,7 +262,7 @@ public void shouldReturnBidderUserSyncMetricsConfiguredWithBidder() { metrics.userSync().forBidder(RUBICON).incCounter(MetricName.sets); // then - assertThat(metricRegistry.counter("usersync.rubicon.sets").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.rubicon.sets").getCount()).isOne(); } @Test @@ -280,7 +282,7 @@ public void cookieSyncShouldReturnCookieSyncMetricsConfiguredWithPrefix() { metrics.cookieSync().incCounter(MetricName.gen); // then - assertThat(metricRegistry.counter("cookie_sync.gen").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync.gen").getCount()).isOne(); } @Test @@ -302,7 +304,7 @@ public void shouldReturnBidderCookieSyncMetricsConfiguredWithBidder() { metrics.cookieSync().forBidder(RUBICON).incCounter(MetricName.gen); // then - assertThat(metricRegistry.counter("cookie_sync.rubicon.gen").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync.rubicon.gen").getCount()).isOne(); } @Test @@ -324,52 +326,34 @@ public void forRequestTypeShouldReturnRequestStatusMetricsConfiguredWithRequestT metrics.forRequestType(MetricName.openrtb2web).incCounter(MetricName.ok); // then - assertThat(metricRegistry.counter("requests.ok.openrtb2-web").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("requests.ok.openrtb2-web").getCount()).isOne(); } @Test public void forCircuitBreakerShouldReturnSameCircuitBreakerMetricsOnSuccessiveCalls() { - assertThat(metrics.forCircuitBreaker("id")).isSameAs(metrics.forCircuitBreaker("id")); - } - - @Test - public void forCircuitBreakerShouldReturnCircuitBreakerMetricsConfiguredWithCounterType() { - verifyCreatesConfiguredCounterType( - metrics -> metrics.forCircuitBreaker("id").incCounter(MetricName.httpclient_circuitbreaker_opened)); + assertThat(metrics.forCircuitBreakerType(MetricName.db)).isSameAs(metrics.forCircuitBreakerType(MetricName.db)); } @Test public void forCircuitBreakerShouldReturnCircuitBreakerMetricsConfiguredWithId() { // when - metrics.forCircuitBreaker("id").incCounter(MetricName.httpclient_circuitbreaker_opened); + metrics.forCircuitBreakerType(MetricName.db).createGauge(MetricName.opened, () -> 1); // then - assertThat(metricRegistry.counter("httpclient_circuitbreaker_opened.id").getCount()).isEqualTo(1); - } - - @Test - public void updateSafariRequestsMetricShouldIncrementMetric() { - // when - metrics.updateSafariRequestsMetric(true); - metrics.updateSafariRequestsMetric(false); - - // then - assertThat(metricRegistry.counter("safari_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.gauge("circuit-breaker.db.opened.count", () -> null).getValue()).isEqualTo(1L); } @Test public void updateAppAndNoCookieAndImpsRequestedMetricsShouldIncrementMetrics() { // when - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(true, false, false, 1); - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, false, false, 2); - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, false, true, 1); - metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, true, false, 1); + metrics.updateAppAndNoCookieAndImpsRequestedMetrics(true, false, 1); + metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, false, 2); + metrics.updateAppAndNoCookieAndImpsRequestedMetrics(false, true, 1); // then - assertThat(metricRegistry.counter("app_requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("no_cookie_requests").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("safari_no_cookie_requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("imps_requested").getCount()).isEqualTo(5); + assertThat(metricRegistry.counter("app_requests").getCount()).isOne(); + assertThat(metricRegistry.counter("no_cookie_requests").getCount()).isOne(); + assertThat(metricRegistry.counter("imps_requested").getCount()).isEqualTo(4); } @Test @@ -388,7 +372,7 @@ public void updateImpTypesMetricsByCountPerMediaTypeShouldIncrementMetrics() { // then assertThat(metricRegistry.counter("imps_banner").getCount()).isEqualTo(3); assertThat(metricRegistry.counter("imps_video").getCount()).isEqualTo(5); - assertThat(metricRegistry.counter("imps_native").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("imps_native").getCount()).isOne(); assertThat(metricRegistry.counter("imps_audio").getCount()).isEqualTo(4); } @@ -415,19 +399,19 @@ public void updateImpTypesMetricsByImpsShouldGroupCountByMediaTypeAndCallOverloa verify(metricsSpy).updateImpTypesMetrics(eq(expectedMap)); - assertThat(metricRegistry.counter("imps_banner").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("imps_banner").getCount()).isOne(); assertThat(metricRegistry.counter("imps_video").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("imps_native").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("imps_native").getCount()).isOne(); assertThat(metricRegistry.counter("imps_audio").getCount()).isEqualTo(2); } @Test public void updateRequestTimeMetricShouldUpdateMetric() { // when - metrics.updateRequestTimeMetric(456L); + metrics.updateRequestTimeMetric(MetricName.request_time, 456L); // then - assertThat(metricRegistry.timer("request_time").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("request_time").getCount()).isOne(); } @Test @@ -441,12 +425,21 @@ public void updateRequestTypeMetricShouldIncrementMetric() { metrics.updateRequestTypeMetric(MetricName.amp, MetricName.networkerr); // then - assertThat(metricRegistry.counter("requests.ok.openrtb2-web").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("requests.blacklisted_account.openrtb2-web").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("requests.blacklisted_app.openrtb2-app").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("requests.err.openrtb2-app").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("requests.badinput.amp").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("requests.networkerr.amp").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("requests.ok.openrtb2-web").getCount()).isOne(); + assertThat(metricRegistry.counter("requests.blacklisted_account.openrtb2-web").getCount()).isOne(); + assertThat(metricRegistry.counter("requests.blacklisted_app.openrtb2-app").getCount()).isOne(); + assertThat(metricRegistry.counter("requests.err.openrtb2-app").getCount()).isOne(); + assertThat(metricRegistry.counter("requests.badinput.amp").getCount()).isOne(); + assertThat(metricRegistry.counter("requests.networkerr.amp").getCount()).isOne(); + } + + @Test + public void uupdateRequestBidderCardinalityMetricShouldIncrementMetrics() { + // when + metrics.updateRequestBidderCardinalityMetric(3); + + // then + assertThat(metricRegistry.counter("bidder-cardinality.3.requests").getCount()).isEqualTo(1); } @Test @@ -455,118 +448,145 @@ public void updateAccountRequestMetricsShouldIncrementMetrics() { metrics.updateAccountRequestMetrics(ACCOUNT_ID, MetricName.openrtb2web); // then - assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isOne(); } @Test public void updateAdapterRequestTypeAndNoCookieMetricsShouldUpdateMetricsAsExpected() { - // given - given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); // when metrics.updateAdapterRequestTypeAndNoCookieMetrics(RUBICON, MetricName.openrtb2app, true); metrics.updateAdapterRequestTypeAndNoCookieMetrics(RUBICON, MetricName.amp, false); - metrics.updateAdapterRequestTypeAndNoCookieMetrics(INVALID_BIDDER, MetricName.openrtb2app, false); - metrics.updateAdapterRequestTypeAndNoCookieMetrics(INVALID_BIDDER, MetricName.amp, false); // then assertThat(metricRegistry.counter("adapter.rubicon.requests.type.openrtb2-app").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.no_cookie_requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.requests.type.amp").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.type.openrtb2-app").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.type.amp").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.no_cookie_requests").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.rubicon.requests.type.amp").getCount()).isOne(); } @Test - public void updateAdapterResponseTimeShouldUpdateMetrics() { - // given - given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); + public void updateAnalyticWithEventTypeShouldUpdateMetricsAsExpected() { + // when + metrics.updateAnalyticEventMetric(ANALYTIC_CODE, MetricName.event_auction, MetricName.ok); + metrics.updateAnalyticEventMetric(ANALYTIC_CODE, MetricName.event_amp, MetricName.timeout); + metrics.updateAnalyticEventMetric(ANALYTIC_CODE, MetricName.event_video, MetricName.err); + metrics.updateAnalyticEventMetric(ANALYTIC_CODE, MetricName.event_cookie_sync, MetricName.timeout); + metrics.updateAnalyticEventMetric(ANALYTIC_CODE, MetricName.event_notification, MetricName.err); + metrics.updateAnalyticEventMetric(ANALYTIC_CODE, MetricName.event_setuid, MetricName.badinput); + + // then + assertThat(metricRegistry.counter("analytics.analyticCode.auction.ok").getCount()).isOne(); + assertThat(metricRegistry.counter("analytics.analyticCode.amp.timeout").getCount()).isOne(); + assertThat(metricRegistry.counter("analytics.analyticCode.video.err").getCount()).isOne(); + assertThat(metricRegistry.counter("analytics.analyticCode.cookie_sync.timeout").getCount()).isOne(); + assertThat(metricRegistry.counter("analytics.analyticCode.event.err").getCount()).isOne(); + assertThat(metricRegistry.counter("analytics.analyticCode.setuid.badinput").getCount()).isOne(); + } + + @Test + public void updateAdapterResponseTimeShouldUpdateMetrics() { // when metrics.updateAdapterResponseTime(RUBICON, ACCOUNT_ID, 500); - metrics.updateAdapterResponseTime(INVALID_BIDDER, ACCOUNT_ID, 500); - metrics.updateAdapterResponseTime(INVALID_BIDDER, ACCOUNT_ID, 500); + metrics.updateAdapterResponseTime(CONVERSANT, ACCOUNT_ID, 500); + metrics.updateAdapterResponseTime(CONVERSANT, ACCOUNT_ID, 500); // then - assertThat(metricRegistry.timer("adapter.rubicon.request_time").getCount()).isEqualTo(1); - assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isEqualTo(1); - assertThat(metricRegistry.timer("adapter.UNKNOWN.request_time").getCount()).isEqualTo(2); - assertThat(metricRegistry.timer("account.accountId.UNKNOWN.request_time").getCount()).isEqualTo(2); + assertThat(metricRegistry.timer("adapter.rubicon.request_time").getCount()).isOne(); + assertThat(metricRegistry.timer("account.accountId.adapter.rubicon.request_time").getCount()).isOne(); + assertThat(metricRegistry.timer("adapter.conversant.request_time").getCount()).isEqualTo(2); + assertThat(metricRegistry.timer("account.accountId.adapter.conversant.request_time").getCount()).isEqualTo(2); } @Test public void updateAdapterRequestNobidMetricsShouldIncrementMetrics() { - // given - given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); - // when metrics.updateAdapterRequestNobidMetrics(RUBICON, ACCOUNT_ID); - metrics.updateAdapterRequestNobidMetrics(INVALID_BIDDER, ACCOUNT_ID); - metrics.updateAdapterRequestNobidMetrics(INVALID_BIDDER, ACCOUNT_ID); + metrics.updateAdapterRequestNobidMetrics(CONVERSANT, ACCOUNT_ID); + metrics.updateAdapterRequestNobidMetrics(CONVERSANT, ACCOUNT_ID); // then - assertThat(metricRegistry.counter("adapter.rubicon.requests.nobid").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.nobid").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("account.accountId.UNKNOWN.requests.nobid").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("adapter.rubicon.requests.nobid").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.requests.nobid").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.conversant.requests.nobid").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("account.accountId.adapter.conversant.requests.nobid").getCount()) + .isEqualTo(2); } @Test public void updateAdapterRequestGotbidsMetricsShouldIncrementMetrics() { - // given - given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); - // when metrics.updateAdapterRequestGotbidsMetrics(RUBICON, ACCOUNT_ID); - metrics.updateAdapterRequestGotbidsMetrics(INVALID_BIDDER, ACCOUNT_ID); - metrics.updateAdapterRequestGotbidsMetrics(INVALID_BIDDER, ACCOUNT_ID); + metrics.updateAdapterRequestGotbidsMetrics(CONVERSANT, ACCOUNT_ID); + metrics.updateAdapterRequestGotbidsMetrics(CONVERSANT, ACCOUNT_ID); // then - assertThat(metricRegistry.counter("adapter.rubicon.requests.gotbids").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.gotbids").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("account.accountId.UNKNOWN.requests.gotbids").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("adapter.rubicon.requests.gotbids").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.requests.gotbids").getCount()) + .isOne(); + assertThat(metricRegistry.counter("adapter.conversant.requests.gotbids").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("account.accountId.adapter.conversant.requests.gotbids").getCount()) + .isEqualTo(2); } @Test public void updateAdapterBidMetricsShouldUpdateMetrics() { - // given - given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); - // when metrics.updateAdapterBidMetrics(RUBICON, ACCOUNT_ID, 1234L, true, "banner"); metrics.updateAdapterBidMetrics(RUBICON, ACCOUNT_ID, 1234L, false, "video"); - metrics.updateAdapterBidMetrics(INVALID_BIDDER, ACCOUNT_ID, 1234L, false, "banner"); - metrics.updateAdapterBidMetrics(INVALID_BIDDER, ACCOUNT_ID, 1234L, false, "banner"); + metrics.updateAdapterBidMetrics(CONVERSANT, ACCOUNT_ID, 1234L, false, "banner"); + metrics.updateAdapterBidMetrics(CONVERSANT, ACCOUNT_ID, 1234L, false, "banner"); // then assertThat(metricRegistry.histogram("adapter.rubicon.prices").getCount()).isEqualTo(2); - assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isEqualTo(2); + assertThat(metricRegistry.histogram("account.accountId.adapter.rubicon.prices").getCount()).isEqualTo(2); assertThat(metricRegistry.counter("adapter.rubicon.bids_received").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("adapter.rubicon.banner.adm_bids_received").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.video.nurl_bids_received").getCount()).isEqualTo(1); - assertThat(metricRegistry.histogram("adapter.UNKNOWN.prices").getCount()).isEqualTo(2); - assertThat(metricRegistry.histogram("account.accountId.UNKNOWN.prices").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("adapter.UNKNOWN.bids_received").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("account.accountId.UNKNOWN.bids_received").getCount()).isEqualTo(2); - assertThat(metricRegistry.counter("adapter.UNKNOWN.banner.nurl_bids_received").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.bids_received").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("adapter.rubicon.banner.adm_bids_received").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.rubicon.video.nurl_bids_received").getCount()).isOne(); + assertThat(metricRegistry.histogram("adapter.conversant.prices").getCount()).isEqualTo(2); + assertThat(metricRegistry.histogram("account.accountId.adapter.conversant.prices").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("adapter.conversant.bids_received").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("account.accountId.adapter.conversant.bids_received").getCount()) + .isEqualTo(2); + assertThat(metricRegistry.counter("adapter.conversant.banner.nurl_bids_received").getCount()).isEqualTo(2); } @Test public void updateAdapterRequestErrorMetricShouldIncrementMetrics() { - // given - given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); - // when metrics.updateAdapterRequestErrorMetric(RUBICON, MetricName.badinput); - metrics.updateAdapterRequestErrorMetric(INVALID_BIDDER, MetricName.badinput); - metrics.updateAdapterRequestErrorMetric(INVALID_BIDDER, MetricName.badinput); + metrics.updateAdapterRequestErrorMetric(CONVERSANT, MetricName.badinput); + metrics.updateAdapterRequestErrorMetric(CONVERSANT, MetricName.badinput); // then - assertThat(metricRegistry.counter("adapter.rubicon.requests.badinput").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.requests.badinput").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("adapter.rubicon.requests.badinput").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.conversant.requests.badinput").getCount()).isEqualTo(2); + } + + @Test + public void updateSizeValidationMetricsShouldIncrementMetrics() { + // when + metrics.updateSizeValidationMetrics(RUBICON, ACCOUNT_ID, MetricName.err); + metrics.updateSizeValidationMetrics(CONVERSANT, ACCOUNT_ID, MetricName.err); + + // then + assertThat(metricRegistry.counter("adapter.rubicon.response.validation.size.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.conversant.response.validation.size.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.response.validation.size.err").getCount()).isEqualTo(2); + } + + @Test + public void updateSecureValidationMetricsShouldIncrementMetrics() { + // when + metrics.updateSecureValidationMetrics(RUBICON, ACCOUNT_ID, MetricName.err); + metrics.updateSecureValidationMetrics(CONVERSANT, ACCOUNT_ID, MetricName.err); + + // then + assertThat(metricRegistry.counter("adapter.rubicon.response.validation.secure.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.conversant.response.validation.secure.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.response.validation.secure.err").getCount()).isEqualTo(2); } @Test @@ -575,7 +595,7 @@ public void updateCookieSyncRequestMetricShouldIncrementMetric() { metrics.updateCookieSyncRequestMetric(); // then - assertThat(metricRegistry.counter("cookie_sync_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync_requests").getCount()).isOne(); } @Test @@ -584,7 +604,7 @@ public void updateUserSyncOptoutMetricShouldIncrementMetric() { metrics.updateUserSyncOptoutMetric(); // then - assertThat(metricRegistry.counter("usersync.opt_outs").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.opt_outs").getCount()).isOne(); } @Test @@ -593,7 +613,7 @@ public void updateUserSyncBadRequestMetricShouldIncrementMetric() { metrics.updateUserSyncBadRequestMetric(); // then - assertThat(metricRegistry.counter("usersync.bad_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.bad_requests").getCount()).isOne(); } @Test @@ -602,7 +622,7 @@ public void updateUserSyncSetsMetricShouldIncrementMetric() { metrics.updateUserSyncSetsMetric(RUBICON); // then - assertThat(metricRegistry.counter("usersync.rubicon.sets").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.rubicon.sets").getCount()).isOne(); } @Test @@ -611,22 +631,19 @@ public void updateUserSyncTcfBlockedMetricShouldIncrementMetric() { metrics.updateUserSyncTcfBlockedMetric(RUBICON); // then - assertThat(metricRegistry.counter("usersync.rubicon.tcf.blocked").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("usersync.rubicon.tcf.blocked").getCount()).isOne(); } @Test public void updateCookieSyncTcfBlockedMetricShouldIncrementMetric() { - // given - given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); - // when metrics.updateCookieSyncTcfBlockedMetric(RUBICON); - metrics.updateCookieSyncTcfBlockedMetric(INVALID_BIDDER); - metrics.updateCookieSyncTcfBlockedMetric(INVALID_BIDDER); + metrics.updateCookieSyncTcfBlockedMetric(CONVERSANT); + metrics.updateCookieSyncTcfBlockedMetric(CONVERSANT); // then - assertThat(metricRegistry.counter("cookie_sync.rubicon.tcf.blocked").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("cookie_sync.UNKNOWN.tcf.blocked").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("cookie_sync.rubicon.tcf.blocked").getCount()).isOne(); + assertThat(metricRegistry.counter("cookie_sync.conversant.tcf.blocked").getCount()).isEqualTo(2); } @Test @@ -635,7 +652,7 @@ public void updateCookieSyncGenMetricShouldIncrementMetric() { metrics.updateCookieSyncGenMetric(RUBICON); // then - assertThat(metricRegistry.counter("cookie_sync.rubicon.gen").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync.rubicon.gen").getCount()).isOne(); } @Test @@ -644,30 +661,159 @@ public void updateCookieSyncMatchesMetricShouldIncrementMetric() { metrics.updateCookieSyncMatchesMetric(RUBICON); // then - assertThat(metricRegistry.counter("cookie_sync.rubicon.matches").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("cookie_sync.rubicon.matches").getCount()).isOne(); } @Test - public void updateAuctionTcfMetricShouldIncrementMetrics() { - // given - given(bidderCatalog.isValidName(INVALID_BIDDER)).willReturn(false); + public void updateGpRequestMetricShouldIncrementPlannerRequestAndPlannerSuccessfulRequest() { + // when + metrics.updatePlannerRequestMetric(true); + // then + assertThat(metricRegistry.counter("pg.planner_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("pg.planner_request_successful").getCount()).isEqualTo(1); + } + + @Test + public void updateGpRequestMetricShouldIncrementPlannerRequestAndPlannerFailedRequest() { + // when + metrics.updatePlannerRequestMetric(false); + + // then + assertThat(metricRegistry.counter("pg.planner_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("pg.planner_request_failed").getCount()).isEqualTo(1); + } + + @Test + public void updateGpRequestMetricShouldIncrementUserDetailsSuccessfulRequest() { + // when + metrics.updateUserDetailsRequestMetric(true); + + // then + assertThat(metricRegistry.counter("user_details_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("user_details_request_successful").getCount()).isEqualTo(1); + } + + @Test + public void updateGpRequestMetricShouldIncrementUserDetailsFailedRequest() { + // when + metrics.updateUserDetailsRequestMetric(false); + + // then + assertThat(metricRegistry.counter("user_details_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("user_details_request_failed").getCount()).isEqualTo(1); + } + + @Test + public void updateGpRequestMetricShouldIncrementWinSuccessfulRequest() { + // when + metrics.updateWinEventRequestMetric(true); + + // then + assertThat(metricRegistry.counter("win_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("win_request_successful").getCount()).isEqualTo(1); + } + + @Test + public void updateGpRequestMetricShouldIncrementWinFailedRequest() { + // when + metrics.updateWinEventRequestMetric(false); + + // then + assertThat(metricRegistry.counter("win_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("win_request_failed").getCount()).isEqualTo(1); + } + + @Test + public void updateWinRequestTimeShouldLogTime() { + // when + metrics.updateWinRequestTime(20L); + + // then + assertThat(metricRegistry.timer("win_request_time").getCount()).isEqualTo(1); + } + + @Test + public void updateWinRequestPreparationFailedShouldIncrementMetric() { + // when + metrics.updateWinRequestPreparationFailed(); + + // then + assertThat(metricRegistry.counter("win_request_preparation_failed").getCount()).isEqualTo(1); + } + + @Test + public void updateUserDetailsRequestPreparationFailedShouldIncrementMetric() { + // when + metrics.updateUserDetailsRequestPreparationFailed(); + + // then + assertThat(metricRegistry.counter("user_details_request_preparation_failed").getCount()).isEqualTo(1); + } + + @Test + public void updateDeliveryRequestMetricShouldIncrementDeliveryRequestAndSuccessfulDeliveryRequest() { + // when + metrics.updateDeliveryRequestMetric(true); + + // then + assertThat(metricRegistry.counter("pg.delivery_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("pg.delivery_request_successful").getCount()).isEqualTo(1); + } + + @Test + public void updateDeliveryRequestMetricShouldIncrementDeliveryRequestAndFailedDeliveryRequest() { + // when + metrics.updateDeliveryRequestMetric(false); + + // then + assertThat(metricRegistry.counter("pg.delivery_requests").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("pg.delivery_request_failed").getCount()).isEqualTo(1); + } + + @Test + public void updateLineItemsNumberMetricShouldIncrementLineItemsNumberForAAcountValue() { + // when + metrics.updateLineItemsNumberMetric(20L); + + // then + assertThat(metricRegistry.counter("pg.planner_lineitems_received").getCount()).isEqualTo(20); + } + + @Test + public void updatePlannerRequestTimeShouldLogTime() { + // when + metrics.updatePlannerRequestTime(20L); + + // then + assertThat(metricRegistry.timer("pg.planner_request_time").getCount()).isEqualTo(1); + } + + @Test + public void updateDeliveryRequestTimeShouldLogTime() { + // when + metrics.updateDeliveryRequestTime(20L); + + // then + assertThat(metricRegistry.timer("pg.delivery_request_time").getCount()).isEqualTo(1); + } + + @Test + public void updateAuctionTcfMetricsShouldIncrementMetrics() { // when metrics.updateAuctionTcfMetrics(RUBICON, MetricName.openrtb2web, true, true, true, true); - metrics.updateAuctionTcfMetrics(INVALID_BIDDER, MetricName.openrtb2web, false, true, false, true); - metrics.updateAuctionTcfMetrics(INVALID_BIDDER, MetricName.openrtb2app, true, false, true, false); + metrics.updateAuctionTcfMetrics(CONVERSANT, MetricName.openrtb2web, false, true, true, false); + metrics.updateAuctionTcfMetrics(CONVERSANT, MetricName.openrtb2app, true, false, false, true); // then - assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.userid_removed").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.geo_masked").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.request_blocked").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.analytics_blocked").getCount()) - .isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-web.tcf.geo_masked").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-web.tcf.analytics_blocked").getCount()) - .isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-app.tcf.userid_removed").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("adapter.UNKNOWN.openrtb2-app.tcf.request_blocked").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.userid_removed").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.geo_masked").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.analytics_blocked").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.rubicon.openrtb2-web.tcf.request_blocked").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.conversant.openrtb2-web.tcf.geo_masked").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.conversant.openrtb2-web.tcf.analytics_blocked").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.conversant.openrtb2-app.tcf.userid_removed").getCount()).isOne(); + assertThat(metricRegistry.counter("adapter.conversant.openrtb2-app.tcf.request_blocked").getCount()).isOne(); } @Test @@ -682,12 +828,13 @@ public void privacyTcfShouldReturnSameMetricsOnSuccessiveCalls() { @Test public void privacyTcfVersionShouldReturnSameMetricsOnSuccessiveCalls() { - assertThat(metrics.privacy().tcf().v1()).isSameAs(metrics.privacy().tcf().v1()); + assertThat(metrics.privacy().tcf().fromVersion(1)).isSameAs(metrics.privacy().tcf().fromVersion(1)); } @Test public void privacyTcfVersionVendorListShouldReturnSameMetricsOnSuccessiveCalls() { - assertThat(metrics.privacy().tcf().v2().vendorList()).isSameAs(metrics.privacy().tcf().v2().vendorList()); + assertThat(metrics.privacy().tcf().fromVersion(2).vendorList()) + .isSameAs(metrics.privacy().tcf().fromVersion(2).vendorList()); } @Test @@ -696,7 +843,7 @@ public void updatePrivacyCoppaMetricShouldIncrementMetric() { metrics.updatePrivacyCoppaMetric(); // then - assertThat(metricRegistry.counter("privacy.coppa").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.coppa").getCount()).isOne(); } @Test @@ -705,7 +852,7 @@ public void updatePrivacyLmtMetricShouldIncrementMetric() { metrics.updatePrivacyLmtMetric(); // then - assertThat(metricRegistry.counter("privacy.lmt").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.lmt").getCount()).isOne(); } @Test @@ -714,8 +861,8 @@ public void updatePrivacyCcpaMetricsShouldIncrementMetrics() { metrics.updatePrivacyCcpaMetrics(true, true); // then - assertThat(metricRegistry.counter("privacy.usp.specified").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("privacy.usp.opt-out").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.usp.specified").getCount()).isOne(); + assertThat(metricRegistry.counter("privacy.usp.opt-out").getCount()).isOne(); } @Test @@ -724,7 +871,7 @@ public void updatePrivacyTcfMissingMetricShouldIncrementMetric() { metrics.updatePrivacyTcfMissingMetric(); // then - assertThat(metricRegistry.counter("privacy.tcf.missing").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.missing").getCount()).isOne(); } @Test @@ -733,7 +880,16 @@ public void updatePrivacyTcfInvalidMetricShouldIncrementMetric() { metrics.updatePrivacyTcfInvalidMetric(); // then - assertThat(metricRegistry.counter("privacy.tcf.invalid").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.invalid").getCount()).isOne(); + } + + @Test + public void updatePrivacyTcfRequestsMetricShouldIncrementMetric() { + // when + metrics.updatePrivacyTcfRequestsMetric(1); + + // then + assertThat(metricRegistry.counter("privacy.tcf.v1.requests").getCount()).isOne(); } @Test @@ -744,9 +900,9 @@ public void updatePrivacyTcfGeoMetricShouldIncrementMetrics() { metrics.updatePrivacyTcfGeoMetric(2, false); // then - assertThat(metricRegistry.counter("privacy.tcf.v1.unknown-geo").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("privacy.tcf.v2.in-geo").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("privacy.tcf.v2.out-geo").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.v1.unknown-geo").getCount()).isOne(); + assertThat(metricRegistry.counter("privacy.tcf.v2.in-geo").getCount()).isOne(); + assertThat(metricRegistry.counter("privacy.tcf.v2.out-geo").getCount()).isOne(); } @Test @@ -755,7 +911,7 @@ public void updatePrivacyTcfVendorListMissingMetricShouldIncrementMetric() { metrics.updatePrivacyTcfVendorListMissingMetric(1); // then - assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.missing").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.missing").getCount()).isOne(); } @Test @@ -764,7 +920,7 @@ public void updatePrivacyTcfVendorListOkMetricShouldIncrementMetric() { metrics.updatePrivacyTcfVendorListOkMetric(1); // then - assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.ok").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.ok").getCount()).isOne(); } @Test @@ -773,7 +929,7 @@ public void updatePrivacyTcfVendorListErrorMetricShouldIncrementMetric() { metrics.updatePrivacyTcfVendorListErrorMetric(1); // then - assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("privacy.tcf.v1.vendorlist.err").getCount()).isOne(); } @Test @@ -798,13 +954,13 @@ public void shouldNotUpdateAccountMetricsIfVerbosityIsNone() { metrics.updateAdapterBidMetrics(RUBICON, ACCOUNT_ID, 1234L, true, "banner"); // then - assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isEqualTo(0); - assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isEqualTo(0); - assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isEqualTo(0); + assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isZero(); + assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isZero(); + assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isZero(); } @Test @@ -820,13 +976,13 @@ public void shouldUpdateAccountRequestsMetricOnlyIfVerbosityIsBasic() { metrics.updateAdapterBidMetrics(RUBICON, ACCOUNT_ID, 1234L, true, "banner"); // then - assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isEqualTo(0); - assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isEqualTo(0); - assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isEqualTo(0); - assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isEqualTo(0); + assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isZero(); + assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.requests.gotbids").getCount()).isZero(); + assertThat(metricRegistry.histogram("account.accountId.rubicon.prices").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.rubicon.bids_received").getCount()).isZero(); } @Test @@ -835,7 +991,7 @@ public void shouldIncrementConnectionAcceptErrorsMetric() { metrics.updateConnectionAcceptErrors(); // then - assertThat(metricRegistry.counter("connection_accept_errors").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("connection_accept_errors").getCount()).isOne(); } @Test @@ -844,61 +1000,44 @@ public void shouldUpdateDatabaseQueryTimeMetric() { metrics.updateDatabaseQueryTimeMetric(456L); // then - assertThat(metricRegistry.timer("db_query_time").getCount()).isEqualTo(1); - } - - @Test - public void shouldIncrementDatabaseCircuitBreakerOpenMetric() { - // when - metrics.updateDatabaseCircuitBreakerMetric(true); - - // then - assertThat(metricRegistry.counter("db_circuitbreaker_opened").getCount()).isEqualTo(1); - } - - @Test - public void shouldIncrementDatabaseCircuitBreakerCloseMetric() { - // when - metrics.updateDatabaseCircuitBreakerMetric(false); - - // then - assertThat(metricRegistry.counter("db_circuitbreaker_closed").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("db_query_time").getCount()).isOne(); } @Test - public void shouldIncrementHttpClientCircuitBreakerOpenMetric() { + public void shouldCreateDatabaseCircuitBreakerGaugeMetric() { // when - metrics.updateHttpClientCircuitBreakerMetric("id", true); + metrics.createDatabaseCircuitBreakerGauge(() -> true); // then - assertThat(metricRegistry.counter("httpclient_circuitbreaker_opened.id").getCount()).isEqualTo(1); + assertThat(metricRegistry.gauge("circuit-breaker.db.opened.count", () -> null).getValue()).isEqualTo(1L); } @Test - public void shouldIncrementHttpClientCircuitBreakerCloseMetric() { + public void shouldCreateHttpClientCircuitBreakerGaugeMetric() { // when - metrics.updateHttpClientCircuitBreakerMetric("id", false); + metrics.createHttpClientCircuitBreakerGauge("id", () -> true); // then - assertThat(metricRegistry.counter("httpclient_circuitbreaker_closed.id").getCount()).isEqualTo(1); + assertThat(metricRegistry.gauge("circuit-breaker.http.named.id.opened.count", () -> null).getValue()) + .isEqualTo(1L); } @Test - public void shouldIncrementGeoLocationCircuitBreakerOpenMetric() { + public void shouldCreateHttpClientCircuitBreakerNumberGaugeMetric() { // when - metrics.updateGeoLocationCircuitBreakerMetric(true); + metrics.createHttpClientCircuitBreakerNumberGauge(() -> 1); // then - assertThat(metricRegistry.counter("geolocation_circuitbreaker_opened").getCount()).isEqualTo(1); + assertThat(metricRegistry.gauge("circuit-breaker.http.existing.count", () -> null).getValue()).isEqualTo(1L); } @Test - public void shouldIncrementGeoLocationCircuitBreakerCloseMetric() { + public void shouldCreateGeoLocationCircuitBreakerGaugeMetric() { // when - metrics.updateGeoLocationCircuitBreakerMetric(false); + metrics.createGeoLocationCircuitBreakerGauge(() -> true); // then - assertThat(metricRegistry.counter("geolocation_circuitbreaker_closed").getCount()).isEqualTo(1); + assertThat(metricRegistry.gauge("circuit-breaker.geo.opened.count", () -> null).getValue()).isEqualTo(1L); } @Test @@ -907,8 +1046,8 @@ public void shouldIncrementBothGeoLocationRequestsAndSuccessfulMetrics() { metrics.updateGeoLocationMetric(true); // then - assertThat(metricRegistry.counter("geolocation_requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("geolocation_successful").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("geolocation_requests").getCount()).isOne(); + assertThat(metricRegistry.counter("geolocation_successful").getCount()).isOne(); } @Test @@ -917,8 +1056,8 @@ public void shouldIncrementBothGeoLocationRequestsAndFailMetrics() { metrics.updateGeoLocationMetric(false); // then - assertThat(metricRegistry.counter("geolocation_requests").getCount()).isEqualTo(1); - assertThat(metricRegistry.counter("geolocation_fail").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("geolocation_requests").getCount()).isOne(); + assertThat(metricRegistry.counter("geolocation_fail").getCount()).isOne(); } @Test @@ -929,9 +1068,9 @@ public void shouldAlwaysIncrementGeoLocationRequestsMetricAndEitherSuccessfulOrF metrics.updateGeoLocationMetric(true); // then - assertThat(metricRegistry.counter("geolocation_requests").getCount()).isEqualTo(3); - assertThat(metricRegistry.counter("geolocation_fail").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("geolocation_fail").getCount()).isOne(); assertThat(metricRegistry.counter("geolocation_successful").getCount()).isEqualTo(2); + assertThat(metricRegistry.counter("geolocation_requests").getCount()).isEqualTo(3); } @Test @@ -940,7 +1079,7 @@ public void shouldIncrementStoredRequestFoundMetric() { metrics.updateStoredRequestMetric(true); // then - assertThat(metricRegistry.counter("stored_requests_found").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("stored_requests_found").getCount()).isOne(); } @Test @@ -949,7 +1088,7 @@ public void shouldIncrementStoredRequestMissingMetric() { metrics.updateStoredRequestMetric(false); // then - assertThat(metricRegistry.counter("stored_requests_missing").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("stored_requests_missing").getCount()).isOne(); } @Test @@ -958,7 +1097,7 @@ public void shouldIncrementStoredImpFoundMetric() { metrics.updateStoredImpsMetric(true); // then - assertThat(metricRegistry.counter("stored_imps_found").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("stored_imps_found").getCount()).isOne(); } @Test @@ -967,7 +1106,7 @@ public void shouldIncrementStoredImpMissingMetric() { metrics.updateStoredImpsMetric(false); // then - assertThat(metricRegistry.counter("stored_imps_missing").getCount()).isEqualTo(1); + assertThat(metricRegistry.counter("stored_imps_missing").getCount()).isOne(); } @Test @@ -977,7 +1116,7 @@ public void shouldIncrementPrebidCacheRequestSuccessTimer() { // then assertThat(metricRegistry.timer("prebid_cache.requests.ok").getCount()).isEqualTo(1); - assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.ok").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.ok").getCount()).isOne(); } @Test @@ -987,7 +1126,7 @@ public void shouldIncrementPrebidCacheRequestFailedTimer() { // then assertThat(metricRegistry.timer("prebid_cache.requests.err").getCount()).isEqualTo(1); - assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.err").getCount()).isOne(); } @Test @@ -1001,6 +1140,212 @@ public void shouldIncrementPrebidCacheCreativeSizeHistogram() { .isEqualTo(1); } + @Test + public void shouldCreateCurrencyRatesGaugeMetric() { + // when + metrics.createCurrencyRatesGauge(() -> true); + + // then + assertThat(metricRegistry.gauge("currency-rates.stale.count", () -> null).getValue()).isEqualTo(1L); + } + + @Test + public void updateSettingsCacheRefreshTimeShouldUpdateTimer() { + // when + metrics.updateSettingsCacheRefreshTime(MetricName.stored_request, MetricName.initialize, 123L); + + // then + assertThat(metricRegistry + .timer("settings.cache.stored-request.refresh.initialize.db_query_time") + .getCount()) + .isEqualTo(1); + } + + @Test + public void updateSettingsCacheRefreshErrorMetricShouldIncrementMetric() { + // when + metrics.updateSettingsCacheRefreshErrorMetric(MetricName.stored_request, MetricName.initialize); + + // then + assertThat(metricRegistry.counter("settings.cache.stored-request.refresh.initialize.err").getCount()) + .isEqualTo(1); + } + + @Test + public void updateSettingsCacheEventMetricShouldIncrementMetric() { + // when + metrics.updateSettingsCacheEventMetric(MetricName.account, MetricName.hit); + + // then + assertThat(metricRegistry.counter("settings.cache.account.hit").getCount()).isEqualTo(1); + } + + @Test + public void updateHooksMetricsShouldIncrementMetrics() { + // when + metrics.updateHooksMetrics( + "module1", Stage.entrypoint, "hook1", ExecutionStatus.success, 5L, ExecutionAction.update); + metrics.updateHooksMetrics( + "module1", Stage.raw_auction_request, "hook2", ExecutionStatus.success, 5L, ExecutionAction.no_action); + metrics.updateHooksMetrics( + "module1", + Stage.processed_auction_request, + "hook3", + ExecutionStatus.success, + 5L, + ExecutionAction.reject); + metrics.updateHooksMetrics( + "module2", Stage.bidder_request, "hook1", ExecutionStatus.failure, 6L, null); + metrics.updateHooksMetrics( + "module2", Stage.raw_bidder_response, "hook2", ExecutionStatus.timeout, 7L, null); + metrics.updateHooksMetrics( + "module2", Stage.processed_bidder_response, "hook3", ExecutionStatus.execution_failure, 5L, null); + metrics.updateHooksMetrics( + "module2", Stage.auction_response, "hook4", ExecutionStatus.invocation_failure, 5L, null); + + // then + assertThat(metricRegistry.counter("modules.module.module1.stage.entrypoint.hook.hook1.call") + .getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module1.stage.entrypoint.hook.hook1.success.update") + .getCount()) + .isEqualTo(1); + assertThat(metricRegistry.timer("modules.module.module1.stage.entrypoint.hook.hook1.duration").getCount()) + .isEqualTo(1); + + assertThat(metricRegistry.counter("modules.module.module1.stage.rawauction.hook.hook2.call").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module1.stage.rawauction.hook.hook2.success.noop").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.timer("modules.module.module1.stage.rawauction.hook.hook2.duration").getCount()) + .isEqualTo(1); + + assertThat(metricRegistry.counter("modules.module.module1.stage.procauction.hook.hook3.call").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module1.stage.procauction.hook.hook3.success.reject") + .getCount()) + .isEqualTo(1); + assertThat(metricRegistry.timer("modules.module.module1.stage.procauction.hook.hook3.duration").getCount()) + .isEqualTo(1); + + assertThat(metricRegistry.counter("modules.module.module2.stage.bidrequest.hook.hook1.call").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module2.stage.bidrequest.hook.hook1.failure").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.timer("modules.module.module2.stage.bidrequest.hook.hook1.duration").getCount()) + .isEqualTo(1); + + assertThat(metricRegistry.counter("modules.module.module2.stage.rawbidresponse.hook.hook2.call").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module2.stage.rawbidresponse.hook.hook2.timeout").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.timer("modules.module.module2.stage.rawbidresponse.hook.hook2.duration").getCount()) + .isEqualTo(1); + + assertThat(metricRegistry.counter("modules.module.module2.stage.procbidresponse.hook.hook3.call").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module2.stage.procbidresponse.hook.hook3.execution-error") + .getCount()) + .isEqualTo(1); + assertThat(metricRegistry.timer("modules.module.module2.stage.procbidresponse.hook.hook3.duration").getCount()) + .isEqualTo(1); + + assertThat(metricRegistry.counter("modules.module.module2.stage.auctionresponse.hook.hook4.call").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module2.stage.auctionresponse.hook.hook4.execution-error") + .getCount()) + .isEqualTo(1); + assertThat(metricRegistry.timer("modules.module.module2.stage.auctionresponse.hook.hook4.duration").getCount()) + .isEqualTo(1); + } + + @Test + public void updateAccountHooksMetricsShouldIncrementMetricsIfVerbosityIsDetailed() { + // given + given(accountMetricsVerbosity.forAccount(anyString())).willReturn(AccountMetricsVerbosityLevel.detailed); + + // when + metrics.updateAccountHooksMetrics( + "accountId", "module1", ExecutionStatus.success, ExecutionAction.update); + metrics.updateAccountHooksMetrics( + "accountId", "module2", ExecutionStatus.failure, null); + metrics.updateAccountHooksMetrics( + "accountId", "module3", ExecutionStatus.timeout, null); + + // then + assertThat(metricRegistry.counter("account.accountId.modules.module.module1.call").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.modules.module.module1.success.update").getCount()) + .isEqualTo(1); + + assertThat(metricRegistry.counter("account.accountId.modules.module.module2.call").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.modules.module.module2.failure").getCount()) + .isEqualTo(1); + + assertThat(metricRegistry.counter("account.accountId.modules.module.module3.call").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("account.accountId.modules.module.module3.failure").getCount()) + .isEqualTo(1); + } + + @Test + public void updateAccountHooksMetricsShouldNotIncrementMetricsIfVerbosityIsNotAtLeastDetailed() { + // given + given(accountMetricsVerbosity.forAccount(anyString())).willReturn(AccountMetricsVerbosityLevel.basic); + + // when + metrics.updateAccountHooksMetrics( + "accountId", "module1", ExecutionStatus.success, ExecutionAction.update); + + // then + assertThat(metricRegistry.counter("account.accountId.modules.module.module1.call").getCount()) + .isZero(); + assertThat(metricRegistry.counter("account.accountId.modules.module.module1.success.update").getCount()) + .isZero(); + } + + @Test + public void updateAccountModuleDurationMetricShouldIncrementMetricsIfVerbosityIsDetailed() { + // given + given(accountMetricsVerbosity.forAccount(anyString())).willReturn(AccountMetricsVerbosityLevel.detailed); + + // when + metrics.updateAccountModuleDurationMetric( + "accountId", "module1", 5L); + metrics.updateAccountModuleDurationMetric( + "accountId", "module2", 6L); + + // then + assertThat(metricRegistry.timer("account.accountId.modules.module.module1.duration").getCount()) + .isEqualTo(1); + assertThat(metricRegistry.timer("account.accountId.modules.module.module2.duration").getCount()) + .isEqualTo(1); + } + + @Test + public void updateAccountModuleDurationMetricShouldNotIncrementMetricsIfVerbosityIsNotAtLeastDetailed() { + // given + given(accountMetricsVerbosity.forAccount(anyString())).willReturn(AccountMetricsVerbosityLevel.basic); + + // when + metrics.updateAccountModuleDurationMetric( + "accountId", "module1", 5L); + + // then + assertThat(metricRegistry.timer("account.accountId.modules.module.module1.duration").getCount()) + .isZero(); + } + + @Test + public void shouldIncrementWinNotificationMetric() { + // when + metrics.updateWinNotificationMetric(); + + // then + assertThat(metricRegistry.counter("win_notifications").getCount()).isEqualTo(1); + } + private void verifyCreatesConfiguredCounterType(Consumer metricsConsumer) { final EnumMap> counterTypeClasses = new EnumMap<>(CounterType.class); counterTypeClasses.put(CounterType.counter, Counter.class); @@ -1015,7 +1360,7 @@ private void verifyCreatesConfiguredCounterType(Consumer metricsConsume // when metricsConsumer.accept(new Metrics(metricRegistry, CounterType.valueOf(counterType.name()), - accountMetricsVerbosity, bidderCatalog)); + accountMetricsVerbosity)); // then softly.assertThat(metricRegistry.getMetrics()).hasValueSatisfying(new Condition<>( diff --git a/src/test/java/org/prebid/server/metric/UpdatableMetricsTest.java b/src/test/java/org/prebid/server/metric/UpdatableMetricsTest.java index 13c467ad183..4270648b08c 100644 --- a/src/test/java/org/prebid/server/metric/UpdatableMetricsTest.java +++ b/src/test/java/org/prebid/server/metric/UpdatableMetricsTest.java @@ -144,6 +144,49 @@ public void updateHistogramShouldCreateMetricNameOnlyOnceOnSuccessiveCalls() { verify(nameCreator).apply(eq(MetricName.prices)); } + @Test + public void createGaugeShouldCreateMetricNameUsingProvidedCreator() { + // given + updatableMetrics = new UpdatableMetrics(metricRegistry, CounterType.counter, + metricName -> "someprefix." + metricName.toString()); + + // when + updatableMetrics.createGauge(MetricName.opened, () -> 1); + + // then + assertThat(metricRegistry.gauge("someprefix.opened", () -> null).getValue()).isEqualTo(1L); + } + + @SuppressWarnings("unchecked") + @Test + public void createGaugeShouldCreateMetricNameOnlyOnceOnSuccessiveCalls() { + // given + final Function nameCreator = mock(Function.class); + given(nameCreator.apply(any())).willReturn(""); + + updatableMetrics = new UpdatableMetrics(metricRegistry, CounterType.counter, nameCreator); + + // when + updatableMetrics.createGauge(MetricName.opened, () -> 1); + updatableMetrics.createGauge(MetricName.opened, () -> 1); + + // then + verify(nameCreator).apply(eq(MetricName.opened)); + } + + @Test + public void removeMetricShouldRemoveExistingMetric() { + // given + updatableMetrics = new UpdatableMetrics(metricRegistry, CounterType.counter, MetricName::toString); + + // when + updatableMetrics.createGauge(MetricName.opened, () -> 1); + updatableMetrics.removeMetric(MetricName.opened); + + // then + assertThat(metricRegistry.getGauges()).doesNotContainKey("opened"); + } + private UpdatableMetrics givenUpdatableMetricsWith(CounterType counterType) { return new UpdatableMetrics(metricRegistry, counterType, MetricName::toString); } diff --git a/src/test/java/org/prebid/server/model/CaseInsensitiveMultiMapTest.java b/src/test/java/org/prebid/server/model/CaseInsensitiveMultiMapTest.java new file mode 100644 index 00000000000..dc6494f4d8f --- /dev/null +++ b/src/test/java/org/prebid/server/model/CaseInsensitiveMultiMapTest.java @@ -0,0 +1,60 @@ +package org.prebid.server.model; + +import org.junit.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CaseInsensitiveMultiMapTest { + + @Test + public void shouldReturnMultipleValuesWhenAddedIndividuallyWithDifferentCase() { + // when + final CaseInsensitiveMultiMap map = CaseInsensitiveMultiMap.builder() + .add("Key1", "value11") + .add("kEy2", "value21") + .add("keY1", "value12") + .add("KEy2", "value22") + .build(); + + // then + assertThat(map.getAll("key1")).containsOnly("value11", "value12"); + assertThat(map.getAll("key2")).containsOnly("value21", "value22"); + } + + @Test + public void shouldReturnMultipleValuesWhenAddedInBulkAndIndividually() { + // when + final CaseInsensitiveMultiMap map = CaseInsensitiveMultiMap.builder() + .add("key1", "value11") + .add("key1", Arrays.asList("value12", "value13")) + .build(); + + // then + assertThat(map.getAll("key1")).containsOnly("value11", "value12", "value13"); + } + + @Test + public void shouldReturnMultipleValuesWhenAddedUsingAddAll() { + // given + final CaseInsensitiveMultiMap anotherMap = CaseInsensitiveMultiMap.builder() + .add("key1", "value13") + .add("key1", "value14") + .add("key3", "value31") + .build(); + + // when + final CaseInsensitiveMultiMap map = CaseInsensitiveMultiMap.builder() + .add("key1", "value11") + .add("key1", "value12") + .add("key2", "value21") + .addAll(anotherMap) + .build(); + + // then + assertThat(map.getAll("key1")).containsOnly("value11", "value12", "value13", "value14"); + assertThat(map.getAll("key2")).containsOnly("value21"); + assertThat(map.getAll("key3")).containsOnly("value31"); + } +} diff --git a/src/test/java/org/prebid/server/privacy/PrivacyExtractorTest.java b/src/test/java/org/prebid/server/privacy/PrivacyExtractorTest.java index 9fec35b41de..6199d71f6fb 100644 --- a/src/test/java/org/prebid/server/privacy/PrivacyExtractorTest.java +++ b/src/test/java/org/prebid/server/privacy/PrivacyExtractorTest.java @@ -15,7 +15,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.request.CookieSyncRequest; -import org.prebid.server.proto.request.PreBidRequest; + +import java.util.ArrayList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; @@ -37,7 +38,8 @@ public void setUp() { @Test public void shouldReturnGdprEmptyValueWhenRegsIsNull() { // given and when - final String gdpr = privacyExtractor.validPrivacyFrom(BidRequest.builder().build()).getGdpr(); + final String gdpr = + privacyExtractor.validPrivacyFrom(BidRequest.builder().build(), new ArrayList<>()).getGdpr(); // then assertThat(gdpr).isEmpty(); @@ -47,7 +49,7 @@ public void shouldReturnGdprEmptyValueWhenRegsIsNull() { public void shouldReturnGdprEmptyValueWhenRegsExtIsNull() { // given and when final String gdpr = privacyExtractor.validPrivacyFrom( - BidRequest.builder().regs(Regs.of(null, null)).build()) + BidRequest.builder().regs(Regs.of(null, null)).build(), new ArrayList<>()) .getGdpr(); // then @@ -60,7 +62,8 @@ public void shouldReturnGdprEmptyValueWhenRegsExtGdprIsNoEqualsToOneOrZero() { final Regs regs = Regs.of(null, ExtRegs.of(2, null)); // when - final String gdpr = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getGdpr(); + final String gdpr = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), new ArrayList<>()).getGdpr(); // then assertThat(gdpr).isEmpty(); @@ -72,7 +75,8 @@ public void shouldReturnGdprOneWhenExtRegsContainsGdprOne() { final Regs regs = Regs.of(null, ExtRegs.of(1, null)); // when - final String gdpr = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getGdpr(); + final String gdpr = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), new ArrayList<>()).getGdpr(); // then assertThat(gdpr).isEqualTo("1"); @@ -84,7 +88,8 @@ public void shouldReturnGdprZeroWhenExtRegsContainsGdprZero() { final Regs regs = Regs.of(null, ExtRegs.of(0, null)); // when - final String gdpr = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getGdpr(); + final String gdpr = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), new ArrayList<>()).getGdpr(); // then assertThat(gdpr).isEqualTo("0"); @@ -93,7 +98,8 @@ public void shouldReturnGdprZeroWhenExtRegsContainsGdprZero() { @Test public void shouldReturnConsentEmptyValueWhenExtUserIsNull() { // given and when - final String consent = privacyExtractor.validPrivacyFrom(BidRequest.builder().build()).getConsentString(); + final String consent = privacyExtractor.validPrivacyFrom(BidRequest.builder().build(), new ArrayList<>()) + .getConsentString(); // then assertThat(consent).isEmpty(); @@ -105,8 +111,9 @@ public void shouldReturnConsentEmptyValueWhenUserConsentIsNull() { final User user = User.builder().ext(ExtUser.builder().build()).build(); // when - final String consent = privacyExtractor.validPrivacyFrom(BidRequest.builder().user(user).build()) - .getConsentString(); + final String consent = + privacyExtractor.validPrivacyFrom(BidRequest.builder().user(user).build(), new ArrayList<>()) + .getConsentString(); // then assertThat(consent).isEmpty(); @@ -118,23 +125,28 @@ public void shouldReturnConsentWhenUserContainsConsent() { final User user = User.builder().ext(ExtUser.builder().consent("consent").build()).build(); // when - final String consent = privacyExtractor.validPrivacyFrom(BidRequest.builder().user(user).build()) - .getConsentString(); + final String consent = + privacyExtractor.validPrivacyFrom(BidRequest.builder().user(user).build(), new ArrayList<>()) + .getConsentString(); // then assertThat(consent).isEqualTo("consent"); } @Test - public void shouldReturnDefaultCcpaIfNotValid() { + public void shouldReturnDefaultCcpaWhenNotValidAndAddError() { // given final Regs regs = Regs.of(null, ExtRegs.of(null, "invalid")); + final ArrayList errors = new ArrayList<>(); // when - final Ccpa ccpa = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getCcpa(); + final Ccpa ccpa = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), errors).getCcpa(); // then assertThat(ccpa).isEqualTo(Ccpa.EMPTY); + assertThat(errors).containsOnly( + "CCPA consent invalid has invalid format: us_privacy must contain 4 characters"); } @Test @@ -143,7 +155,9 @@ public void shouldReturnDefaultCoppaIfNull() { final Regs regs = Regs.of(null, null); // when - final Integer coppa = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getCoppa(); + final Integer coppa = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), new ArrayList<>()) + .getCoppa(); // then assertThat(coppa).isZero(); @@ -155,7 +169,9 @@ public void shouldReturnCoppaIfNotNull() { final Regs regs = Regs.of(42, null); // when - final Integer coppa = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build()).getCoppa(); + final Integer coppa = + privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).build(), new ArrayList<>()) + .getCoppa(); // then assertThat(coppa).isEqualTo(42); @@ -168,21 +184,8 @@ public void shouldReturnPrivacyWithParametersExtractedFromBidRequest() { final User user = User.builder().ext(ExtUser.builder().consent("consent").build()).build(); // when - final Privacy privacy = privacyExtractor.validPrivacyFrom(BidRequest.builder().regs(regs).user(user).build()); - - // then - assertThat(privacy).isEqualTo(Privacy.of("0", "consent", Ccpa.of("1Yn-"), 0)); - } - - @Test - public void shouldReturnPrivacyWithParametersExtractedFromPreBidRequest() { - // given - final Regs regs = Regs.of(null, ExtRegs.of(0, "1Yn-")); - final User user = User.builder().ext(ExtUser.builder().consent("consent").build()).build(); - - // when - final Privacy privacy = privacyExtractor.validPrivacyFrom( - PreBidRequest.builder().regs(regs).user(user).build()); + final Privacy privacy = privacyExtractor + .validPrivacyFrom(BidRequest.builder().regs(regs).user(user).build(), new ArrayList<>()); // then assertThat(privacy).isEqualTo(Privacy.of("0", "consent", Ccpa.of("1Yn-"), 0)); diff --git a/src/test/java/org/prebid/server/privacy/gdpr/GdprServiceTest.java b/src/test/java/org/prebid/server/privacy/gdpr/GdprServiceTest.java deleted file mode 100644 index 2a7011e2ea1..00000000000 --- a/src/test/java/org/prebid/server/privacy/gdpr/GdprServiceTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package org.prebid.server.privacy.gdpr; - -import io.vertx.core.Future; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.prebid.server.VertxTest; -import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; -import org.prebid.server.privacy.gdpr.model.VendorPermission; -import org.prebid.server.privacy.gdpr.vendorlist.VendorListService; -import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorListV1; -import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV1; - -import static java.util.Collections.emptyMap; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.BDDMockito.given; -import static org.prebid.server.assertion.FutureAssertion.assertThat; - -public class GdprServiceTest extends VertxTest { - - @Rule - public final MockitoRule mockitoRule = MockitoJUnit.rule(); - - @Mock - private VendorListService vendorListService; - - private GdprService gdprService; - - @Before - public void setUp() { - gdprService = new GdprService(vendorListService); - } - - @Test - public void shouldReturnAllDeniedWhenNoConsentParam() { - assertThat(gdprService.resultFor(singleton(1), null)) - .succeededWith(singletonList(VendorPermission.of(1, null, denyAll()))); - } - - @Test - public void shouldReturnAllDeniedWhenConsentParamIsInvalid() { - assertThat(gdprService.resultFor(singleton(1), "invalid-consent")) - .succeededWith(singletonList(VendorPermission.of(1, null, denyAll()))); - } - - @Test - public void shouldReturnAllDeniedWhenVendorListIsNotFetchedYet() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.failedFuture("Not fetched yet")); - - // when and then - assertThat(gdprService.resultFor(singleton(9), "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA")) - .succeededWith(singletonList(VendorPermission.of(9, null, denyAll()))); - } - - @Test - public void shouldReturnAllDeniedWhenVendorIsNotAllowed() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.succeededFuture(emptyMap())); - - // when and then - assertThat(gdprService.resultFor(singleton(9), "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA")) - .succeededWith(singletonList(VendorPermission.of(9, null, denyAll()))); - } - - @Test - public void shouldReturnAllDeniedWhenVendorIsNotInVendorList() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.succeededFuture(emptyMap())); - - // when and then - assertThat(gdprService.resultFor(singleton(1), "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA")) - .succeededWith(singletonList(VendorPermission.of(1, null, denyAll()))); - } - - @Test - public void shouldReturnAllDeniedWhenAllClaimedPurposesAreNotAllowed() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.succeededFuture( - singletonMap(1, VendorV1.of(1, singleton(4), singleton(5))))); - - // when and then - assertThat(gdprService.resultFor(singleton(1), "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA")) - .succeededWith(singletonList(VendorPermission.of(1, null, denyAll()))); - } - - @Test - public void shouldReturnPrivateInfoAllowedUserSyncDeniedWhenAllClaimedPurposesAreAllowed() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.succeededFuture( - singletonMap(1, VendorV1.of(1, singleton(2), singleton(3))))); - - // when and then - assertThat(gdprService.resultFor(singleton(1), "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA")) - .succeededWith(singletonList(VendorPermission.of(1, null, action(true, false)))); - } - - @Test - public void shouldReturnAllAllowedWhenAllClaimedPurposesAreAllowedIncludingPurposeOne() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.succeededFuture( - singletonMap(1, VendorV1.of(1, singleton(1), singleton(2))))); - - // when and then - assertThat(gdprService.resultFor(singleton(1), "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA")) - .succeededWith(singletonList(VendorPermission.of(1, null, action(true, true)))); - } - - @Test - public void shouldReturnPrivateInfoDeniedUserSyncAllowedWhenNotAllClaimedPurposesAreAllowedButPurposeOneIs() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.succeededFuture( - singletonMap(1, VendorV1.of(1, singleton(1), singleton(4))))); - - // when and then - assertThat(gdprService.resultFor(singleton(1), "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA")) - .succeededWith(singletonList(VendorPermission.of(1, null, action(false, true)))); - } - - @Test - public void shouldReturnUserSyncDeniedWhenPurposeOneIsAllowedButNotClaimed() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.succeededFuture( - singletonMap(1, VendorV1.of(1, singleton(2), singleton(3))))); - - // when and then - assertThat(gdprService.resultFor(singleton(1), "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA")) - .succeededWith(singletonList(VendorPermission.of(1, null, action(true, false)))); - } - - @Test - public void shouldReturnFailedFutureWhenGettingAllowedPurposesFails() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.succeededFuture(emptyMap())); - - // when and then - assertThat(gdprService.resultFor(singleton(1), "BONciguONcjGKADACHENAOLS1r")) - .isFailed() - .hasMessage("Error when retrieving allowed purpose ids in a reason of invalid consent string"); - } - - @Test - public void shouldReturnFailedFutureWhenGettingIfVendorAllowedFails() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.succeededFuture(emptyMap())); - - // when and then - assertThat(gdprService.resultFor(singleton(1), "BOSbaBZOSbaBoABABBENBcoAAAAgSABgBAA")) - .isFailed() - .hasMessage("Error when checking if vendor is allowed in a reason of invalid consent string"); - } - - @Test - public void shouldReturnAllDeniedWhenVendorListServiceFailed() { - // given - given(vendorListService.forVersion(anyInt())).willReturn(Future.failedFuture("error")); - - // when and then - assertThat(gdprService.resultFor(singleton(1), "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA")) - .succeededWith(singletonList(VendorPermission.of(1, null, denyAll()))); - } - - private static PrivacyEnforcementAction denyAll() { - return action(false, false); - } - - private static PrivacyEnforcementAction action(boolean allowPrivateInfo, boolean allowUserSync) { - return PrivacyEnforcementAction.builder() - .removeUserIds(!allowPrivateInfo) - .maskGeo(!allowPrivateInfo) - .maskDeviceIp(!allowPrivateInfo) - .maskDeviceInfo(!allowPrivateInfo) - .blockAnalyticsReport(false) - .blockBidderRequest(false) - .blockPixelSync(!allowUserSync) - .build(); - } -} diff --git a/src/test/java/org/prebid/server/privacy/gdpr/Tcf2ServiceTest.java b/src/test/java/org/prebid/server/privacy/gdpr/Tcf2ServiceTest.java index 4b98d048a54..c080f277659 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/Tcf2ServiceTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/Tcf2ServiceTest.java @@ -48,6 +48,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.prebid.server.assertion.FutureAssertion.assertThat; +import static org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode.FOUR; +import static org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode.ONE; +import static org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode.SEVEN; +import static org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode.TWO; public class Tcf2ServiceTest extends VertxTest { @@ -80,6 +84,11 @@ public class Tcf2ServiceTest extends VertxTest { private Purpose purpose4; private Purpose purpose7; + private Purpose weakPurpose1; + private Purpose weakPurpose2; + private Purpose weakPurpose4; + private Purpose weakPurpose7; + private SpecialFeature specialFeature1; private SpecialFeatures specialFeatures; @@ -91,10 +100,10 @@ public class Tcf2ServiceTest extends VertxTest { @Before public void setUp() { given(tcString.getVendorListVersion()).willReturn(10); - given(purposeStrategyOne.getPurposeId()).willReturn(1); - given(purposeStrategyTwo.getPurposeId()).willReturn(2); - given(purposeStrategyFour.getPurposeId()).willReturn(4); - given(purposeStrategySeven.getPurposeId()).willReturn(7); + given(purposeStrategyOne.getPurpose()).willReturn(ONE); + given(purposeStrategyTwo.getPurpose()).willReturn(TWO); + given(purposeStrategyFour.getPurpose()).willReturn(FOUR); + given(purposeStrategySeven.getPurpose()).willReturn(SEVEN); purposeStrategies = asList(purposeStrategyOne, purposeStrategyTwo, purposeStrategyFour, purposeStrategySeven); given(specialFeaturesStrategyOne.getSpecialFeatureId()).willReturn(1); @@ -120,6 +129,11 @@ private void initPurposes() { .p4(purpose4) .p7(purpose7) .build(); + + weakPurpose1 = Purpose.of(EnforcePurpose.basic, false, emptyList()); + weakPurpose2 = Purpose.of(EnforcePurpose.no, false, emptyList()); + weakPurpose4 = Purpose.of(EnforcePurpose.no, false, emptyList()); + weakPurpose7 = Purpose.of(EnforcePurpose.basic, false, emptyList()); } private void initSpecialFeatures() { @@ -256,6 +270,41 @@ public void permissionsForShouldMergeAccountSpecialFeatures() { singletonList(expectedVendorPermission)); } + @Test + public void permissionsForShouldSplitIntoWeakPurposesWhenAccountHaveBasicEnforcementBidders() { + // given + final VendorIdResolver vendorIdResolver = mock(VendorIdResolver.class); + given(vendorIdResolver.resolve(eq("b1"))).willReturn(1); + given(vendorIdResolver.resolve(eq("b2"))).willReturn(2); + + final AccountGdprConfig accountGdprConfig = AccountGdprConfig.builder() + .basicEnforcementVendors(singletonList("b2")) + .build(); + + // when + final Future> result = + target.permissionsFor(new HashSet<>(asList("b1", "b2")), vendorIdResolver, tcString, accountGdprConfig); + + // then + final VendorPermission expectedVendorPermission1 = + VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); + final VendorPermission expectedVendorPermission2 = + VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); + final VendorPermissionWithGvl expectedVendorPermissionWitGvl1 = + VendorPermissionWithGvl.of(expectedVendorPermission1, VendorV2.empty(1)); + final VendorPermissionWithGvl expectedVendorPermissionWitGvl2 = + VendorPermissionWithGvl.of(expectedVendorPermission2, VendorV2.empty(2)); + + verifyEachPurposeStrategyReceive(singletonList(expectedVendorPermissionWitGvl1)); + verifyEachPurposeStrategyReceiveWeak(singletonList(expectedVendorPermissionWitGvl2)); + verifyEachSpecialFeatureStrategyReceive(asList(expectedVendorPermission2, expectedVendorPermission1)); + + verify(vendorIdResolver, times(2)).resolve(anyString()); + verify(tcString).getVendorListVersion(); + + assertThat(result).succeededWith(asList(expectedVendorPermission2, expectedVendorPermission1)); + } + @Test public void permissionsForShouldReturnBidderNamesResult() { // given @@ -334,10 +383,20 @@ public void permissionsForShouldReturnAllDeniedWhenP1TIIsNoAccessAllowed() { assertThat(result).succeededWith( singletonList(VendorPermission.of(1, "rubicon", PrivacyEnforcementAction.restrictAll()))); + final VendorPermission expectedVendorPermission1 = VendorPermission.of(1, "rubicon", + PrivacyEnforcementAction.restrictAll()); + final VendorPermissionWithGvl expectedVendorPermissionWitGvl1 = VendorPermissionWithGvl.of( + expectedVendorPermission1, VendorV2.empty(1)); + final List standardPermissions = singletonList(expectedVendorPermissionWitGvl1); + verify(purposeStrategyOne, never()).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); - verify(purposeStrategyTwo).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); - verify(purposeStrategySeven).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); - verify(purposeStrategyFour).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); + verify(purposeStrategyTwo).processTypePurposeStrategy(any(), any(), eq(standardPermissions), eq(false)); + verify(purposeStrategySeven).processTypePurposeStrategy(any(), any(), eq(standardPermissions), eq(false)); + verify(purposeStrategyFour).processTypePurposeStrategy(any(), any(), eq(standardPermissions), eq(false)); + + verify(purposeStrategyTwo).processTypePurposeStrategy(any(), any(), eq(emptyList()), eq(true)); + verify(purposeStrategySeven).processTypePurposeStrategy(any(), any(), eq(emptyList()), eq(true)); + verify(purposeStrategyFour).processTypePurposeStrategy(any(), any(), eq(emptyList()), eq(true)); verify(specialFeaturesStrategyOne).processSpecialFeaturesStrategy(any(), any(), anyCollection()); } @@ -360,11 +419,21 @@ public void permissionsForShouldAllowAllWhenP1TIIsAccessAllowed() { target.permissionsFor(singleton(1), tcString); // then + final VendorPermission expectedVendorPermission1 = VendorPermission.of(1, "rubicon", + PrivacyEnforcementAction.restrictAll()); + final VendorPermissionWithGvl expectedVendorPermissionWitGvl1 = VendorPermissionWithGvl.of( + expectedVendorPermission1, VendorV2.empty(1)); + final List standardPermissions = singletonList(expectedVendorPermissionWitGvl1); + verify(purposeStrategyOne, never()).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); verify(purposeStrategyOne).allow(any()); - verify(purposeStrategyTwo).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); - verify(purposeStrategySeven).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); - verify(purposeStrategyFour).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); + verify(purposeStrategyTwo).processTypePurposeStrategy(any(), any(), eq(standardPermissions), eq(false)); + verify(purposeStrategySeven).processTypePurposeStrategy(any(), any(), eq(standardPermissions), eq(false)); + verify(purposeStrategyFour).processTypePurposeStrategy(any(), any(), eq(standardPermissions), eq(false)); + + verify(purposeStrategyTwo).processTypePurposeStrategy(any(), any(), eq(emptyList()), eq(true)); + verify(purposeStrategySeven).processTypePurposeStrategy(any(), any(), eq(emptyList()), eq(true)); + verify(purposeStrategyFour).processTypePurposeStrategy(any(), any(), eq(emptyList()), eq(true)); verify(specialFeaturesStrategyOne).processSpecialFeaturesStrategy(any(), any(), anyCollection()); } @@ -387,11 +456,15 @@ public void permissionsForShouldNotAllowAllWhenP1TIsFalseAndP1TIIsAccessAllowed( target.permissionsFor(singleton(1), tcString); // then + final VendorPermission expectedVendorPermission1 = VendorPermission.of(1, "rubicon", + PrivacyEnforcementAction.restrictAll()); + final VendorPermissionWithGvl expectedVendorPermissionWitGvl1 = VendorPermissionWithGvl.of( + expectedVendorPermission1, VendorV2.empty(1)); + final List standardPermissions = singletonList(expectedVendorPermissionWitGvl1); + verify(purposeStrategyOne, never()).allow(any()); - verify(purposeStrategyOne).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); - verify(purposeStrategyTwo).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); - verify(purposeStrategySeven).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); - verify(purposeStrategyFour).processTypePurposeStrategy(any(), any(), anyCollection(), anyBoolean()); + verifyEachPurposeStrategyReceive(standardPermissions); + verifyEachPurposeStrategyReceiveWeak(emptyList()); verify(specialFeaturesStrategyOne).processSpecialFeaturesStrategy(any(), any(), anyCollection()); } @@ -403,6 +476,13 @@ public void verifyEachPurposeStrategyReceive(List vendo verify(purposeStrategySeven).processTypePurposeStrategy(tcString, purpose7, vendorPermissionWithGvls, false); } + public void verifyEachPurposeStrategyReceiveWeak(List vendorPermissionWithGvls) { + verify(purposeStrategyOne).processTypePurposeStrategy(tcString, weakPurpose1, vendorPermissionWithGvls, true); + verify(purposeStrategyTwo).processTypePurposeStrategy(tcString, weakPurpose2, vendorPermissionWithGvls, true); + verify(purposeStrategyFour).processTypePurposeStrategy(tcString, weakPurpose4, vendorPermissionWithGvls, true); + verify(purposeStrategySeven).processTypePurposeStrategy(tcString, weakPurpose7, vendorPermissionWithGvls, true); + } + public void verifyEachSpecialFeatureStrategyReceive(List vendorPermission) { verify(specialFeaturesStrategyOne).processSpecialFeaturesStrategy(tcString, specialFeature1, vendorPermission); } diff --git a/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java b/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java index bcd8f1e2ab4..2d2ba53a759 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java @@ -9,6 +9,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.auction.IpAddressHelper; +import org.prebid.server.auction.model.IpAddress; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.geolocation.model.GeoInfo; @@ -27,8 +28,10 @@ import org.prebid.server.settings.model.Purpose; import org.prebid.server.settings.model.Purposes; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Set; import static java.util.Arrays.asList; @@ -38,11 +41,13 @@ import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.prebid.server.assertion.FutureAssertion.assertThat; @@ -54,8 +59,6 @@ public class TcfDefinerServiceTest { @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock - private GdprService gdprService; @Mock private Tcf2Service tcf2Service; @Mock @@ -85,7 +88,6 @@ public void setUp() { tcfDefinerService = new TcfDefinerService( gdprConfig, singleton(EEA_COUNTRY), - gdprService, tcf2Service, geoLocationService, bidderCatalog, @@ -100,7 +102,6 @@ public void resolveTcfContextShouldReturnContextWhenGdprIsDisabled() { tcfDefinerService = new TcfDefinerService( gdprConfig, singleton(EEA_COUNTRY), - gdprService, tcf2Service, geoLocationService, bidderCatalog, @@ -109,7 +110,7 @@ public void resolveTcfContextShouldReturnContextWhenGdprIsDisabled() { // when final Future result = tcfDefinerService.resolveTcfContext( - Privacy.of(null, null, null, null), null, null, null); + Privacy.of(null, null, null, null), null, null, MetricName.setuid, null, null); // then assertThat(result).succeededWith(TcfContext.empty()); @@ -127,7 +128,7 @@ public void resolveTcfContextShouldReturnContextWithGdprZeroWhenGdprIsDisabledBy // when final Future result = tcfDefinerService.resolveTcfContext( - Privacy.of(null, null, null, null), null, null, accountGdprConfig, MetricName.amp, null); + Privacy.of(null, null, null, null), null, null, accountGdprConfig, MetricName.amp, null, null, null); // then assertThat(result).succeededWith(TcfContext.builder().build()); @@ -143,7 +144,7 @@ public void resolveTcfContextShouldReturnContextWhenGdprIsDisabledByAccount() { // when final Future result = tcfDefinerService.resolveTcfContext( - Privacy.of(null, null, null, null), null, accountGdprConfig, null); + Privacy.of(null, null, null, null), null, accountGdprConfig, MetricName.setuid, null, null); // then assertThat(result).succeededWith(TcfContext.empty()); @@ -162,7 +163,7 @@ public void resolveTcfContextShouldCheckAccountConfigValueWhenRequestTypeIsUnkno // when final Future result = tcfDefinerService.resolveTcfContext( - Privacy.of(null, null, null, null), null, null, accountGdprConfig, MetricName.legacy, null); + Privacy.of(null, null, null, null), null, null, accountGdprConfig, MetricName.setuid, null, null, null); // then assertThat(result).succeededWith(TcfContext.empty()); @@ -179,7 +180,6 @@ public void resolveTcfContextShouldCheckServiceConfigValueWhenRequestTypeIsUnkno tcfDefinerService = new TcfDefinerService( gdprConfig, singleton(EEA_COUNTRY), - gdprService, tcf2Service, geoLocationService, bidderCatalog, @@ -192,7 +192,7 @@ public void resolveTcfContextShouldCheckServiceConfigValueWhenRequestTypeIsUnkno // when final Future result = tcfDefinerService.resolveTcfContext( - Privacy.of(null, null, null, null), null, null, accountGdprConfig, MetricName.legacy, null); + Privacy.of(null, null, null, null), null, null, accountGdprConfig, MetricName.setuid, null, null, null); // then assertThat(result).succeededWith(TcfContext.empty()); @@ -202,17 +202,17 @@ public void resolveTcfContextShouldCheckServiceConfigValueWhenRequestTypeIsUnkno } @Test - public void resolveTcfContextShouldConsiderPresenceOfConsentStringAsInScope() { + public void resolveTcfContextShouldConsiderTcfVersionOneAsCorruptedVersionTwo() { // given final GdprConfig gdprConfig = GdprConfig.builder() .enabled(true) .consentStringMeansInScope(true) .build(); + final List debugWarnings = new ArrayList<>(); tcfDefinerService = new TcfDefinerService( gdprConfig, singleton(EEA_COUNTRY), - gdprService, tcf2Service, geoLocationService, bidderCatalog, @@ -223,20 +223,56 @@ public void resolveTcfContextShouldConsiderPresenceOfConsentStringAsInScope() { // when final Future result = tcfDefinerService.resolveTcfContext( - Privacy.of(null, vendorConsent, null, null), null, null, null); + Privacy.of(null, vendorConsent, null, null), "london", null, + null, MetricName.setuid, null, null, debugWarnings); + + // then + assertThat(result).isSucceeded(); + assertThat(result.result().getConsent()).isInstanceOf(TCStringEmpty.class); + assertThat(debugWarnings) + .containsExactly("Parsing consent string:\"BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA\" failed. " + + "TCF version 1 is deprecated and treated as corrupted TCF version 2"); + } + + @Test + public void resolveTcfContextShouldConsiderPresenceOfConsentStringAsInScope() { + // given + final GdprConfig gdprConfig = GdprConfig.builder() + .enabled(true) + .consentStringMeansInScope(true) + .build(); + + tcfDefinerService = new TcfDefinerService( + gdprConfig, + singleton(EEA_COUNTRY), + tcf2Service, + geoLocationService, + bidderCatalog, + ipAddressHelper, + metrics); + + final String vendorConsent = "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA"; + + // when + final Future result = tcfDefinerService.resolveTcfContext( + Privacy.of(null, vendorConsent, null, null), null, null, null, null, null); // then assertThat(result).isSucceeded(); assertThat(result.result()).extracting( TcfContext::getGdpr, TcfContext::getConsentString, + TcfContext::getIsConsentValid, TcfContext::getGeoInfo, TcfContext::getInEea, TcfContext::getIpAddress) - .containsExactly("1", "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA", null, null, null); + .containsExactly("1", "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + true, null, null, null); assertThat(result.result().getConsent()).isNotNull(); verifyZeroInteractions(geoLocationService); + verify(metrics).updatePrivacyTcfRequestsMetric(2); + verify(metrics).updatePrivacyTcfGeoMetric(2, null); } @Test @@ -244,7 +280,7 @@ public void resolveTcfContextShouldReturnGdprFromCountryWhenGdprFromRequestIsNot // when final Future result = tcfDefinerService.resolveTcfContext( - Privacy.of(EMPTY, "consent", null, null), EEA_COUNTRY, "ip", null, null, null); + Privacy.of(EMPTY, "consent", null, null), EEA_COUNTRY, "ip", null, null, null, null, null); // then assertThat(result).isSucceeded(); @@ -263,6 +299,7 @@ public void resolveTcfContextShouldReturnGdprFromCountryWhenGdprFromRequestIsNot @Test public void resolveTcfContextShouldReturnGdprFromGeoLocationServiceWhenGdprFromRequestIsNotValid() { // given + given(ipAddressHelper.toIpAddress(anyString())).willReturn(IpAddress.of("ip", IpAddress.IP.v4)); given(ipAddressHelper.maskIpv4(anyString())).willReturn("ip-masked"); final GeoInfo geoInfo = GeoInfo.builder().vendor("vendor").country("ua").build(); @@ -272,7 +309,7 @@ public void resolveTcfContextShouldReturnGdprFromGeoLocationServiceWhenGdprFromR // when final Future result = tcfDefinerService.resolveTcfContext( - Privacy.of(EMPTY, consentString, null, null), "ip", null, null); + Privacy.of(EMPTY, consentString, null, null), "ip", null, MetricName.setuid, null, null); // then assertThat(result).isSucceeded(); @@ -300,7 +337,6 @@ public void resolveTcfContextShouldConsultDefaultValueWhenGeoLookupFailed() { tcfDefinerService = new TcfDefinerService( gdprConfig, singleton(EEA_COUNTRY), - gdprService, tcf2Service, geoLocationService, bidderCatalog, @@ -311,7 +347,7 @@ public void resolveTcfContextShouldConsultDefaultValueWhenGeoLookupFailed() { // when final Future result = tcfDefinerService.resolveTcfContext( - Privacy.of(null, null, null, null), "ip", null, null); + Privacy.of(null, null, null, null), "ip", null, MetricName.setuid, null, null); // then assertThat(result).isSucceeded(); @@ -336,7 +372,6 @@ public void resolveTcfContextShouldConsultDefaultValueAndSkipGeoLookupWhenIpIsNu tcfDefinerService = new TcfDefinerService( gdprConfig, singleton(EEA_COUNTRY), - gdprService, tcf2Service, geoLocationService, bidderCatalog, @@ -347,7 +382,7 @@ public void resolveTcfContextShouldConsultDefaultValueAndSkipGeoLookupWhenIpIsNu // when final Future result = tcfDefinerService.resolveTcfContext( - Privacy.of(null, null, null, null), null, null, null); + Privacy.of(null, null, null, null), null, null, MetricName.setuid, null, null); // then assertThat(result).isSucceeded(); @@ -362,11 +397,43 @@ public void resolveTcfContextShouldConsultDefaultValueAndSkipGeoLookupWhenIpIsNu verifyZeroInteractions(geoLocationService); } + @Test + public void resolveTcfContextShouldReturnTcfContextWithConsentValidAsTrue() { + // when + final Future result = tcfDefinerService.resolveTcfContext( + Privacy.of("1", "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + null, null), null, null, MetricName.setuid, null, + null); + + // then + assertThat(result).isSucceeded(); + assertThat(result.result()).extracting( + TcfContext::getGdpr, + TcfContext::getConsentString, + TcfContext::getIsConsentValid) + .containsExactly("1", "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", true); + } + + @Test + public void resolveTcfContextShouldReturnTcfContextWithConsentValidAsFalse() { + // when + final Future result = tcfDefinerService.resolveTcfContext( + Privacy.of("1", "invalid", null, null), null, null, MetricName.setuid, null, null); + + // then + assertThat(result).isSucceeded(); + assertThat(result.result()).extracting( + TcfContext::getGdpr, + TcfContext::getConsentString, + TcfContext::getIsConsentValid) + .containsExactly("1", "invalid", false); + } + @Test public void resolveTcfContextShouldIncrementMissingConsentStringMetric() { // when tcfDefinerService.resolveTcfContext( - Privacy.of("1", EMPTY, null, null), null, null, null); + Privacy.of("1", EMPTY, null, null), null, null, MetricName.setuid, null, null); // then verify(metrics).updatePrivacyTcfMissingMetric(); @@ -376,12 +443,29 @@ public void resolveTcfContextShouldIncrementMissingConsentStringMetric() { public void resolveTcfContextShouldIncrementInvalidConsentStringMetric() { // when tcfDefinerService.resolveTcfContext( - Privacy.of("1", "abc", null, null), null, null, null); + Privacy.of("1", "abc", null, null), null, null, MetricName.setuid, null, null); // then verify(metrics).updatePrivacyTcfInvalidMetric(); } + @Test + public void resultForVendorIdsShouldNotSetTcfRequestsAndTcfGeoMetricsWhenConsentIsNotValid() { + // given + given(tcf2Service.permissionsFor(any(), any())).willReturn(Future.succeededFuture()); + + // when + tcfDefinerService.resultForVendorIds(singleton(1), TcfContext.builder() + .gdpr("1") + .consent(TCStringEmpty.create()) + .ipAddress("ip") + .build()); + + // then + verify(metrics, never()).updatePrivacyTcfRequestsMetric(anyInt()); + verify(metrics, never()).updatePrivacyTcfGeoMetric(anyInt(), any()); + } + @Test public void resultForVendorIdsShouldAllowAllWhenGdprIsZero() { // when @@ -392,7 +476,6 @@ public void resultForVendorIdsShouldAllowAllWhenGdprIsZero() { assertThat(result).succeededWith( TcfResponse.of(false, singletonMap(1, PrivacyEnforcementAction.allowAll()), null)); - verifyZeroInteractions(gdprService); verifyZeroInteractions(tcf2Service); } @@ -409,7 +492,6 @@ public void resultForVendorIdsShouldReturnRestrictAllWhenConsentIsMissing() { // then verify(tcf2Service).permissionsFor(any(), argThat(arg -> arg.getClass() == TCStringEmpty.class)); - verifyZeroInteractions(gdprService); } @Test @@ -422,79 +504,6 @@ public void resultForBidderNamesShouldReturnAllowAllWhenGdprIsZero() { assertThat(result).succeededWith( TcfResponse.of(false, singletonMap("b1", PrivacyEnforcementAction.allowAll()), null)); - verifyZeroInteractions(tcf2Service); - verifyZeroInteractions(gdprService); - } - - @Test - public void resultForVendorIdsShouldReturnTcfResponseFromGdprServiceWhenConsentStringIsFirstVersion() { - // given - given(gdprService.resultFor(anySet(), anyString())) - .willReturn(Future.succeededFuture(asList( - VendorPermission.of(1, null, PrivacyEnforcementAction.allowAll()), - VendorPermission.of(2, null, PrivacyEnforcementAction.restrictAll())))); - - final String consentString = "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA"; - - // when - final Future> result = tcfDefinerService.resultForVendorIds( - new HashSet<>(asList(1, 2)), - TcfContext.builder() - .gdpr("1") - .consentString(consentString) - .consent(TCString.decode(consentString)) - .build()); - - // then - final HashMap expectedVendorIdToPrivacyMap = new HashMap<>(); - expectedVendorIdToPrivacyMap.put(1, PrivacyEnforcementAction.allowAll()); - expectedVendorIdToPrivacyMap.put(2, PrivacyEnforcementAction.restrictAll()); - assertThat(result).succeededWith(TcfResponse.of(true, expectedVendorIdToPrivacyMap, null)); - - verifyZeroInteractions(tcf2Service); - } - - @Test - public void resultForBidderNamesShouldReturnTcfResponseFromGdprServiceWhenConsentStringIsFirstVersion() { - // given - given(gdprService.resultFor(anySet(), anyString())) - .willReturn(Future.succeededFuture(asList( - VendorPermission.of(1, null, PrivacyEnforcementAction.allowAll()), - VendorPermission.of(2, null, PrivacyEnforcementAction.allowAll())))); - - given(bidderCatalog.isActive(eq("b1"))).willReturn(true); - given(bidderCatalog.isActive(eq("b2"))).willReturn(true); - given(bidderCatalog.isActive(eq("b3"))).willReturn(false); - given(bidderCatalog.vendorIdByName(eq("b1"))).willReturn(1); - given(bidderCatalog.vendorIdByName(eq("b2"))).willReturn(2); - - final String consentString = "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA"; - - // when - final Future> result = tcfDefinerService.resultForBidderNames( - new HashSet<>(asList("b1", "b2", "b3")), - TcfContext.builder() - .gdpr("1") - .consentString(consentString) - .consent(TCString.decode(consentString)) - .build(), - null); - - // then - final HashMap expectedBidderNameToPrivacyMap = new HashMap<>(); - expectedBidderNameToPrivacyMap.put("b1", PrivacyEnforcementAction.allowAll()); - expectedBidderNameToPrivacyMap.put("b2", PrivacyEnforcementAction.allowAll()); - expectedBidderNameToPrivacyMap.put("b3", PrivacyEnforcementAction.builder() - .removeUserIds(true) - .maskGeo(true) - .maskDeviceIp(true) - .maskDeviceInfo(true) - .blockAnalyticsReport(false) - .blockBidderRequest(false) - .blockPixelSync(true) - .build()); - assertThat(result).succeededWith(TcfResponse.of(true, expectedBidderNameToPrivacyMap, null)); - verifyZeroInteractions(tcf2Service); } @@ -518,8 +527,6 @@ public void resultForVendorIdsShouldReturnTcfResponseFromTcf2ServiceWhenConsentS expectedVendorIdToPrivacyMap.put(1, PrivacyEnforcementAction.allowAll()); expectedVendorIdToPrivacyMap.put(2, PrivacyEnforcementAction.allowAll()); assertThat(result).succeededWith(TcfResponse.of(true, expectedVendorIdToPrivacyMap, null)); - - verifyZeroInteractions(gdprService); } @Test @@ -546,13 +553,12 @@ public void resultForBidderNamesShouldReturnTcfResponseFromTcf2ServiceWhenConsen expectedBidderNameToPrivacyMap.put("b1", PrivacyEnforcementAction.allowAll()); expectedBidderNameToPrivacyMap.put("b2", PrivacyEnforcementAction.allowAll()); assertThat(result).succeededWith(TcfResponse.of(true, expectedBidderNameToPrivacyMap, null)); - - verifyZeroInteractions(gdprService); } @Test public void isConsentStringValidShouldReturnTrueWhenStringIsValid() { - assertThat(TcfDefinerService.isConsentStringValid("BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA")).isTrue(); + assertThat(TcfDefinerService.isConsentStringValid("CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA")) + .isTrue(); } @Test diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategyTest.java index 3cef1c4f6ea..a7838f91918 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeEightStrategyTest.java @@ -13,6 +13,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -26,13 +27,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeEightStrategyTest { - private static final int PURPOSE_ID = 8; + private static final PurposeCode PURPOSE_CODE = + PurposeCode.EIGHT; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -84,7 +86,7 @@ public void allowNaturallyShouldReturnExpectedValue() { @Test public void getPurposeIdShouldReturnExpectedValue() { // when and then - assertThat(target.getPurposeId()).isEqualTo(PURPOSE_ID); + assertThat(target.getPurpose()).isEqualTo(PURPOSE_CODE); } @Test @@ -104,7 +106,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -119,7 +121,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -140,7 +142,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi VendorV2.empty(3)); final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -155,7 +157,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -178,7 +180,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -192,7 +194,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @@ -214,7 +216,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -222,20 +224,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +249,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +291,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); + + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +331,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategyTest.java index e9cda4d9328..2314b95f39f 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFiveStrategyTest.java @@ -13,6 +13,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -26,13 +27,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeFiveStrategyTest { - private static final int PURPOSE_ID = 5; + private static final PurposeCode PURPOSE_CODE = + PurposeCode.FIVE; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -84,7 +86,7 @@ public void allowNaturallyShouldReturnExpectedValue() { @Test public void getPurposeIdShouldReturnExpectedValue() { // when and then - assertThat(target.getPurposeId()).isEqualTo(PURPOSE_ID); + assertThat(target.getPurpose()).isEqualTo(PURPOSE_CODE); } @Test @@ -104,7 +106,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -119,7 +121,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -140,7 +142,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi VendorV2.empty(3)); final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -155,7 +157,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -178,7 +180,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -192,7 +194,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @@ -214,7 +216,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -222,20 +224,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +249,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +291,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); + + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +331,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategyTest.java index a2053d1a46c..8caa0d96aea 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeFourStrategyTest.java @@ -13,6 +13,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -26,13 +27,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeFourStrategyTest { - private static final int PURPOSE_ID = 4; + private static final PurposeCode PURPOSE_CODE = + PurposeCode.FOUR; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -84,7 +86,7 @@ public void allowNaturallyShouldReturnExpectedValue() { @Test public void getPurposeIdShouldReturnExpectedValue() { // when and then - assertThat(target.getPurposeId()).isEqualTo(PURPOSE_ID); + assertThat(target.getPurpose()).isEqualTo(PURPOSE_CODE); } @Test @@ -104,7 +106,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -119,7 +121,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -141,7 +143,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -156,7 +158,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -180,7 +182,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithEnforcementsWhenAll final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -194,7 +196,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithEnforcementsWhenAll assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @@ -216,7 +218,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -224,20 +226,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -249,34 +251,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -288,36 +293,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); + + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setMaskDeviceInfo(false); - privacyEnforcementAction.setRemoveUserIds(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -325,7 +333,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategyTest.java index 9b3aacce285..17afe4913be 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeNineStrategyTest.java @@ -13,6 +13,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -26,13 +27,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeNineStrategyTest { - private static final int PURPOSE_ID = 9; + private static final PurposeCode PURPOSE_CODE = + PurposeCode.NINE; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -84,7 +86,7 @@ public void allowNaturallyShouldReturnExpectedValue() { @Test public void getPurposeIdShouldReturnExpectedValue() { // when and then - assertThat(target.getPurposeId()).isEqualTo(PURPOSE_ID); + assertThat(target.getPurpose()).isEqualTo(PURPOSE_CODE); } @Test @@ -104,7 +106,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -119,7 +121,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -140,7 +142,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi VendorV2.empty(3)); final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -155,7 +157,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -178,7 +180,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -192,7 +194,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @@ -214,7 +216,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -222,20 +224,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +249,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +291,38 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +330,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategyTest.java index 0c0f0f563d9..afb2ca9bf2c 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeOneStrategyTest.java @@ -13,6 +13,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -26,13 +27,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeOneStrategyTest { - private static final int PURPOSE_ID = 1; + private static final PurposeCode PURPOSE_CODE = + PurposeCode.ONE; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -84,7 +86,7 @@ public void allowNaturallyShouldReturnExpectedValue() { @Test public void getPurposeIdShouldReturnExpectedValue() { // when and then - assertThat(target.getPurposeId()).isEqualTo(PURPOSE_ID); + assertThat(target.getPurpose()).isEqualTo(PURPOSE_CODE); } @Test @@ -104,7 +106,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -119,7 +121,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -141,7 +143,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -156,7 +158,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -180,7 +182,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithEnforcementsWhenAll final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -194,7 +196,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithEnforcementsWhenAll assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @@ -216,7 +218,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -224,20 +226,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -249,34 +251,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); - assertThat(result).usingFieldByFieldElementComparator().isEqualTo( - Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); + assertThat(result).usingFieldByFieldElementComparator() + .isEqualTo(Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -288,29 +293,35 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); + + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } private static PrivacyEnforcementAction allowPurpose() { @@ -318,4 +329,8 @@ private static PrivacyEnforcementAction allowPurpose() { privacyEnforcementAction.setBlockPixelSync(false); return privacyEnforcementAction; } + + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowPurpose(); + } } diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategyTest.java index a46e48b42c1..20ec884e9fc 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSevenStrategyTest.java @@ -13,6 +13,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -26,13 +27,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeSevenStrategyTest { - private static final int PURPOSE_ID = 7; + private static final PurposeCode PURPOSE_CODE = + PurposeCode.SEVEN; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -84,7 +86,7 @@ public void allowNaturallyShouldReturnExpectedValue() { @Test public void getPurposeIdShouldReturnExpectedValue() { // when and then - assertThat(target.getPurposeId()).isEqualTo(PURPOSE_ID); + assertThat(target.getPurpose()).isEqualTo(PURPOSE_CODE); } @Test @@ -104,7 +106,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -119,7 +121,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -141,7 +143,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -156,7 +158,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -180,7 +182,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithEnforcementsWhenAll final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -194,7 +196,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithEnforcementsWhenAll assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @@ -216,7 +218,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -224,20 +226,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -249,34 +251,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -288,37 +293,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); + + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setBlockAnalyticsReport(false); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -328,7 +335,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategyTest.java index d982df97900..d569fb5e82c 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeSixStrategyTest.java @@ -13,6 +13,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -26,13 +27,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeSixStrategyTest { - private static final int PURPOSE_ID = 6; + private static final PurposeCode PURPOSE_CODE = + PurposeCode.SIX; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -84,7 +86,7 @@ public void allowNaturallyShouldReturnExpectedValue() { @Test public void getPurposeIdShouldReturnExpectedValue() { // when and then - assertThat(target.getPurposeId()).isEqualTo(PURPOSE_ID); + assertThat(target.getPurpose()).isEqualTo(PURPOSE_CODE); } @Test @@ -104,7 +106,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -119,7 +121,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -140,7 +142,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi VendorV2.empty(3)); final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -155,7 +157,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -178,7 +180,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -192,7 +194,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @@ -214,7 +216,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -222,20 +224,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +249,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +291,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); + + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +331,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategyTest.java index b13c7e04a13..7a4e081e266 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTenStrategyTest.java @@ -13,6 +13,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -26,13 +27,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeTenStrategyTest { - private static final int PURPOSE_ID = 10; + private static final PurposeCode PURPOSE_CODE = + PurposeCode.TEN; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -84,7 +86,7 @@ public void allowNaturallyShouldReturnExpectedValue() { @Test public void getPurposeIdShouldReturnExpectedValue() { // when and then - assertThat(target.getPurposeId()).isEqualTo(PURPOSE_ID); + assertThat(target.getPurpose()).isEqualTo(PURPOSE_CODE); } @Test @@ -104,7 +106,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -119,7 +121,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -140,7 +142,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi VendorV2.empty(3)); final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -155,7 +157,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -178,7 +180,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -192,7 +194,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @@ -214,7 +216,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -222,20 +224,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +249,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +291,38 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +330,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategyTest.java index 45ca4a2d40a..6e436c05796 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeThreeStrategyTest.java @@ -13,6 +13,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -26,13 +27,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeThreeStrategyTest { - private static final int PURPOSE_ID = 3; + private static final PurposeCode PURPOSE_CODE = + PurposeCode.THREE; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -84,7 +86,7 @@ public void allowNaturallyShouldReturnExpectedValue() { @Test public void getPurposeIdShouldReturnExpectedValue() { // when and then - assertThat(target.getPurposeId()).isEqualTo(PURPOSE_ID); + assertThat(target.getPurpose()).isEqualTo(PURPOSE_CODE); } @Test @@ -104,7 +106,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -119,7 +121,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -140,7 +142,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi VendorV2.empty(3)); final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -155,7 +157,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -178,7 +180,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -192,7 +194,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @@ -214,7 +216,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -222,20 +224,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +249,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,36 +291,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); + + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -323,7 +331,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategyTest.java index 33bd70bd7db..85857d6a76a 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeTwoStrategyTest.java @@ -13,6 +13,7 @@ import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.BasicEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.FullEnforcePurposeStrategy; import org.prebid.server.privacy.gdpr.tcfstrategies.purpose.typestrategies.NoEnforcePurposeStrategy; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -26,13 +27,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PurposeTwoStrategyTest { - private static final int PURPOSE_ID = 2; + private static final PurposeCode PURPOSE_CODE = + PurposeCode.TWO; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -84,7 +86,7 @@ public void allowNaturallyShouldReturnExpectedValue() { @Test public void getPurposeIdShouldReturnExpectedValue() { // when and then - assertThat(target.getPurposeId()).isEqualTo(PURPOSE_ID); + assertThat(target.getPurpose()).isEqualTo(PURPOSE_CODE); } @Test @@ -104,7 +106,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -119,7 +121,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -140,7 +142,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi VendorV2.empty(3)); final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(Arrays.asList(vendorPermission1, vendorPermission2)); // when @@ -155,7 +157,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl3), singletonList(vendorPermissionWitGvl2), false); } @@ -178,7 +180,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -192,7 +194,7 @@ public void processTypePurposeStrategyShouldPassListWithEnforcementsAndExcludeBi assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @@ -214,7 +216,7 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); // when @@ -222,20 +224,20 @@ public void processTypePurposeStrategyShouldPassEmptyListWithFullEnforcementsWhe vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowAllPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowAllPurpose()); - final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowAllPurpose()); + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); + final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurposeAndNaturally()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, emptyList(), vendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowance() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyVendorExceptions() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.full, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -247,34 +249,37 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); - given(fullEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + given(fullEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions) - .willReturn(emptyList()); + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, false); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(fullEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(fullEnforcePurposeStrategy, times(2)).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } @Test - public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNaturalAllowanceForDowngraded() { + public void processTypePurposeStrategyShouldAllowPurposeAndNaturallyWhenVendorPermissionsReturnedForDowngraded() { // given - final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2", "b3", "b5", "b7")); + final Purpose purpose = Purpose.of(EnforcePurpose.no, null, Arrays.asList("b1", "b2")); final VendorPermission vendorPermission1 = VendorPermission.of(1, "b1", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission2 = VendorPermission.of(2, "b2", PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission3 = VendorPermission.of(3, "b3", PrivacyEnforcementAction.restrictAll()); @@ -286,37 +291,39 @@ public void processTypePurposeStrategyShouldAllowOnlyPurposeWhenThereAreNoNatura VendorV2.empty(3)); final List vendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2, vendorPermission3); - final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, - vendorPermissionWitGvl2, vendorPermissionWitGvl3); - given(noEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) + final List excludedVendorPermissions = Arrays.asList(vendorPermission1, vendorPermission2); + + given(noEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) .willReturn(vendorPermissions); - given(basicEnforcePurposeStrategy.allowedByTypeStrategy(anyInt(), any(), any(), any(), anyBoolean())) - .willReturn(emptyList()); + given(basicEnforcePurposeStrategy.allowedByTypeStrategy(any(), any(), any(), any(), anyBoolean())) + .willReturn(excludedVendorPermissions); + + final List vendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2, vendorPermissionWitGvl3); // when final Collection result = target.processTypePurposeStrategy(tcString, purpose, vendorPermissionsWithGvl, true); // then - final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurpose()); - final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurpose()); + final List excludedVendorPermissionsWithGvl = Arrays.asList(vendorPermissionWitGvl1, + vendorPermissionWitGvl2); + + final VendorPermission vendorPermission1Changed = VendorPermission.of(1, "b1", allowPurposeAndNaturally()); + final VendorPermission vendorPermission2Changed = VendorPermission.of(2, "b2", allowPurposeAndNaturally()); final VendorPermission vendorPermission3Changed = VendorPermission.of(3, "b3", allowPurpose()); assertThat(result).usingFieldByFieldElementComparator().isEqualTo( Arrays.asList(vendorPermission1Changed, vendorPermission2Changed, vendorPermission3Changed)); - verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, emptyList(), - vendorPermissionsWithGvl, true); - verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_ID, tcString, vendorPermissionsWithGvl, - emptyList(), true); + verify(noEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); + verify(basicEnforcePurposeStrategy).allowedByTypeStrategy(PURPOSE_CODE, tcString, + singletonList(vendorPermissionWitGvl3), excludedVendorPermissionsWithGvl, true); } - private static PrivacyEnforcementAction allowAllPurpose() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); - privacyEnforcementAction.setBlockBidderRequest(false); - privacyEnforcementAction.setRemoveUserIds(false); - privacyEnforcementAction.setMaskDeviceInfo(false); - return privacyEnforcementAction; + private static PrivacyEnforcementAction allowPurposeAndNaturally() { + return allowNatural(allowPurpose()); } private static PrivacyEnforcementAction allowPurpose() { @@ -326,7 +333,10 @@ private static PrivacyEnforcementAction allowPurpose() { } private static PrivacyEnforcementAction allowNatural() { - final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll(); + return allowNatural(PrivacyEnforcementAction.restrictAll()); + } + + private static PrivacyEnforcementAction allowNatural(PrivacyEnforcementAction privacyEnforcementAction) { privacyEnforcementAction.setRemoveUserIds(false); privacyEnforcementAction.setMaskDeviceInfo(false); return privacyEnforcementAction; diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategyTest.java index 93896659194..6c8a3947453 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/BasicEnforcePurposeStrategyTest.java @@ -11,6 +11,7 @@ import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import java.util.Arrays; @@ -26,7 +27,7 @@ public class BasicEnforcePurposeStrategyTest { - private static final int PURPOSE_ID = 1; + private static final PurposeCode PURPOSE_CODE = PurposeCode.ONE; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -68,7 +69,7 @@ public void allowedByTypeStrategyShouldReturnEmptyListWhenVendorIsNotAllowedAndV final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -84,7 +85,7 @@ public void allowedByTypeStrategyShouldReturnEmptyListWhenVendorIsNotAllowedAndV final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -102,7 +103,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorIsAllowedAnd given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -120,7 +121,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorIsAllowedAnd given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -138,7 +139,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorLIIsAllowedA given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -156,7 +157,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorLIIsAllowedA given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -175,7 +176,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenPurposeLIAndPurpos given(purposesLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -194,7 +195,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenPurposeLIAndPurpos given(purposesLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -214,10 +215,10 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenPurposeLIAndVendor vendorPermissionWitGvl2); given(allowedVendors.contains(anyInt())).willReturn(true); - given(purposesLI.contains(PURPOSE_ID)).willReturn(true); + given(purposesLI.contains(PURPOSE_CODE.code())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -237,10 +238,10 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenPurposeAndVendorLI vendorPermissionWitGvl2); given(allowedVendorsLI.contains(anyInt())).willReturn(true); - given(purposesConsent.contains(PURPOSE_ID)).willReturn(true); + given(purposesConsent.contains(PURPOSE_CODE.code())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -258,11 +259,10 @@ public void allowedByTypeStrategyShouldReturnExcludedVendors() { VendorV2.empty(2)); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, singleton(vendorPermissionWitGvl1), singleton(vendorPermissionWitGvl2), true); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission2); } } - diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/FullEnforcePurposeStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/FullEnforcePurposeStrategyTest.java index be49039e767..3c4d88ebc41 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/FullEnforcePurposeStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/FullEnforcePurposeStrategyTest.java @@ -13,14 +13,15 @@ import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import java.util.Arrays; import java.util.Collection; +import java.util.EnumSet; import java.util.List; import static java.util.Collections.emptyList; -import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; @@ -33,7 +34,7 @@ public class FullEnforcePurposeStrategyTest { - private static final int PURPOSE_ID = 1; + private static final PurposeCode PURPOSE_CODE = PurposeCode.ONE; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -65,7 +66,7 @@ public void setUp() { given(tcString.getPublisherRestrictions()).willReturn(singletonList(publisherRestriction)); - given(publisherRestriction.getPurposeId()).willReturn(PURPOSE_ID); + given(publisherRestriction.getPurposeId()).willReturn(PURPOSE_CODE.code()); given(publisherRestriction.getVendorIds()).willReturn(vendorIds); given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.UNDEFINED); @@ -82,12 +83,12 @@ public void setUp() { public void shouldReturnOnlyExcludedAllowedWhenMultiplePublisherRestrictionsProvided() { // given final IntIterable requireConsentIterable = mock(IntIterable.class); - final PublisherRestriction publisherRestriction1 = new PublisherRestriction(PURPOSE_ID, + final PublisherRestriction publisherRestriction1 = new PublisherRestriction(PURPOSE_CODE.code(), RestrictionType.REQUIRE_CONSENT, requireConsentIterable); given(requireConsentIterable.spliterator()).willReturn(singletonList(1).spliterator()); final IntIterable notAllowedIterable = mock(IntIterable.class); - final PublisherRestriction publisherRestriction2 = new PublisherRestriction(PURPOSE_ID, + final PublisherRestriction publisherRestriction2 = new PublisherRestriction(PURPOSE_CODE.code(), RestrictionType.NOT_ALLOWED, notAllowedIterable); given(notAllowedIterable.spliterator()).willReturn(Arrays.asList(4, 2).spliterator()); @@ -111,7 +112,7 @@ public void shouldReturnOnlyExcludedAllowedWhenMultiplePublisherRestrictionsProv VendorV2.empty(5)); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(requireConsentPermission, notAllowedPermission, notMentionedPermission), Arrays.asList(excludedNotMentionedPermission, excludedNotAllowedPermission), true); @@ -123,13 +124,13 @@ public void shouldReturnOnlyExcludedAllowedWhenMultiplePublisherRestrictionsProv public void shouldReturnExpectedWhenMultiplePublisherRestrictionsProvided() { // given final IntIterable requireConsentIterable = mock(IntIterable.class); - final PublisherRestriction publisherRestriction1 = new PublisherRestriction(PURPOSE_ID, + final PublisherRestriction publisherRestriction1 = new PublisherRestriction(PURPOSE_CODE.code(), RestrictionType.REQUIRE_CONSENT, requireConsentIterable); given(requireConsentIterable.spliterator()).willReturn(singletonList(1).spliterator()); given(requireConsentIterable.contains(eq(1))).willReturn(true); final IntIterable notAllowedIterable = mock(IntIterable.class); - final PublisherRestriction publisherRestriction2 = new PublisherRestriction(PURPOSE_ID, + final PublisherRestriction publisherRestriction2 = new PublisherRestriction(PURPOSE_CODE.code(), RestrictionType.NOT_ALLOWED, notAllowedIterable); given(notAllowedIterable.spliterator()).willReturn(Arrays.asList(4, 2).spliterator()); given(notAllowedIterable.contains(eq(4))).willReturn(true); @@ -144,20 +145,20 @@ public void shouldReturnExpectedWhenMultiplePublisherRestrictionsProvided() { final VendorPermission vendorPermission4 = VendorPermission.of(4, null, PrivacyEnforcementAction.restrictAll()); final VendorPermission vendorPermission5 = VendorPermission.of(5, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl requireConsentPermission = VendorPermissionWithGvl.of(vendorPermission1, - VendorV2.builder().id(1).purposes(singleton(PURPOSE_ID)).build()); + VendorV2.builder().id(1).purposes(EnumSet.of(PURPOSE_CODE)).build()); final VendorPermissionWithGvl notAllowedPermission = VendorPermissionWithGvl.of(vendorPermission2, - VendorV2.builder().id(2).purposes(singleton(PURPOSE_ID)).build()); + VendorV2.builder().id(2).purposes(EnumSet.of(PURPOSE_CODE)).build()); final VendorPermissionWithGvl excludedNotMentionedPermission = VendorPermissionWithGvl.of(vendorPermission3, - VendorV2.builder().id(3).purposes(singleton(PURPOSE_ID)).build()); + VendorV2.builder().id(3).purposes(EnumSet.of(PURPOSE_CODE)).build()); final VendorPermissionWithGvl excludedNotAllowedPermission = VendorPermissionWithGvl.of(vendorPermission4, - VendorV2.builder().id(4).purposes(singleton(PURPOSE_ID)).build()); + VendorV2.builder().id(4).purposes(EnumSet.of(PURPOSE_CODE)).build()); final VendorPermissionWithGvl notMentionedPermission = VendorPermissionWithGvl.of(vendorPermission5, - VendorV2.builder().id(5).purposes(singleton(PURPOSE_ID)).build()); + VendorV2.builder().id(5).purposes(EnumSet.of(PURPOSE_CODE)).build()); given(purposesConsent.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, Arrays.asList(requireConsentPermission, notAllowedPermission, notMentionedPermission), Arrays.asList(excludedNotMentionedPermission, excludedNotAllowedPermission), false); @@ -166,36 +167,90 @@ public void shouldReturnExpectedWhenMultiplePublisherRestrictionsProvided() { vendorPermission5); } + // GVL Purpose part + @Test public void shouldAllowWhenInGvlPurposeAndPurposeConsentAllowed() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(emptySet()) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .build(); + + final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); + final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); + final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); + + given(purposesConsent.contains(anyInt())).willReturn(true); + + // when + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, + vendorPermissionWithGvls, emptyList(), false); + + // then + assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); + + verify(purposesConsent).contains(PURPOSE_CODE.code()); + } + + @Test + public void shouldAllowWhenInGvlPurposeAndPurposeConsentAllowedAndRequireConsent() { + // given + final VendorV2 vendorGvl = VendorV2.builder() + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); + setRestriction(RestrictionType.REQUIRE_CONSENT); + given(purposesConsent.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesConsent).contains(PURPOSE_ID); + verify(purposesConsent).contains(PURPOSE_CODE.code()); + } + + @Test + public void shouldEmptyWhenInGvlPurposeAndPurposeConsentAllowedAndRequireLI() { + // given + final VendorV2 vendorGvl = VendorV2.builder() + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .build(); + + final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); + final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); + final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); + + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + + given(purposesConsent.contains(anyInt())).willReturn(true); + + // when + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, + vendorPermissionWithGvls, emptyList(), false); + + // then + assertThat(result).isEmpty(); + + verifyZeroInteractions(purposesConsent); } @Test public void shouldEmptyWhenInGvlPurposeAndPurposeLIAllowed() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(emptySet()) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); @@ -205,12 +260,13 @@ public void shouldEmptyWhenInGvlPurposeAndPurposeLIAllowed() { given(purposesLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then assertThat(result).isEmpty(); + verify(purposesConsent).contains(PURPOSE_CODE.code()); verifyZeroInteractions(purposesLI); } @@ -218,8 +274,8 @@ public void shouldEmptyWhenInGvlPurposeAndPurposeLIAllowed() { public void shouldAllowWhenInGvlPurposeAndPurposeConsentAllowedAndVendorConsentAllowedAndEnforced() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(emptySet()) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); @@ -230,13 +286,13 @@ public void shouldAllowWhenInGvlPurposeAndPurposeConsentAllowedAndVendorConsentA given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesConsent).contains(PURPOSE_ID); + verify(purposesConsent).contains(PURPOSE_CODE.code()); verify(allowedVendors).contains(1); } @@ -244,8 +300,8 @@ public void shouldAllowWhenInGvlPurposeAndPurposeConsentAllowedAndVendorConsentA public void shouldEmptyWhenInGvlPurposeAndPurposeConsentAllowedAndVendorLIAllowedAndEnforced() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(emptySet()) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); @@ -256,46 +312,101 @@ public void shouldEmptyWhenInGvlPurposeAndPurposeConsentAllowedAndVendorLIAllowe given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).isEmpty(); - verify(purposesConsent).contains(PURPOSE_ID); + verify(purposesConsent).contains(PURPOSE_CODE.code()); verifyZeroInteractions(purposesLI); } + // GVL Legitimate interest Purpose part + @Test public void shouldAllowWhenInGvlPurposeLIAndPurposeLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(emptySet()) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .build(); + + final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); + final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); + final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); + + given(purposesLI.contains(anyInt())).willReturn(true); + + // when + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, + vendorPermissionWithGvls, emptyList(), false); + + // then + assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); + + verify(purposesLI).contains(PURPOSE_CODE.code()); + } + + @Test + public void shouldAllowWhenInGvlPurposeLIAndPurposeLIAndRequireLI() { + // given + final VendorV2 vendorGvl = VendorV2.builder() + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + given(purposesLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesLI).contains(PURPOSE_ID); + verify(purposesLI).contains(PURPOSE_CODE.code()); + } + + @Test + public void shouldEmptyWhenInGvlPurposeLIAndPurposeLIAndRequireConsent() { + // given + final VendorV2 vendorGvl = VendorV2.builder() + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .build(); + + final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); + final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); + final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); + + RestrictionType requireConsent = RestrictionType.REQUIRE_CONSENT; + setRestriction(requireConsent); + + given(purposesLI.contains(anyInt())).willReturn(true); + + // when + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, + vendorPermissionWithGvls, emptyList(), false); + + // then + assertThat(result).isEmpty(); + + verifyZeroInteractions(purposesLI); } @Test public void shouldAllowWhenInGvlPurposeLIAndPurposeLIAndVendorLIAllowedAndEnforced() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(emptySet()) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); @@ -306,13 +417,13 @@ public void shouldAllowWhenInGvlPurposeLIAndPurposeLIAndVendorLIAllowedAndEnforc given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesLI).contains(PURPOSE_ID); + verify(purposesLI).contains(PURPOSE_CODE.code()); verify(allowedVendorsLI).contains(1); } @@ -320,8 +431,8 @@ public void shouldAllowWhenInGvlPurposeLIAndPurposeLIAndVendorLIAllowedAndEnforc public void shouldEmptyWhenInGvlPurposeLIAndPurposeConsentAllowedAndVendorConsentAllowedAndEnforced() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(emptySet()) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); @@ -333,71 +444,71 @@ public void shouldEmptyWhenInGvlPurposeLIAndPurposeConsentAllowedAndVendorConsen given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).isEmpty(); - verify(purposesLI).contains(PURPOSE_ID); + verify(purposesLI).contains(PURPOSE_CODE.code()); verifyZeroInteractions(allowedVendors); } // Flexible GVL Purpose part - // Restriction type is REQUIRE_CONSENT + // Restriction type is REQUIRE_CONSENT part @Test public void shouldAllowWhenInGvlPurposeAndPurposeConsentAllowedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); given(purposesConsent.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesConsent).contains(PURPOSE_ID); + verify(purposesConsent).contains(PURPOSE_CODE.code()); } @Test public void shouldAllowWhenInGvlPurposeAndPurposeConsentAndVendorConsentAndEnforcedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); given(purposesConsent.contains(anyInt())).willReturn(true); given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesConsent).contains(PURPOSE_ID); + verify(purposesConsent).contains(PURPOSE_CODE.code()); verify(allowedVendors).contains(1); } @@ -405,21 +516,20 @@ public void shouldAllowWhenInGvlPurposeAndPurposeConsentAndVendorConsentAndEnfor public void shouldEmptyWhenInGvlPurposeAndPurposeLIAllowedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -432,22 +542,21 @@ public void shouldEmptyWhenInGvlPurposeAndPurposeLIAllowedAndFlexibleAndRequireC public void shouldEmptyWhenInGvlPurposeAndPurposeLIAndVendorLIAllowedAndEnforcedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesLI.contains(anyInt())).willReturn(true); given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -461,75 +570,74 @@ public void shouldEmptyWhenInGvlPurposeAndPurposeLIAndVendorLIAllowedAndEnforced public void shouldEmptyWhenInGvlPurposeAndPurposeConsentAndVendorLIAndEnforcedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); given(purposesConsent.contains(anyInt())).willReturn(true); given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).isEmpty(); - verify(purposesConsent).contains(PURPOSE_ID); + verify(purposesConsent).contains(PURPOSE_CODE.code()); } @Test public void shouldEmptyWhenInGvlPurposeAndPurposeLIAndVendorConsentAndEnforcedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); given(purposesLI.contains(anyInt())).willReturn(true); given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).isEmpty(); } - // Restriction tipe is REQUIRE_LEGITIMATE_INTEREST + // Restriction type is REQUIRE_LEGITIMATE_INTEREST part @Test public void shouldEmptyWhenInGvlPurposeAndPurposeConsentAllowedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesConsent.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -542,22 +650,21 @@ public void shouldEmptyWhenInGvlPurposeAndPurposeConsentAllowedAndFlexibleAndReq public void shouldEmptyWhenInGvlPurposeAndPurposeConsentAndVendorConsentAndEnforcedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesConsent.contains(anyInt())).willReturn(true); given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -571,53 +678,53 @@ public void shouldEmptyWhenInGvlPurposeAndPurposeConsentAndVendorConsentAndEnfor public void shouldAllowWhenInGvlPurposeAndPurposeLIAllowedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); given(purposesLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesLI).contains(PURPOSE_ID); + verify(purposesLI).contains(PURPOSE_CODE.code()); } @Test public void shouldAllowWhenInGvlPurposeAndPurposeLIAndVendorLIAllowedAndEnforcedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); given(purposesLI.contains(anyInt())).willReturn(true); given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesLI).contains(PURPOSE_ID); + verify(purposesLI).contains(PURPOSE_CODE.code()); verify(allowedVendorsLI).contains(1); } @@ -625,22 +732,21 @@ public void shouldAllowWhenInGvlPurposeAndPurposeLIAndVendorLIAllowedAndEnforced public void shouldEmptyWhenInGvlPurposeAndPurposeConsentAndVendorLIAllowedAndEnforcedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesConsent.contains(anyInt())).willReturn(true); given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -654,86 +760,85 @@ public void shouldEmptyWhenInGvlPurposeAndPurposeConsentAndVendorLIAllowedAndEnf public void shouldEmptyWhenInGvlPurposeAndPurposeLIAndVendorConsentAllowedAndEnforcedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .purposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .purposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesLI.contains(anyInt())).willReturn(true); given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).isEmpty(); - verify(purposesLI).contains(PURPOSE_ID); + verify(purposesLI).contains(PURPOSE_CODE.code()); verifyZeroInteractions(allowedVendors); } // Flexible GVL Purpose Legitimate interest part - // Restriction type is REQUIRE_CONSENT + // Restriction type is REQUIRE_CONSENT part @Test public void shouldAllowWhenInGvlPurposeLIAndPurposeConsentAllowedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); given(purposesConsent.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesConsent).contains(PURPOSE_ID); + verify(purposesConsent).contains(PURPOSE_CODE.code()); } @Test public void shouldAllowWhenInGvlPurposeLIAndPurposeAndVendorConsentAndEnforcedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); given(purposesConsent.contains(anyInt())).willReturn(true); given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesConsent).contains(PURPOSE_ID); + verify(purposesConsent).contains(PURPOSE_CODE.code()); verify(allowedVendors).contains(1); } @@ -741,21 +846,20 @@ public void shouldAllowWhenInGvlPurposeLIAndPurposeAndVendorConsentAndEnforcedAn public void shouldEmptyWhenInGvlPurposeLIAndPurposeLIAllowedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -768,22 +872,21 @@ public void shouldEmptyWhenInGvlPurposeLIAndPurposeLIAllowedAndFlexibleAndRequir public void shouldEmptyWhenInGvlPurposeLIAndPurposeLIAndVendorLIAllowedAndEnforcedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesLI.contains(anyInt())).willReturn(true); given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -797,28 +900,27 @@ public void shouldEmptyWhenInGvlPurposeLIAndPurposeLIAndVendorLIAllowedAndEnforc public void shouldEmptyWhenInGvlPurposeLIAndPurposeConsentAndVendorLIAndEnforcedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesConsent.contains(anyInt())).willReturn(true); given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).isEmpty(); - verify(purposesConsent).contains(PURPOSE_ID); + verify(purposesConsent).contains(PURPOSE_CODE.code()); verifyZeroInteractions(allowedVendorsLI); } @@ -826,52 +928,50 @@ public void shouldEmptyWhenInGvlPurposeLIAndPurposeConsentAndVendorLIAndEnforced public void shouldEmptyWhenInGvlPurposeLIAndPurposeLIAndVendorConsentAndEnforcedAndFlexibleAndRequireConsent() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_CONSENT); + setRestriction(RestrictionType.REQUIRE_CONSENT); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesLI.contains(anyInt())).willReturn(true); given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).isEmpty(); - verify(allowedVendors).contains(PURPOSE_ID); + verify(allowedVendors).contains(PURPOSE_CODE.code()); verifyZeroInteractions(purposesLI); } - // Restriction type is REQUIRE_LEGITIMATE_INTEREST + // Restriction type is REQUIRE_LEGITIMATE_INTEREST part @Test public void shouldEmptyWhenInGvlPurposeLIAndPurposeConsentAllowedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesConsent.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -884,22 +984,21 @@ public void shouldEmptyWhenInGvlPurposeLIAndPurposeConsentAllowedAndFlexibleAndR public void shouldEmptyWhenInGvlPurposeLIAndPurposeConsentAndVendorConsentAndEnforcedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesConsent.contains(anyInt())).willReturn(true); given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -913,53 +1012,53 @@ public void shouldEmptyWhenInGvlPurposeLIAndPurposeConsentAndVendorConsentAndEnf public void shouldAllowWhenInGvlPurposeLIAndPurposeLIAllowedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); given(purposesLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesLI).contains(PURPOSE_ID); + verify(purposesLI).contains(PURPOSE_CODE.code()); } @Test public void shouldAllowWhenInGvlPurposeLIAndPurposeLIAndVendorLIAllowedAndEnforcedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); given(purposesLI.contains(anyInt())).willReturn(true); given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission); - verify(purposesLI).contains(PURPOSE_ID); + verify(purposesLI).contains(PURPOSE_CODE.code()); verify(allowedVendorsLI).contains(1); } @@ -967,22 +1066,21 @@ public void shouldAllowWhenInGvlPurposeLIAndPurposeLIAndVendorLIAllowedAndEnforc public void shouldEmptyWhenInGvlPurposeLIAndPurposeConsentAndVendorLIAllowedAndEnforcedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesConsent.contains(anyInt())).willReturn(true); given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -996,22 +1094,21 @@ public void shouldEmptyWhenInGvlPurposeLIAndPurposeConsentAndVendorLIAllowedAndE public void shouldEmptyWhenInGvlPurposeLIAndPurposeLIAndVendorConsentAllowedAndEnforcedAndFlexibleAndRequireLI() { // given final VendorV2 vendorGvl = VendorV2.builder() - .legIntPurposes(singleton(PURPOSE_ID)) - .flexiblePurposes(singleton(PURPOSE_ID)) + .legIntPurposes(EnumSet.of(PURPOSE_CODE)) + .flexiblePurposes(EnumSet.of(PURPOSE_CODE)) .build(); final VendorPermission vendorPermission = VendorPermission.of(1, null, PrivacyEnforcementAction.restrictAll()); final VendorPermissionWithGvl vendorPermissionWitGvl = VendorPermissionWithGvl.of(vendorPermission, vendorGvl); final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); - given(publisherRestriction.getRestrictionType()).willReturn(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); + setRestriction(RestrictionType.REQUIRE_LEGITIMATE_INTEREST); - given(vendorIds.contains(anyInt())).willReturn(true); given(purposesLI.contains(anyInt())).willReturn(true); given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -1032,11 +1129,15 @@ public void shouldReturnExcludedVendors() { VendorV2.empty(2)); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, singleton(vendorPermissionWitGvl1), singleton(vendorPermissionWitGvl2), true); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission2); } -} + private void setRestriction(RestrictionType requireConsent) { + given(publisherRestriction.getRestrictionType()).willReturn(requireConsent); + given(vendorIds.contains(anyInt())).willReturn(true); + } +} diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategyTest.java index d3e32351154..5942a673f90 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/NoEnforcePurposeStrategyTest.java @@ -11,6 +11,7 @@ import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import java.util.Arrays; @@ -26,7 +27,7 @@ public class NoEnforcePurposeStrategyTest { - private static final int PURPOSE_ID = 1; + private static final PurposeCode PURPOSE_CODE = PurposeCode.ONE; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -60,7 +61,7 @@ public void allowedByTypeStrategyShouldReturnExpectedListWhenVendorIsNotAllowedA final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -76,7 +77,7 @@ public void allowedByTypeStrategyShouldReturnEmptyListWhenVendorIsNotAllowedAndV final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -94,7 +95,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorIsAllowedAnd given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -112,7 +113,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorIsAllowedAnd given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -130,7 +131,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorLIIsAllowedA given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPurposeWithGvls, emptyList(), true); // then @@ -148,7 +149,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorLIIsAllowedA given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPurposeWithGvls, emptyList(), false); // then @@ -171,7 +172,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorIsAllowedFor given(allowedVendors.contains(eq(2))).willReturn(false); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPurposeWithGvls, emptyList(), false); // then @@ -193,7 +194,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorIsAllowedFor given(allowedVendors.contains(eq(2))).willReturn(false); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPurposeWithGvls, emptyList(), true); // then @@ -216,7 +217,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenPurposeAndVendorLI given(allowedVendorsLI.contains(eq(2))).willReturn(false); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPurposeWithGvls, emptyList(), false); // then @@ -239,7 +240,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenPurposeAndVendorLI given(allowedVendorsLI.contains(eq(2))).willReturn(false); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPurposeWithGvls, emptyList(), true); // then @@ -257,11 +258,10 @@ public void allowedByTypeStrategyShouldReturnExcludedVendors() { VendorV2.empty(1)); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, singletonList(vendorPermissionWitGvl1), singletonList(vendorPermissionWitGvl2), true); // then assertThat(result).usingFieldByFieldElementComparator().containsOnly(vendorPermission2); } } - diff --git a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/PurposeTwoBasicEnforcePurposeStrategyTest.java b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/PurposeTwoBasicEnforcePurposeStrategyTest.java index 796178df155..125ee817059 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/PurposeTwoBasicEnforcePurposeStrategyTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/typestrategies/PurposeTwoBasicEnforcePurposeStrategyTest.java @@ -11,6 +11,7 @@ import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; import org.prebid.server.privacy.gdpr.model.VendorPermission; import org.prebid.server.privacy.gdpr.model.VendorPermissionWithGvl; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import java.util.Arrays; @@ -26,7 +27,7 @@ public class PurposeTwoBasicEnforcePurposeStrategyTest { - private static final int PURPOSE_ID = 1; + private static final PurposeCode PURPOSE_CODE = PurposeCode.ONE; @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -68,7 +69,7 @@ public void allowedByTypeStrategyShouldReturnEmptyListWhenVendorIsNotAllowedAndV final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -84,7 +85,7 @@ public void allowedByTypeStrategyShouldReturnEmptyListWhenVendorIsNotAllowedAndV final List vendorPermissionWithGvls = singletonList(vendorPermissionWitGvl); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -102,7 +103,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorIsAllowedAnd given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -120,7 +121,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorIsAllowedAnd given(allowedVendors.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -138,7 +139,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorLIIsAllowedA given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -156,7 +157,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenVendorLIIsAllowedA given(allowedVendorsLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -175,7 +176,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenPurposeLIAndPurpos given(purposesLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -194,7 +195,7 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenPurposeLIAndPurpos given(purposesLI.contains(anyInt())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), true); // then @@ -214,10 +215,10 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenPurposeLIAndVendor vendorPermissionWitGvl2); given(allowedVendors.contains(anyInt())).willReturn(true); - given(purposesLI.contains(PURPOSE_ID)).willReturn(true); + given(purposesLI.contains(PURPOSE_CODE.code())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -237,10 +238,10 @@ public void allowedByTypeStrategyShouldReturnExpectedValueWhenPurposeAndVendorLI vendorPermissionWitGvl2); given(allowedVendorsLI.contains(anyInt())).willReturn(true); - given(purposesConsent.contains(PURPOSE_ID)).willReturn(true); + given(purposesConsent.contains(PURPOSE_CODE.code())).willReturn(true); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, vendorPermissionWithGvls, emptyList(), false); // then @@ -258,7 +259,7 @@ public void allowedByTypeStrategyShouldReturnExcludedVendors() { VendorV2.empty(2)); // when - final Collection result = target.allowedByTypeStrategy(PURPOSE_ID, tcString, + final Collection result = target.allowedByTypeStrategy(PURPOSE_CODE, tcString, singleton(vendorPermissionWitGvl1), singleton(vendorPermissionWitGvl2), true); // then diff --git a/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV1Test.java b/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV1Test.java deleted file mode 100644 index 49d75317568..00000000000 --- a/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV1Test.java +++ /dev/null @@ -1,521 +0,0 @@ -package org.prebid.server.privacy.gdpr.vendorlist; - -import com.fasterxml.jackson.core.JsonProcessingException; -import io.vertx.core.Future; -import io.vertx.core.Handler; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.file.FileSystem; -import org.apache.commons.lang3.StringUtils; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.mockito.stubbing.Answer; -import org.prebid.server.VertxTest; -import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.metric.Metrics; -import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorListV1; -import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV1; -import org.prebid.server.vertx.http.HttpClient; -import org.prebid.server.vertx.http.model.HttpClientResponse; - -import java.io.File; -import java.util.Date; -import java.util.Map; - -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.prebid.server.assertion.FutureAssertion.assertThat; - -public class VendorListServiceV1Test extends VertxTest { - - private static final String CACHE_DIR = "/cache/dir"; - private static final long REFRESH_MISSING_LIST_PERIOD_MS = 3600000L; - private static final String FALLBACK_VENDOR_LIST_PATH = "fallback.json"; - - @Rule - public final MockitoRule mockitoRule = MockitoJUnit.rule(); - - @Mock - private Vertx vertx; - @Mock - private FileSystem fileSystem; - @Mock - private HttpClient httpClient; - @Mock - private Metrics metrics; - @Mock - private BidderCatalog bidderCatalog; - - private VendorListService vendorListService; - - @Before - public void setUp() throws JsonProcessingException { - given(fileSystem.existsBlocking(anyString())).willReturn(false); // always create cache dir - - given(bidderCatalog.knownVendorIds()).willReturn(singleton(52)); - - given(fileSystem.readFileBlocking(eq(FALLBACK_VENDOR_LIST_PATH))) - .willReturn(Buffer.buffer(mapper.writeValueAsString(givenVendorList()))); - - vendorListService = new VendorListServiceV1( - CACHE_DIR, - "http://vendorlist/{VERSION}", - 0, - REFRESH_MISSING_LIST_PERIOD_MS, - null, - FALLBACK_VENDOR_LIST_PATH, - bidderCatalog, - vertx, - fileSystem, - httpClient, - metrics, - jacksonMapper); - } - - // Creation related tests - - @Test - public void creationShouldFailIfCannotCreateCacheDir() { - // given - given(fileSystem.mkdirsBlocking(anyString())).willThrow(new RuntimeException("dir creation error")); - - // then - assertThatThrownBy( - () -> new VendorListServiceV1( - CACHE_DIR, - "http://vendorlist/%s", - 0, - REFRESH_MISSING_LIST_PERIOD_MS, - null, - FALLBACK_VENDOR_LIST_PATH, - bidderCatalog, - vertx, - fileSystem, - httpClient, - metrics, - jacksonMapper)) - .hasMessage("dir creation error"); - } - - @Test - public void creationShouldFailIfCannotReadFiles() { - // given - given(fileSystem.readDirBlocking(anyString())).willThrow(new RuntimeException("read error")); - - // then - assertThatThrownBy( - () -> new VendorListServiceV1( - CACHE_DIR, - "http://vendorlist/%s", - 0, - REFRESH_MISSING_LIST_PERIOD_MS, - null, - FALLBACK_VENDOR_LIST_PATH, - bidderCatalog, - vertx, - fileSystem, - httpClient, - metrics, - jacksonMapper)) - .isInstanceOf(RuntimeException.class) - .hasMessage("read error"); - } - - @Test - public void creationShouldFailIfCannotReadAtLeastOneVendorListFile() { - // given - given(fileSystem.readDirBlocking(anyString())).willReturn(singletonList("1.json")); - given(fileSystem.readFileBlocking(anyString())).willThrow(new RuntimeException("read error")); - - // then - assertThatThrownBy( - () -> new VendorListServiceV1( - CACHE_DIR, - "http://vendorlist/%s", - 0, - REFRESH_MISSING_LIST_PERIOD_MS, - null, - FALLBACK_VENDOR_LIST_PATH, - bidderCatalog, - vertx, - fileSystem, - httpClient, - metrics, - jacksonMapper)) - .isInstanceOf(RuntimeException.class) - .hasMessage("read error"); - } - - @Test - public void creationShouldFailIfAtLeastOneVendorListFileCannotBeParsed() { - // given - given(fileSystem.readDirBlocking(anyString())).willReturn(singletonList("1.json")); - given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer("invalid")); - - // then - assertThatThrownBy( - () -> new VendorListServiceV1( - CACHE_DIR, - "http://vendorlist/%s", - 0, - REFRESH_MISSING_LIST_PERIOD_MS, - null, - FALLBACK_VENDOR_LIST_PATH, - bidderCatalog, - vertx, - fileSystem, - httpClient, - metrics, - jacksonMapper)) - .isInstanceOf(PreBidException.class) - .hasMessage("Cannot parse vendor list from: invalid"); - } - - // Http related tests - - @Test - public void shouldPerformHttpRequestWithExpectedQueryIfVendorListNotFound() { - // given - givenHttpClientReturnsResponse(200, null); - - // when - vendorListService.forVersion(1); - - // then - verify(httpClient).get(eq("http://vendorlist/1"), anyLong()); - } - - @Test - public void shouldNotAskToSaveFileIfReadingHttpResponseFails() { - // given - givenHttpClientProducesException(new RuntimeException("Response exception")); - - // when - vendorListService.forVersion(1); - - // then - verify(httpClient).get(anyString(), anyLong()); - verify(fileSystem, never()).writeFile(any(), any(), any()); - } - - @Test - public void shouldNotAskToSaveFileIfResponseCodeIsNot200() { - // given - givenHttpClientReturnsResponse(503, null); - - // when - vendorListService.forVersion(1); - - // then - verify(httpClient).get(anyString(), anyLong()); - verify(fileSystem, never()).writeFile(any(), any(), any()); - } - - @Test - public void shouldNotAskToSaveFileIfResponseBodyCouldNotBeParsed() { - // given - givenHttpClientReturnsResponse(200, "response"); - - // when - vendorListService.forVersion(1); - - // then - verify(httpClient).get(anyString(), anyLong()); - verify(fileSystem, never()).writeFile(any(), any(), any()); - } - - @Test - public void shouldNotAskToSaveFileIfFetchedVendorListHasInvalidVendorListVersion() throws JsonProcessingException { - // given - final VendorListV1 vendorList = VendorListV1.of(null, null, null); - givenHttpClientReturnsResponse(200, mapper.writeValueAsString(vendorList)); - - // when - vendorListService.forVersion(1); - - // then - verify(httpClient).get(anyString(), anyLong()); - verify(fileSystem, never()).writeFile(any(), any(), any()); - } - - @Test - public void shouldNotAskToSaveFileIfFetchedVendorListHasInvalidLastUpdated() throws JsonProcessingException { - // given - final VendorListV1 vendorList = VendorListV1.of(1, null, null); - givenHttpClientReturnsResponse(200, mapper.writeValueAsString(vendorList)); - - // when - vendorListService.forVersion(1); - - // then - verify(httpClient).get(anyString(), anyLong()); - verify(fileSystem, never()).writeFile(any(), any(), any()); - } - - @Test - public void shouldNotAskToSaveFileIfFetchedVendorListHasNoVendors() throws JsonProcessingException { - // given - final VendorListV1 vendorList = VendorListV1.of(1, new Date(), null); - givenHttpClientReturnsResponse(200, mapper.writeValueAsString(vendorList)); - - // when - vendorListService.forVersion(1); - - // then - verify(httpClient).get(anyString(), anyLong()); - verify(fileSystem, never()).writeFile(any(), any(), any()); - } - - @Test - public void shouldNotAskToSaveFileIfFetchedVendorListHasEmptyVendors() throws JsonProcessingException { - // given - final VendorListV1 vendorList = VendorListV1.of(1, new Date(), emptyList()); - givenHttpClientReturnsResponse(200, mapper.writeValueAsString(vendorList)); - - // when - vendorListService.forVersion(1); - - // then - verify(httpClient).get(anyString(), anyLong()); - verify(fileSystem, never()).writeFile(any(), any(), any()); - } - - @Test - public void shouldNotAskToSaveFileIfFetchedVendorListHasAtLeastOneInvalidVendor() throws JsonProcessingException { - // given - final VendorListV1 vendorList = VendorListV1.of(1, new Date(), singletonList(VendorV1.of(null, null, null))); - givenHttpClientReturnsResponse(200, mapper.writeValueAsString(vendorList)); - - // when - vendorListService.forVersion(1); - - // then - verify(httpClient).get(anyString(), anyLong()); - verify(fileSystem, never()).writeFile(any(), any(), any()); - } - - // File system related tests - - @Test - public void shouldSaveFileWithExpectedPathAndContentIfVendorListNotFound() throws JsonProcessingException { - // given - final String vendorListAsString = mapper.writeValueAsString(givenVendorList()); - givenHttpClientReturnsResponse(200, vendorListAsString); - // generate file path to avoid conflicts with path separators in different OS - final String filePath = new File("/cache/dir/1.json").getPath(); - - // when - vendorListService.forVersion(1); - - // then - verify(fileSystem).writeFile(eq(filePath), eq(Buffer.buffer(vendorListAsString)), any()); - } - - // In-memory cache related tests - - @Test - public void shouldFailIfVendorListIsBelowOrZero() { - // given - givenHttpClientProducesException(new RuntimeException()); - - // when - final Future result1 = vendorListService.forVersion(0); - final Future result2 = vendorListService.forVersion(-2); - - // then - assertThat(result1).isFailed().hasMessage("TCF 1 vendor list for version 0 not valid."); - assertThat(result2).isFailed().hasMessage("TCF 1 vendor list for version -2 not valid."); - } - - @Test - public void shouldFailIfVendorListNotFound() { - // given - givenHttpClientProducesException(new RuntimeException()); - - // when - final Future> future = vendorListService.forVersion(1); - - // then - assertThat(future).isFailed().hasMessage("TCF 1 vendor list for version 1 not fetched yet, try again later."); - } - - @Test - public void shouldReturnVendorListFromCache() throws JsonProcessingException { - // given - givenHttpClientReturnsResponse(200, mapper.writeValueAsString(givenVendorList())); - - given(fileSystem.writeFile(anyString(), any(), any())) - .willAnswer(withSelfAndPassObjectToHandler(Future.succeededFuture())); - - // when - vendorListService.forVersion(1); // populate cache - final Future> result = vendorListService.forVersion(1); - - // then - assertThat(result).succeededWith(singletonMap(52, VendorV1.of(52, singleton(1), singleton(2)))); - } - - @Test - public void shouldKeepPurposesOnlyForKnownVendors() throws JsonProcessingException { - // given - final VendorListV1 vendorList = VendorListV1.of(1, new Date(), - asList(VendorV1.of(52, singleton(1), singleton(2)), VendorV1.of(42, singleton(1), singleton(2)))); - givenHttpClientReturnsResponse(200, mapper.writeValueAsString(vendorList)); - - given(fileSystem.writeFile(anyString(), any(), any())) - .willAnswer(withSelfAndPassObjectToHandler(Future.succeededFuture())); - - // when - vendorListService.forVersion(1); // populate cache - final Future> future = vendorListService.forVersion(1); - - // then - assertThat(future).succeededWith(singletonMap(52, VendorV1.of(52, singleton(1), singleton(2)))); - } - - @Test - public void shouldReturnFallbackIfVendorListNotFound() { - // given - givenHttpClientReturnsResponse(404, StringUtils.EMPTY); - - // when - - // first call triggers http request that results in 404 - final Future> future1 = vendorListService.forVersion(1); - // second call yields fallback vendor list - final Future> future2 = vendorListService.forVersion(1); - - // then - assertThat(future1).isFailed(); - assertThat(future2).succeededWith(singletonMap(52, VendorV1.of(52, singleton(1), singleton(2)))); - } - - @Test - public void shouldReturnFallbackIfServerUnavailable() { - // given - givenHttpClientReturnsResponse(503, StringUtils.EMPTY); - - // when - - // first call triggers http request that results in 503 - final Future> future1 = vendorListService.forVersion(1); - // second call yields fallback vendor list - final Future> future2 = vendorListService.forVersion(1); - - // then - assertThat(future1).isFailed(); - assertThat(future2).succeededWith(singletonMap(52, VendorV1.of(52, singleton(1), singleton(2)))); - } - - // Metrics tests - - @Test - public void shouldIncrementVendorListMissingMetric() { - // given - givenHttpClientReturnsResponse(200, null); - - // when - vendorListService.forVersion(1); - - // then - verify(metrics).updatePrivacyTcfVendorListMissingMetric(eq(1)); - } - - @Test - public void shouldIncrementVendorListErrorMetricWhenFileIsNotDownloaded() { - // given - givenHttpClientReturnsResponse(503, null); - - // when - vendorListService.forVersion(1); - - // then - verify(metrics).updatePrivacyTcfVendorListErrorMetric(eq(1)); - } - - @Test - public void shouldIncrementVendorListErrorMetricWhenFileIsNotSaved() throws JsonProcessingException { - // given - givenHttpClientReturnsResponse(200, mapper.writeValueAsString(givenVendorList())); - - given(fileSystem.writeFile(anyString(), any(), any())) - .willAnswer(withSelfAndPassObjectToHandler(Future.failedFuture("error"))); - - // when - vendorListService.forVersion(1); - - // then - verify(metrics).updatePrivacyTcfVendorListErrorMetric(eq(1)); - } - - @Test - public void shouldIncrementVendorListOkMetric() throws JsonProcessingException { - // given - givenHttpClientReturnsResponse(200, mapper.writeValueAsString(givenVendorList())); - - given(fileSystem.writeFile(anyString(), any(), any())) - .willAnswer(withSelfAndPassObjectToHandler(Future.succeededFuture())); - - // when - vendorListService.forVersion(1); - - // then - verify(metrics).updatePrivacyTcfVendorListOkMetric(eq(1)); - } - - @Test - public void shouldIncrementVendorListFallbackMetric() { - // given - givenHttpClientReturnsResponse(404, StringUtils.EMPTY); - - // when - - // first call triggers http request that results in 404 - vendorListService.forVersion(1); - // second call yields fallback vendor list - vendorListService.forVersion(1); - - // then - verify(metrics).updatePrivacyTcfVendorListFallbackMetric(eq(1)); - } - - private static VendorListV1 givenVendorList() { - final VendorV1 vendor = VendorV1.of(52, singleton(1), singleton(2)); - return VendorListV1.of(1, new Date(), singletonList(vendor)); - } - - private void givenHttpClientReturnsResponse(int statusCode, String response) { - given(httpClient.get(anyString(), anyLong())) - .willReturn(Future.succeededFuture(HttpClientResponse.of(statusCode, null, response))); - } - - private void givenHttpClientProducesException(Throwable throwable) { - given(httpClient.get(anyString(), anyLong())) - .willReturn(Future.failedFuture(throwable)); - } - - @SuppressWarnings("unchecked") - private static Answer withSelfAndPassObjectToHandler(T obj) { - return inv -> { - // invoking handler right away passing mock to it - ((Handler) inv.getArgument(2)).handle(obj); - return inv.getMock(); - }; - } -} diff --git a/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV2Test.java b/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV2Test.java index c6c7a991a11..e21cfadf86a 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV2Test.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VendorListServiceV2Test.java @@ -18,6 +18,10 @@ import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.exception.PreBidException; import org.prebid.server.metric.Metrics; +import org.prebid.server.privacy.gdpr.vendorlist.proto.Feature; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; +import org.prebid.server.privacy.gdpr.vendorlist.proto.SpecialFeature; +import org.prebid.server.privacy.gdpr.vendorlist.proto.SpecialPurpose; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorListV2; import org.prebid.server.privacy.gdpr.vendorlist.proto.VendorV2; import org.prebid.server.vertx.http.HttpClient; @@ -25,11 +29,11 @@ import java.io.File; import java.util.Date; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import static java.util.Collections.emptyMap; -import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; @@ -41,7 +45,10 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.prebid.server.assertion.FutureAssertion.assertThat; +import static org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode.ONE; +import static org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode.TWO; public class VendorListServiceV2Test extends VertxTest { @@ -79,6 +86,7 @@ public void setUp() throws JsonProcessingException { "http://vendorlist/{VERSION}", 0, REFRESH_MISSING_LIST_PERIOD_MS, + false, null, FALLBACK_VENDOR_LIST_PATH, bidderCatalog, @@ -103,6 +111,7 @@ public void creationShouldFailsIfCannotCreateCacheDir() { "http://vendorlist/%s", 0, REFRESH_MISSING_LIST_PERIOD_MS, + false, null, FALLBACK_VENDOR_LIST_PATH, bidderCatalog, @@ -114,6 +123,62 @@ public void creationShouldFailsIfCannotCreateCacheDir() { .hasMessage("dir creation error"); } + @Test + public void shouldStartUsingFallbackVersionIfDeprecatedIsTrue() { + // given + vendorListService = new VendorListServiceV2( + CACHE_DIR, + "http://vendorlist/{VERSION}", + 0, + REFRESH_MISSING_LIST_PERIOD_MS, + true, + null, + FALLBACK_VENDOR_LIST_PATH, + bidderCatalog, + vertx, + fileSystem, + httpClient, + metrics, + jacksonMapper); + + // when + final Future> future = vendorListService.forVersion(1); + + // then + verifyZeroInteractions(httpClient); + assertThat(future).succeededWith(singletonMap( + 52, VendorV2.builder() + .id(52) + .purposes(EnumSet.of(ONE)) + .legIntPurposes(EnumSet.of(TWO)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .specialPurposes(EnumSet.noneOf(SpecialPurpose.class)) + .features(EnumSet.noneOf(Feature.class)) + .specialFeatures(EnumSet.noneOf(SpecialFeature.class)) + .build())); + } + + @Test + public void shouldThrowExceptionIfVersionIsDeprecatedAndNoFallbackPresent() { + // then + assertThatThrownBy(() -> new VendorListServiceV2( + CACHE_DIR, + "http://vendorlist/{VERSION}", + 0, + REFRESH_MISSING_LIST_PERIOD_MS, + true, + null, + null, + bidderCatalog, + vertx, + fileSystem, + httpClient, + metrics, + jacksonMapper)) + .isInstanceOf(PreBidException.class) + .hasMessage("No fallback vendorList for deprecated version present"); + } + @Test public void creationShouldFailsIfCannotReadFiles() { // given @@ -126,6 +191,7 @@ public void creationShouldFailsIfCannotReadFiles() { "http://vendorlist/%s", 0, REFRESH_MISSING_LIST_PERIOD_MS, + false, null, FALLBACK_VENDOR_LIST_PATH, bidderCatalog, @@ -151,6 +217,7 @@ public void creationShouldFailsIfCannotReadAtLeastOneVendorListFile() { "http://vendorlist/%s", 0, REFRESH_MISSING_LIST_PERIOD_MS, + false, null, FALLBACK_VENDOR_LIST_PATH, bidderCatalog, @@ -176,6 +243,7 @@ public void creationShouldFailsIfAtLeastOneVendorListFileCannotBeParsed() { "http://vendorlist/%s", 0, REFRESH_MISSING_LIST_PERIOD_MS, + false, null, FALLBACK_VENDOR_LIST_PATH, bidderCatalog, vertx, @@ -371,12 +439,12 @@ public void shouldReturnVendorListFromCache() throws JsonProcessingException { assertThat(result).succeededWith(singletonMap( 52, VendorV2.builder() .id(52) - .purposes(singleton(1)) - .legIntPurposes(singleton(2)) - .flexiblePurposes(emptySet()) - .specialPurposes(emptySet()) - .features(emptySet()) - .specialFeatures(emptySet()) + .purposes(EnumSet.of(ONE)) + .legIntPurposes(EnumSet.of(TWO)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .specialPurposes(EnumSet.noneOf(SpecialPurpose.class)) + .features(EnumSet.noneOf(Feature.class)) + .specialFeatures(EnumSet.noneOf(SpecialFeature.class)) .build())); } @@ -385,21 +453,21 @@ public void shouldKeepPurposesForAllVendors() throws JsonProcessingException { // given final VendorV2 firstExternalV2 = VendorV2.builder() .id(52) - .purposes(singleton(1)) - .legIntPurposes(singleton(2)) - .flexiblePurposes(emptySet()) - .specialPurposes(emptySet()) - .features(emptySet()) - .specialFeatures(emptySet()) + .purposes(EnumSet.of(ONE)) + .legIntPurposes(EnumSet.of(TWO)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .specialPurposes(EnumSet.noneOf(SpecialPurpose.class)) + .features(EnumSet.noneOf(Feature.class)) + .specialFeatures(EnumSet.noneOf(SpecialFeature.class)) .build(); final VendorV2 secondExternalV2 = VendorV2.builder() .id(42) - .purposes(singleton(1)) - .legIntPurposes(singleton(2)) - .flexiblePurposes(emptySet()) - .specialPurposes(emptySet()) - .features(emptySet()) - .specialFeatures(emptySet()) + .purposes(EnumSet.of(ONE)) + .legIntPurposes(EnumSet.of(TWO)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .specialPurposes(EnumSet.noneOf(SpecialPurpose.class)) + .features(EnumSet.noneOf(Feature.class)) + .specialFeatures(EnumSet.noneOf(SpecialFeature.class)) .build(); final Map idToVendor = new HashMap<>(); idToVendor.put(52, firstExternalV2); @@ -436,12 +504,12 @@ public void shouldReturnFallbackIfVendorListNotFound() { assertThat(future2).succeededWith(singletonMap( 52, VendorV2.builder() .id(52) - .purposes(singleton(1)) - .legIntPurposes(singleton(2)) - .flexiblePurposes(emptySet()) - .specialPurposes(emptySet()) - .features(emptySet()) - .specialFeatures(emptySet()) + .purposes(EnumSet.of(ONE)) + .legIntPurposes(EnumSet.of(TWO)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .specialPurposes(EnumSet.noneOf(SpecialPurpose.class)) + .features(EnumSet.noneOf(Feature.class)) + .specialFeatures(EnumSet.noneOf(SpecialFeature.class)) .build())); } @@ -520,12 +588,12 @@ public void shouldIncrementVendorListFallbackMetric() { private static VendorListV2 givenVendorList() { final VendorV2 vendor = VendorV2.builder() .id(52) - .purposes(singleton(1)) - .legIntPurposes(singleton(2)) - .flexiblePurposes(emptySet()) - .specialPurposes(emptySet()) - .features(emptySet()) - .specialFeatures(emptySet()) + .purposes(EnumSet.of(ONE)) + .legIntPurposes(EnumSet.of(TWO)) + .flexiblePurposes(EnumSet.noneOf(PurposeCode.class)) + .specialPurposes(EnumSet.noneOf(SpecialPurpose.class)) + .features(EnumSet.noneOf(Feature.class)) + .specialFeatures(EnumSet.noneOf(SpecialFeature.class)) .build(); return VendorListV2.of(1, new Date(), singletonMap(52, vendor)); } diff --git a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java index 6e2b0b90a7b..9022be3a384 100644 --- a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java @@ -11,7 +11,10 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; @@ -43,6 +46,8 @@ public class CachingApplicationSettingsTest { @Mock private ApplicationSettings applicationSettings; + @Mock + private Metrics metrics; private CachingApplicationSettings cachingApplicationSettings; @@ -52,14 +57,25 @@ public class CachingApplicationSettingsTest { public void setUp() { timeout = new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())).create(500L); - cachingApplicationSettings = new CachingApplicationSettings(applicationSettings, new SettingsCache(360, 100), - new SettingsCache(360, 100), new SettingsCache(360, 100), 360, 100); + cachingApplicationSettings = new CachingApplicationSettings( + applicationSettings, + new SettingsCache(360, 100), + new SettingsCache(360, 100), + new SettingsCache(360, 100), + metrics, + 360, + 100); } @Test public void getAccountByIdShouldReturnResultFromCacheOnSuccessiveCalls() { // given - final Account account = Account.builder().id("accountId").priceGranularity("med").build(); + final Account account = Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .priceGranularity("med") + .build()) + .build(); given(applicationSettings.getAccountById(eq("accountId"), same(timeout))) .willReturn(Future.succeededFuture(account)); @@ -133,110 +149,57 @@ public void getAccountByIdShouldNotCacheNotPreBidException() { } @Test - public void getAdUnitConfigByIdShouldReturnResultFromCacheOnSuccessiveCalls() { - // given - given(applicationSettings.getAdUnitConfigById(eq("adUnitConfigId"), same(timeout))) - .willReturn(Future.succeededFuture("config")); - - // when - final Future future = cachingApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); - cachingApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); - - // then - assertThat(future.succeeded()).isTrue(); - assertThat(future.result()).isEqualTo("config"); - verify(applicationSettings).getAdUnitConfigById(eq("adUnitConfigId"), same(timeout)); - verifyNoMoreInteractions(applicationSettings); - } - - @Test - public void getAdUnitConfigByIdShouldPropagateFailure() { - // given - given(applicationSettings.getAdUnitConfigById(anyString(), any())) - .willReturn(Future.failedFuture(new PreBidException("error"))); - - // when - final Future future = - cachingApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); - - // then - assertThat(future.failed()).isTrue(); - assertThat(future.cause()) - .isInstanceOf(PreBidException.class) - .hasMessage("error"); - } - - @Test - public void getAdUnitConfigByIdShouldCachePreBidException() { - // given - given(applicationSettings.getAdUnitConfigById(anyString(), any())) - .willReturn(Future.failedFuture(new PreBidException("error"))); - - // when - cachingApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); - cachingApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); - cachingApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); - final Future lastFuture = - cachingApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); - - // then - verify(applicationSettings).getAdUnitConfigById(anyString(), any()); - assertThat(lastFuture.failed()).isTrue(); - assertThat(lastFuture.cause()) - .isInstanceOf(PreBidException.class) - .hasMessage("error"); - } - - @Test - public void getAdUnitConfigByIdShouldNotCacheNotPreBidException() { + public void getAccountByIdShouldUpdateMetrics() { // given - given(applicationSettings.getAdUnitConfigById(anyString(), any())) - .willReturn(Future.failedFuture(new InvalidRequestException("error"))); + final Account account = Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .priceGranularity("med") + .build()) + .build(); + given(applicationSettings.getAccountById(eq("accountId"), same(timeout))) + .willReturn(Future.succeededFuture(account)); // when - - cachingApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); - cachingApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); - final Future lastFuture = - cachingApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); + cachingApplicationSettings.getAccountById("accountId", timeout); + cachingApplicationSettings.getAccountById("accountId", timeout); // then - verify(applicationSettings, times(3)).getAdUnitConfigById(anyString(), any()); - assertThat(lastFuture.failed()).isTrue(); - assertThat(lastFuture.cause()) - .isInstanceOf(InvalidRequestException.class) - .hasMessage("error"); + verify(metrics).updateSettingsCacheEventMetric(eq(MetricName.account), eq(MetricName.miss)); + verify(metrics).updateSettingsCacheEventMetric(eq(MetricName.account), eq(MetricName.hit)); } @Test public void getStoredDataShouldReturnResultOnSuccessiveCalls() { // given - given(applicationSettings.getStoredData(eq(singleton("reqid")), eq(singleton("impid")), same(timeout))) + given(applicationSettings.getStoredData(any(), eq(singleton("reqid")), eq(singleton("impid")), same(timeout))) .willReturn(Future.succeededFuture(StoredDataResult.of( singletonMap("reqid", "json"), singletonMap("impid", "json2"), emptyList()))); // when final Future future = - cachingApplicationSettings.getStoredData(singleton("reqid"), singleton("impid"), timeout); - cachingApplicationSettings.getStoredData(singleton("reqid"), singleton("impid"), timeout); // second call + cachingApplicationSettings.getStoredData("1001", singleton("reqid"), singleton("impid"), timeout); + // second call + cachingApplicationSettings.getStoredData("1001", singleton("reqid"), singleton("impid"), timeout); // then assertThat(future.succeeded()).isTrue(); assertThat(future.result()).isEqualTo(StoredDataResult.of( singletonMap("reqid", "json"), singletonMap("impid", "json2"), emptyList())); - verify(applicationSettings).getStoredData(eq(singleton("reqid")), eq(singleton("impid")), same(timeout)); + verify(applicationSettings) + .getStoredData(eq("1001"), eq(singleton("reqid")), eq(singleton("impid")), same(timeout)); verifyNoMoreInteractions(applicationSettings); } @Test public void getStoredDataShouldPropagateFailure() { // given - given(applicationSettings.getStoredData(anySet(), anySet(), any())) + given(applicationSettings.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.failedFuture(new InvalidRequestException("error"))); // when final Future future = - cachingApplicationSettings.getStoredData(singleton("id"), emptySet(), timeout); + cachingApplicationSettings.getStoredData(null, singleton("id"), emptySet(), timeout); // then assertThat(future.failed()).isTrue(); @@ -248,13 +211,13 @@ public void getStoredDataShouldPropagateFailure() { @Test public void getStoredDataShouldReturnResultWithErrorsOnNotSuccessiveCallToCacheAndErrorInDelegateCall() { // given - given(applicationSettings.getStoredData(eq(singleton("id")), eq(emptySet()), any())) + given(applicationSettings.getStoredData(any(), eq(singleton("id")), eq(emptySet()), any())) .willReturn(Future.succeededFuture(StoredDataResult.of( emptyMap(), emptyMap(), singletonList("error")))); // when final Future future = - cachingApplicationSettings.getStoredData(singleton("id"), emptySet(), timeout); + cachingApplicationSettings.getStoredData(null, singleton("id"), emptySet(), timeout); // then assertThat(future.succeeded()).isTrue(); @@ -262,6 +225,30 @@ public void getStoredDataShouldReturnResultWithErrorsOnNotSuccessiveCallToCacheA .isEqualTo(StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error"))); } + @Test + public void getStoredDataShouldReturnResultWithErrorIfAccountDiffers() { + // given + given(applicationSettings.getStoredData(any(), any(), any(), any())) + .willReturn(Future.succeededFuture( + StoredDataResult.of(singletonMap("reqid", "json"), emptyMap(), emptyList()))) + .willReturn(Future.failedFuture("error")); + + // when + cachingApplicationSettings.getStoredData("1001", singleton("reqid"), emptySet(), timeout); + // second call + final Future future = + cachingApplicationSettings.getStoredData("1002", singleton("reqid"), emptySet(), timeout); + + // then + assertThat(future.failed()).isTrue(); + assertThat(future.cause()).hasMessage("error"); + verify(applicationSettings) + .getStoredData(eq("1001"), eq(singleton("reqid")), eq(emptySet()), same(timeout)); + verify(applicationSettings) + .getStoredData(eq("1002"), eq(singleton("reqid")), eq(emptySet()), same(timeout)); + verifyNoMoreInteractions(applicationSettings); + } + @Test public void getStoredResponseShouldPropagateFailure() { // given diff --git a/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java index 3cc6abb814c..4230a5308cc 100644 --- a/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/CompositeApplicationSettingsTest.java @@ -10,6 +10,7 @@ import org.mockito.junit.MockitoRule; import org.prebid.server.exception.PreBidException; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; @@ -60,7 +61,12 @@ public void creationShouldFailOnEmptyDelegates() { @Test public void getAccountByIdShouldReturnAccountFromFirstDelegateIfPresent() { // given - final Account account = Account.builder().id("accountId").priceGranularity("low").build(); + final Account account = Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .priceGranularity("low") + .build()) + .build(); given(delegate1.getAccountById(anyString(), any())).willReturn(Future.succeededFuture(account)); // when @@ -78,7 +84,12 @@ public void getAccountByIdShouldReturnAccountFromSecondDelegateIfFirstDelegateFa given(delegate1.getAccountById(anyString(), any())) .willReturn(Future.failedFuture(new PreBidException("error1"))); - final Account account = Account.builder().id("accountId").priceGranularity("low").build(); + final Account account = Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .priceGranularity("low") + .build()) + .build(); given(delegate2.getAccountById(anyString(), any())) .willReturn(Future.succeededFuture(account)); @@ -107,66 +118,17 @@ public void getAccountByIdShouldReturnEmptyResultIfAllDelegatesFail() { assertThat(future.cause().getMessage()).isEqualTo("error2"); } - @Test - public void getAdUnitConfigByIdShouldReturnConfigFromFirstDelegateIfPresent() { - // given - given(delegate1.getAdUnitConfigById(anyString(), any())) - .willReturn(Future.succeededFuture("adUnitConfig1")); - - // when - final Future future = compositeApplicationSettings.getAdUnitConfigById("ignore", null); - - // then - assertThat(future.succeeded()).isTrue(); - assertThat(future.result()).isEqualTo("adUnitConfig1"); - verifyZeroInteractions(delegate2); - } - - @Test - public void getAdUnitConfigByIdShouldReturnConfigFromSecondDelegateIfFirstDelegateFails() { - // given - given(delegate1.getAdUnitConfigById(anyString(), any())) - .willReturn(Future.failedFuture(new PreBidException("error1"))); - - given(delegate2.getAdUnitConfigById(anyString(), any())) - .willReturn(Future.succeededFuture("adUnitConfig1")); - - // when - final Future future = compositeApplicationSettings.getAdUnitConfigById("ignore", null); - - // then - assertThat(future.succeeded()).isTrue(); - assertThat(future.result()).isEqualTo("adUnitConfig1"); - } - - @Test - public void getAdUnitConfigByIdShouldReturnEmptyResultIfAllDelegatesFail() { - // given - given(delegate1.getAdUnitConfigById(anyString(), any())) - .willReturn(Future.failedFuture(new PreBidException("error1"))); - - given(delegate2.getAdUnitConfigById(anyString(), any())) - .willReturn(Future.failedFuture(new PreBidException("error2"))); - - // when - final Future future = compositeApplicationSettings.getAdUnitConfigById("ignore", null); - - // then - assertThat(future.failed()).isTrue(); - assertThat(future.cause().getMessage()).isEqualTo("error2"); - } - @Test public void getStoredDataShouldReturnResultFromFirstDelegateIfPresent() { // given - given(delegate1.getStoredData(anySet(), anySet(), any())) + given(delegate1.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(singletonMap("key1", "value1"), singletonMap("key2", "value2"), emptyList()))); // when final Future future = - compositeApplicationSettings.getStoredData(singleton("key1"), singleton("key2"), null); + compositeApplicationSettings.getStoredData(null, singleton("key1"), singleton("key2"), null); // then assertThat(future.succeeded()).isTrue(); @@ -182,18 +144,18 @@ public void getStoredDataShouldReturnResultFromFirstDelegateIfPresent() { @Test public void getStoredDataShouldReturnResultFromFromSecondDelegateIfFirstDelegateFails() { // given - given(delegate1.getStoredData(anySet(), anySet(), any())) + given(delegate1.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error1")))); - given(delegate2.getStoredData(anySet(), anySet(), any())) + given(delegate2.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(singletonMap("key1", "value1"), singletonMap("key2", "value2"), emptyList()))); // when final Future future = - compositeApplicationSettings.getStoredData(singleton("key1"), singleton("key2"), null); + compositeApplicationSettings.getStoredData(null, singleton("key1"), singleton("key2"), null); // then assertThat(future.succeeded()).isTrue(); @@ -208,17 +170,17 @@ public void getStoredDataShouldReturnResultFromFromSecondDelegateIfFirstDelegate @Test public void getStoredDataShouldReturnEmptyResultIfAllDelegatesFail() { // given - given(delegate1.getStoredData(anySet(), anySet(), any())) + given(delegate1.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error1")))); - given(delegate2.getStoredData(anySet(), anySet(), any())) + given(delegate2.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error2")))); // when final Future future = - compositeApplicationSettings.getStoredData(singleton("key1"), emptySet(), null); + compositeApplicationSettings.getStoredData(null, singleton("key1"), emptySet(), null); // then assertThat(future.succeeded()).isTrue(); @@ -230,20 +192,20 @@ public void getStoredDataShouldReturnEmptyResultIfAllDelegatesFail() { @Test public void getStoredDataShouldPassOnlyMissingIdsToSecondDelegateIfFirstDelegateAlreadyObtainedThey() { // given - given(delegate1.getStoredData(anySet(), anySet(), any())) + given(delegate1.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(singletonMap("key1", "value1"), singletonMap("key3", "value3"), singletonList("error1")))); // when - compositeApplicationSettings.getStoredData(new HashSet<>(asList("key1", "key2")), + compositeApplicationSettings.getStoredData(null, new HashSet<>(asList("key1", "key2")), new HashSet<>(asList("key3", "key4")), null); // then @SuppressWarnings("unchecked") final ArgumentCaptor> requestCaptor = ArgumentCaptor.forClass( Set.class); @SuppressWarnings("unchecked") final ArgumentCaptor> impCaptor = ArgumentCaptor.forClass(Set.class); - verify(delegate2).getStoredData(requestCaptor.capture(), impCaptor.capture(), any()); + verify(delegate2).getStoredData(any(), requestCaptor.capture(), impCaptor.capture(), any()); assertThat(requestCaptor.getValue()).hasSize(1) .containsOnly("key2"); @@ -254,19 +216,19 @@ public void getStoredDataShouldPassOnlyMissingIdsToSecondDelegateIfFirstDelegate @Test public void getStoredDataShouldReturnResultConsequentlyFromAllDelegates() { // given - given(delegate1.getStoredData(anySet(), anySet(), any())) + given(delegate1.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(singletonMap("key1", "value1"), singletonMap("key3", "value3"), asList("key2 not found", "key4 not found")))); - given(delegate2.getStoredData(anySet(), anySet(), any())) + given(delegate2.getStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(singletonMap("key2", "value2"), singletonMap("key4", "value4"), emptyList()))); // when final Future future = - compositeApplicationSettings.getStoredData(new HashSet<>(asList("key1", "key2")), + compositeApplicationSettings.getStoredData(null, new HashSet<>(asList("key1", "key2")), new HashSet<>(asList("key3", "key4")), null); // then @@ -285,13 +247,13 @@ public void getStoredDataShouldReturnResultConsequentlyFromAllDelegates() { @Test public void getAmpStoredDataShouldReturnResultFromFirstDelegateIfPresent() { // given - given(delegate1.getAmpStoredData(anySet(), anySet(), any())) + given(delegate1.getAmpStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(singletonMap("key1", "value1"), emptyMap(), emptyList()))); // when final Future future = - compositeApplicationSettings.getAmpStoredData(singleton("key1"), emptySet(), null); + compositeApplicationSettings.getAmpStoredData(null, singleton("key1"), emptySet(), null); // then assertThat(future.succeeded()).isTrue(); @@ -305,17 +267,17 @@ public void getAmpStoredDataShouldReturnResultFromFirstDelegateIfPresent() { @Test public void getAmpStoredDataShouldReturnResultFromFromSecondDelegateIfFirstDelegateFails() { // given - given(delegate1.getAmpStoredData(anySet(), anySet(), any())) + given(delegate1.getAmpStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error1")))); - given(delegate2.getAmpStoredData(anySet(), anySet(), any())) + given(delegate2.getAmpStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(singletonMap("key1", "value1"), emptyMap(), emptyList()))); // when final Future future = - compositeApplicationSettings.getAmpStoredData(singleton("key1"), emptySet(), null); + compositeApplicationSettings.getAmpStoredData(null, singleton("key1"), emptySet(), null); // then assertThat(future.succeeded()).isTrue(); @@ -328,17 +290,17 @@ public void getAmpStoredDataShouldReturnResultFromFromSecondDelegateIfFirstDeleg @Test public void getAmpStoredDataShouldReturnEmptyResultIfAllDelegatesFail() { // given - given(delegate1.getAmpStoredData(anySet(), anySet(), any())) + given(delegate1.getAmpStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error1")))); - given(delegate2.getAmpStoredData(anySet(), anySet(), any())) + given(delegate2.getAmpStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(emptyMap(), emptyMap(), singletonList("error2")))); // when final Future future = - compositeApplicationSettings.getAmpStoredData(singleton("key1"), emptySet(), null); + compositeApplicationSettings.getAmpStoredData(null, singleton("key1"), emptySet(), null); // then assertThat(future.succeeded()).isTrue(); @@ -350,17 +312,18 @@ public void getAmpStoredDataShouldReturnEmptyResultIfAllDelegatesFail() { @Test public void getAmpStoredDataShouldPassOnlyMissingIdsToSecondDelegateIfFirstDelegateAlreadyObtainedThey() { // given - given(delegate1.getAmpStoredData(anySet(), anySet(), any())) + given(delegate1.getAmpStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(singletonMap("key1", "value1"), emptyMap(), singletonList("error1")))); // when - compositeApplicationSettings.getAmpStoredData(new HashSet<>(asList("key1", "key2")), emptySet(), null); + compositeApplicationSettings.getAmpStoredData(null, new HashSet<>(asList("key1", "key2")), emptySet(), + null); // then @SuppressWarnings("unchecked") final ArgumentCaptor> requestCaptor = ArgumentCaptor.forClass( Set.class); - verify(delegate2).getAmpStoredData(requestCaptor.capture(), anySet(), any()); + verify(delegate2).getAmpStoredData(any(), requestCaptor.capture(), anySet(), any()); assertThat(requestCaptor.getValue()).hasSize(1) .containsOnly("key2"); @@ -369,18 +332,18 @@ public void getAmpStoredDataShouldPassOnlyMissingIdsToSecondDelegateIfFirstDeleg @Test public void getAmpStoredDataShouldReturnResultConsequentlyFromAllDelegates() { // given - given(delegate1.getAmpStoredData(anySet(), anySet(), any())) + given(delegate1.getAmpStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(singletonMap("key1", "value1"), emptyMap(), singletonList("key2 not found")))); - given(delegate2.getAmpStoredData(anySet(), anySet(), any())) + given(delegate2.getAmpStoredData(any(), anySet(), anySet(), any())) .willReturn(Future.succeededFuture( StoredDataResult.of(singletonMap("key2", "value2"), emptyMap(), emptyList()))); // when - final Future future = - compositeApplicationSettings.getAmpStoredData(new HashSet<>(asList("key1", "key2")), emptySet(), null); + final Future future = compositeApplicationSettings.getAmpStoredData(null, + new HashSet<>(asList("key1", "key2")), emptySet(), null); // then assertThat(future.succeeded()).isTrue(); @@ -406,7 +369,7 @@ public void getStoredResponsesShouldReturnResultFromFirstDelegateIfPresent() { assertThat(future.succeeded()).isTrue(); assertThat(future.result()).isNotNull(); assertThat(future.result().getErrors()).isEmpty(); - assertThat(future.result().getStoredSeatBid()).hasSize(1) + assertThat(future.result().getIdToStoredResponses()).hasSize(1) .containsOnly(entry("key1", "value1")); verifyZeroInteractions(delegate2); } @@ -429,7 +392,7 @@ public void getStoredResponsesShouldReturnResultFromFromSecondDelegateIfFirstDel assertThat(future.succeeded()).isTrue(); assertThat(future.result()).isNotNull(); assertThat(future.result().getErrors()).isEmpty(); - assertThat(future.result().getStoredSeatBid()).hasSize(1) + assertThat(future.result().getIdToStoredResponses()).hasSize(1) .containsOnly(entry("key1", "value1")); } @@ -450,7 +413,7 @@ public void getStoredResponsesShouldReturnEmptyResultIfAllDelegatesFail() { // then assertThat(future.succeeded()).isTrue(); - assertThat(future.result().getStoredSeatBid()).isEmpty(); + assertThat(future.result().getIdToStoredResponses()).isEmpty(); assertThat(future.result().getErrors()).hasSize(1) .containsOnly("error2"); } @@ -491,7 +454,7 @@ public void getStoredResponsesShouldReturnResultConsequentlyFromAllDelegates() { // then assertThat(future.succeeded()).isTrue(); assertThat(future.result().getErrors()).isEmpty(); - assertThat(future.result().getStoredSeatBid()).hasSize(2) + assertThat(future.result().getIdToStoredResponses()).hasSize(2) .containsOnly( entry("key1", "value1"), entry("key2", "value2")); diff --git a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java new file mode 100644 index 00000000000..00b8938378e --- /dev/null +++ b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java @@ -0,0 +1,159 @@ +package org.prebid.server.settings; + +import io.vertx.core.Future; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.execution.Timeout; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; +import org.prebid.server.settings.model.EnabledForRequestType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.prebid.server.assertion.FutureAssertion.assertThat; + +public class EnrichingApplicationSettingsTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private ApplicationSettings delegate; + private final JsonMerger jsonMerger = new JsonMerger(jacksonMapper); + + private EnrichingApplicationSettings enrichingApplicationSettings; + + @Mock + private Timeout timeout; + + @Test + public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsNull() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings(null, delegate, jsonMerger); + + final Account returnedAccount = Account.builder().build(); + given(delegate.getAccountById(anyString(), any())).willReturn(Future.succeededFuture(returnedAccount)); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).isSucceeded(); + assertThat(accountFuture.result()).isSameAs(returnedAccount); + + verify(delegate).getAccountById(eq("123"), eq(timeout)); + } + + @Test + public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsEmpty() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings( + "{}", + delegate, + jsonMerger); + + final Account returnedAccount = Account.builder().build(); + given(delegate.getAccountById(anyString(), any())).willReturn(Future.succeededFuture(returnedAccount)); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).isSucceeded(); + assertThat(accountFuture.result()).isSameAs(returnedAccount); + + verify(delegate).getAccountById(eq("123"), eq(timeout)); + } + + @Test + public void getAccountByIdShouldMergeAccountWithDefaultAccount() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings( + "{\"auction\": {\"banner-cache-ttl\": 100}," + + "\"privacy\": {\"gdpr\": {\"enabled\": true, \"integration-enabled\": {\"web\": false}}}}", + delegate, + jsonMerger); + + given(delegate.getAccountById(anyString(), any())).willReturn(Future.succeededFuture(Account.builder() + .id("123") + .auction(AccountAuctionConfig.builder() + .videoCacheTtl(200) + .build()) + .privacy(AccountPrivacyConfig.of( + true, + AccountGdprConfig.builder() + .enabledForRequestType(EnabledForRequestType.of(true, null, null, null)) + .build(), + null)) + .build())); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).succeededWith(Account.builder() + .id("123") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(100) + .videoCacheTtl(200) + .build()) + .privacy(AccountPrivacyConfig.of( + true, + AccountGdprConfig.builder() + .enabled(true) + .enabledForRequestType(EnabledForRequestType.of(true, null, null, null)) + .build(), + null)) + .build()); + } + + @Test + public void getAccountByIdShouldReturnDefaultAccountWhenDelegateFailed() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings( + "{\"auction\": {\"banner-cache-ttl\": 100}}", + delegate, + jsonMerger); + + given(delegate.getAccountById(anyString(), any())).willReturn(Future.failedFuture("Exception")); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).succeededWith(Account.builder() + .id("123") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(100) + .build()) + .build()); + } + + @Test + public void getAccountByIdShouldPassOnFailureWhenDefaultAccountIsEmpty() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings( + "{}", + delegate, + jsonMerger); + + given(delegate.getAccountById(anyString(), any())).willReturn(Future.failedFuture("Exception")); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).isFailed(); + } +} diff --git a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java index 6ae3a6c0eaf..05377eecfe4 100644 --- a/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/FileApplicationSettingsTest.java @@ -8,9 +8,17 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountBidValidationConfig; +import org.prebid.server.settings.model.AccountCookieSyncConfig; +import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; +import org.prebid.server.settings.model.AccountStatus; +import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.settings.model.EnabledForRequestType; import org.prebid.server.settings.model.EnforcePurpose; import org.prebid.server.settings.model.Purpose; @@ -36,7 +44,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -public class FileApplicationSettingsTest { +public class FileApplicationSettingsTest extends VertxTest { @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -57,7 +65,7 @@ public void creationShouldFailIfFileCouldNotBeParsed() { @Test public void getAccountByIdShouldReturnEmptyWhenAccountsAreMissing() { // given - given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer("configs:")); + given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer("domains:")); final FileApplicationSettings applicationSettings = new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore"); @@ -75,19 +83,30 @@ public void getAccountByIdShouldReturnPresentAccount() { given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer( "accounts: [" + "{" - + "id: '123'," - + "priceGranularity: 'low'," - + "bannerCacheTtl: '100'," - + "videoCacheTtl : '100'," - + "eventsEnabled: 'true'," - + "enforceCcpa: 'true'," + + "id: 123," + + "status: active," + + "auction: {" + + "price-granularity: low," + + "banner-cache-ttl: 100," + + "video-cache-ttl : 100," + + "truncate-target-attr: 20," + + "default-integration: web," + + "bid-validations: {" + + "banner-creative-max-size: enforce" + + "}," + + "events: {" + + "enabled: true" + + "}" + + "}," + + "privacy: {" + + "enforce-ccpa: true," + "gdpr: {" - + "enabled: 'true'," + + "enabled: true," + "integration-enabled: {" - + "amp: 'true'," - + "web: 'true'," - + "video: 'true'," - + "app: 'true'" + + "amp: true," + + "web: true," + + "video: true," + + "app: true" + "}," + "purposes: {" + "p1: {enforce-purpose: basic,enforce-vendors: false,vendor-exceptions: [rubicon, appnexus]}," @@ -98,13 +117,13 @@ public void getAccountByIdShouldReturnPresentAccount() { + "sf2: {enforce: false,vendor-exceptions: [openx]}" + "}," + "purpose-one-treatment-interpretation: access-allowed" - + "}," - + "analyticsSamplingFactor : '1'," - + "truncateTargetAttr: '20'," - + "defaultIntegration: 'web'," - + "analyticsConfig: {" - + "auction-events: {amp: 'true'}" + "}" + + "}," + + "analytics: {" + + "auction-events: {amp: true}," + + "modules: {some-analytics: {supported-endpoints: [auction]}}" + + "}," + + "cookie-sync: {default-limit: 5,max-limit: 8,default-coop-sync: true}" + "}" + "]")); @@ -118,28 +137,39 @@ public void getAccountByIdShouldReturnPresentAccount() { assertThat(account.succeeded()).isTrue(); assertThat(account.result()).isEqualTo(Account.builder() .id("123") - .priceGranularity("low") - .bannerCacheTtl(100) - .videoCacheTtl(100) - .eventsEnabled(true) - .enforceCcpa(true) - .gdpr(AccountGdprConfig.builder() - .enabled(true) - .enabledForRequestType(EnabledForRequestType.of(true, true, true, true)) - .purposes(Purposes.builder() - .p1(Purpose.of(EnforcePurpose.basic, false, asList("rubicon", "appnexus"))) - .p2(Purpose.of(EnforcePurpose.full, true, singletonList("openx"))) - .build()) - .specialFeatures(SpecialFeatures.builder() - .sf1(SpecialFeature.of(true, asList("rubicon", "appnexus"))) - .sf2(SpecialFeature.of(false, singletonList("openx"))) - .build()) - .purposeOneTreatmentInterpretation(PurposeOneTreatmentInterpretation.accessAllowed) + .status(AccountStatus.active) + .auction(AccountAuctionConfig.builder() + .priceGranularity("low") + .bannerCacheTtl(100) + .videoCacheTtl(100) + .truncateTargetAttr(20) + .defaultIntegration("web") + .bidValidations(AccountBidValidationConfig.of(BidValidationEnforcement.enforce)) + .events(AccountEventsConfig.of(true)) .build()) - .analyticsSamplingFactor(1) - .truncateTargetAttr(20) - .defaultIntegration("web") - .analyticsConfig(AccountAnalyticsConfig.of(singletonMap("amp", true))) + .privacy(AccountPrivacyConfig.of( + true, + AccountGdprConfig.builder() + .enabled(true) + .enabledForRequestType(EnabledForRequestType.of(true, true, true, true)) + .purposes(Purposes.builder() + .p1(Purpose.of(EnforcePurpose.basic, false, asList("rubicon", "appnexus"))) + .p2(Purpose.of(EnforcePurpose.full, true, singletonList("openx"))) + .build()) + .specialFeatures(SpecialFeatures.builder() + .sf1(SpecialFeature.of(true, asList("rubicon", "appnexus"))) + .sf2(SpecialFeature.of(false, singletonList("openx"))) + .build()) + .purposeOneTreatmentInterpretation(PurposeOneTreatmentInterpretation.accessAllowed) + .build(), + null)) + .analytics(AccountAnalyticsConfig.of( + singletonMap("amp", true), + singletonMap( + "some-analytics", + mapper.createObjectNode() + .set("supported-endpoints", mapper.createArrayNode().add("auction"))))) + .cookieSync(AccountCookieSyncConfig.of(5, 8, true)) .build()); } @@ -159,56 +189,6 @@ public void getAccountByIdShouldReturnEmptyForUnknownAccount() { assertThat(account.failed()).isTrue(); } - @Test - public void getAdUnitConfigByIdShouldReturnEmptyWhenConfigsAreMissing() { - // given - given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer("accounts:")); - - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore"); - - // when - final Future config = applicationSettings.getAdUnitConfigById("123", null); - - // then - assertThat(config.failed()).isTrue(); - } - - @Test - public void getAdUnitConfigByIdShouldReturnPresentConfig() { - // given - given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer( - "configs: [ {id: '123', config: '{\"bidder\": \"rubicon\"}'}, {id: '456'} ]")); - - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore"); - - // when - final Future adUnitConfigById1 = applicationSettings.getAdUnitConfigById("123", null); - final Future adUnitConfigById2 = applicationSettings.getAdUnitConfigById("456", null); - - // then - assertThat(adUnitConfigById1.succeeded()).isTrue(); - assertThat(adUnitConfigById1.result()).isEqualTo("{\"bidder\": \"rubicon\"}"); - assertThat(adUnitConfigById2.succeeded()).isTrue(); - assertThat(adUnitConfigById2.result()).isEqualTo(""); - } - - @Test - public void getAdUnitConfigByIdShouldReturnEmptyForUnknownConfig() { - // given - given(fileSystem.readFileBlocking(anyString())).willReturn(Buffer.buffer("configs: [ id: '123', id: '456' ]")); - - final FileApplicationSettings applicationSettings = - new FileApplicationSettings(fileSystem, "ignore", "ignore", "ignore", "ignore"); - - // when - final Future config = applicationSettings.getAdUnitConfigById("789", null); - - // then - assertThat(config.failed()).isTrue(); - } - @Test public void getStoredDataShouldReturnResultWithNotFoundErrorForNonExistingRequestId() { // given @@ -224,7 +204,7 @@ public void getStoredDataShouldReturnResultWithNotFoundErrorForNonExistingReques // when final Future storedRequestResult = - applicationSettings.getStoredData(singleton("2"), emptySet(), null); + applicationSettings.getStoredData(null, singleton("2"), emptySet(), null); // then verify(fileSystem).readFileBlocking(eq("/home/user/requests/1.json")); @@ -252,7 +232,7 @@ public void getStoredDataShouldReturnResultWithNotFoundErrorForNonExistingImpId( // when final Future storedRequestResult = - applicationSettings.getStoredData(emptySet(), singleton("2"), null); + applicationSettings.getStoredData(null, emptySet(), singleton("2"), null); // then verify(fileSystem).readFileBlocking(eq("/home/user/imps/1.json")); @@ -279,7 +259,7 @@ public void getStoredDataShouldReturnResultWithNoErrorsIfAllIdsArePresent() { // when final Future storedRequestResult = - applicationSettings.getStoredData(singleton("1"), singleton("2"), null); + applicationSettings.getStoredData(null, singleton("1"), singleton("2"), null); // then verify(fileSystem).readFileBlocking(eq("/home/user/requests/1.json")); @@ -306,7 +286,7 @@ public void getAmpStoredDataShouldIgnoreImpIdsArgument() { // when final Future storedRequestResult = - applicationSettings.getAmpStoredData(emptySet(), singleton("2"), null); + applicationSettings.getAmpStoredData(null, emptySet(), singleton("2"), null); // then assertThat(storedRequestResult.result().getErrors()).isNotNull().isEmpty(); @@ -338,7 +318,7 @@ public void getStoredResponsesShouldReturnEmptyResultAndErrorsWhenResponseIdsAre verify(fileSystem).readFileBlocking(eq("/home/user/responses/1.json")); assertThat(storedResponsesResult.succeeded()).isTrue(); assertThat(storedResponsesResult.result().getErrors()).isNotNull().isEmpty(); - assertThat(storedResponsesResult.result().getStoredSeatBid()).isNotNull().isEmpty(); + assertThat(storedResponsesResult.result().getIdToStoredResponses()).isNotNull().isEmpty(); } @Test @@ -367,7 +347,7 @@ public void getStoredResponsesShouldReturnResultWithMissingIdsIfNotAllIdsArePres assertThat(storedResponsesResult.succeeded()).isTrue(); assertThat(storedResponsesResult.result().getErrors()).isNotNull().hasSize(1) .isEqualTo(singletonList("No stored seatbid found for id: 2")); - assertThat(storedResponsesResult.result().getStoredSeatBid()).isNotNull().hasSize(1) + assertThat(storedResponsesResult.result().getIdToStoredResponses()).isNotNull().hasSize(1) .isEqualTo(singletonMap("1", "value1")); } @@ -396,7 +376,7 @@ public void getStoredResponsesShouldReturnResultWithoutErrorsIfAllIdsArePresent( verify(fileSystem).readFileBlocking(eq("/home/user/responses/1.json")); assertThat(storedResponsesResult.succeeded()).isTrue(); assertThat(storedResponsesResult.result().getErrors()).isNotNull().isEmpty(); - assertThat(storedResponsesResult.result().getStoredSeatBid()).isNotNull().hasSize(1) + assertThat(storedResponsesResult.result().getIdToStoredResponses()).isNotNull().hasSize(1) .isEqualTo(singletonMap("1", "value1")); } diff --git a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java index 4a659447762..f77f302f781 100644 --- a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java @@ -14,8 +14,11 @@ import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; +import org.prebid.server.settings.proto.response.HttpAccountsResponse; import org.prebid.server.settings.proto.response.HttpFetcherResponse; import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; @@ -23,6 +26,7 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.util.Collections; import java.util.HashSet; import java.util.Map; @@ -94,23 +98,92 @@ public void creationShouldFailsOnInvalidVideoEndpoint() { } @Test - public void getAccountByIdShouldReturnEmptyResult() { + public void getAccountByIdShouldReturnFetchedAccount() throws JsonProcessingException { + // given + final Account account = Account.builder() + .id("someId") + .auction(AccountAuctionConfig.builder() + .priceGranularity("testPriceGranularity") + .build()) + .privacy(AccountPrivacyConfig.of(true, null, null)) + .build(); + HttpAccountsResponse response = HttpAccountsResponse.of(Collections.singletonMap("someId", account)); + givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response)); + // when - final Future future = httpApplicationSettings.getAccountById(null, null); + final Future future = httpApplicationSettings.getAccountById("someId", timeout); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result().getId()).isEqualTo("someId"); + assertThat(future.result().getPrivacy().getEnforceCcpa()).isEqualTo(true); + assertThat(future.result().getAuction().getPriceGranularity()).isEqualTo("testPriceGranularity"); + + verify(httpClient).get(eq("http://stored-requests?account-ids=[\"someId\"]"), any(), + anyLong()); + } + + @Test + public void getAccountByIdShouldReturnFaildedFutureIfResponseIsNotPresent() throws JsonProcessingException { + // given + HttpAccountsResponse response = HttpAccountsResponse.of(null); + givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response)); + + // when + final Future future = httpApplicationSettings.getAccountById("notFoundId", timeout); // then assertThat(future.failed()).isTrue(); - assertThat(future.cause()).isInstanceOf(PreBidException.class).hasMessage("Not supported"); + assertThat(future.cause()) + .isInstanceOf(PreBidException.class) + .hasMessage("Account with id : notFoundId not found"); + } + + @Test + public void getAccountByIdShouldReturnErrorIdAccountNotFound() throws JsonProcessingException { + // given + HttpAccountsResponse response = HttpAccountsResponse.of(Collections.emptyMap()); + givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response)); + + // when + final Future future = httpApplicationSettings.getAccountById("notExistingId", timeout); + + // then + assertThat(future.failed()).isTrue(); + assertThat(future.cause()) + .isInstanceOf(PreBidException.class) + .hasMessage("Account with id : notExistingId not found"); } @Test - public void getAdUnitConfigByIdShouldReturnEmptyResult() { + public void getAccountByIdShouldReturnErrorIfResponseStatusIsDifferentFromOk() { + // given + givenHttpClientReturnsResponse(400, null); + // when - final Future future = httpApplicationSettings.getAdUnitConfigById(null, null); + final Future future = httpApplicationSettings.getAccountById("accountId", timeout); // then assertThat(future.failed()).isTrue(); - assertThat(future.cause()).isInstanceOf(PreBidException.class).hasMessage("Not supported"); + assertThat(future.cause()) + .isInstanceOf(PreBidException.class) + .hasMessage("Error fetching accounts [accountId] via http: unexpected response status 400"); + } + + @Test + public void getAccountByIdShouldReturnErrorIfResponseHasInvalidStructure() { + // given + givenHttpClientReturnsResponse(200, "not valid response"); + + // when + final Future future = httpApplicationSettings.getAccountById("accountId", timeout); + + // then + assertThat(future.failed()).isTrue(); + assertThat(future.cause()) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Error fetching accounts [accountId] via http: " + + "failed to parse response: Failed to decode:"); } @Test @@ -126,7 +199,8 @@ public void getStoredResponsesShouldReturnFailedFutureWithNotSupportedReason() { @Test public void getStoredDataShouldReturnEmptyResultIfEmptyRequestsIdsGiven() { // when - final Future future = httpApplicationSettings.getStoredData(emptySet(), emptySet(), null); + final Future future = httpApplicationSettings.getStoredData(null, emptySet(), + emptySet(), null); // then assertThat(future.succeeded()).isTrue(); @@ -140,7 +214,7 @@ public void getStoredDataShouldReturnEmptyResultIfEmptyRequestsIdsGiven() { public void getStoredDataShouldReturnResultWithErrorIfTimeoutAlreadyExpired() { // when final Future future = - httpApplicationSettings.getStoredData(singleton("id1"), emptySet(), expiredTimeout); + httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), expiredTimeout); // then assertThat(future.succeeded()).isTrue(); @@ -157,8 +231,8 @@ public void getStoredDataShouldSendHttpRequestWithExpectedNewParams() { givenHttpClientReturnsResponse(200, null); // when - httpApplicationSettings.getStoredData(new HashSet<>(asList("id1", "id2")), new HashSet<>(asList("id3", "id4")), - timeout); + httpApplicationSettings.getStoredData(null, new HashSet<>(asList("id1", "id2")), + new HashSet<>(asList("id3", "id4")), timeout); // then verify(httpClient).get(eq("http://stored-requests?request-ids=[\"id2\",\"id1\"]&imp-ids=[\"id4\",\"id3\"]"), @@ -173,7 +247,7 @@ public void getStoredDataShouldSendHttpRequestWithExpectedAppendedParams() { "http://some-domain?param1=value1", AMP_ENDPOINT, VIDEO_ENDPOINT); // when - httpApplicationSettings.getStoredData(singleton("id1"), singleton("id2"), timeout); + httpApplicationSettings.getStoredData(null, singleton("id1"), singleton("id2"), timeout); // then verify(httpClient).get(eq("http://some-domain?param1=value1&request-ids=[\"id1\"]&imp-ids=[\"id2\"]"), any(), @@ -187,7 +261,7 @@ public void getStoredDataShouldReturnResultWithErrorIfHttpClientFails() { // when final Future future = - httpApplicationSettings.getStoredData(singleton("id1"), emptySet(), timeout); + httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), timeout); // then assertThat(future.succeeded()).isTrue(); @@ -204,7 +278,7 @@ public void getStoredDataShouldReturnResultWithErrorIfHttpClientRespondsNot200St // when final Future future = - httpApplicationSettings.getStoredData(singleton("id1"), emptySet(), timeout); + httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), timeout); // then assertThat(future.succeeded()).isTrue(); @@ -221,7 +295,7 @@ public void getStoredDataShouldReturnResultWithErrorIfHttpResponseIsMalformed() // when final Future future = - httpApplicationSettings.getStoredData(singleton("id1"), emptySet(), timeout); + httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), timeout); // then assertThat(future.succeeded()).isTrue(); @@ -240,7 +314,7 @@ public void getStoredDataShouldReturnResultWithErrorIfStoredRequestObjectIsMalfo // when final Future future = - httpApplicationSettings.getStoredData(singleton("id1"), emptySet(), timeout); + httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), timeout); // then assertThat(future.succeeded()).isTrue(); @@ -260,7 +334,7 @@ public void getStoredDataShouldReturnResultWithErrorIfStoredImpObjectIsMalformed // when final Future future = - httpApplicationSettings.getStoredData(singleton("id1"), emptySet(), timeout); + httpApplicationSettings.getStoredData(null, singleton("id1"), emptySet(), timeout); // then assertThat(future.succeeded()).isTrue(); @@ -280,7 +354,7 @@ public void getStoredDataShouldTolerateMissedId() throws JsonProcessingException // when final Future future = httpApplicationSettings.getStoredData( - new HashSet<>(asList("id1", "id2")), new HashSet<>(asList("id3", "id4")), timeout); + null, new HashSet<>(asList("id1", "id2")), new HashSet<>(asList("id3", "id4")), timeout); // then assertThat(future.succeeded()).isTrue(); @@ -304,7 +378,7 @@ public void getStoredDataShouldReturnExpectedResult() throws JsonProcessingExcep // when final Future future = - httpApplicationSettings.getStoredData(singleton("id1"), singleton("id2"), timeout); + httpApplicationSettings.getStoredData(null, singleton("id1"), singleton("id2"), timeout); // then assertThat(future.succeeded()).isTrue(); @@ -323,7 +397,7 @@ public void getAmpStoredDataShouldIgnoreImpIdsArgument() { givenHttpClientReturnsResponse(200, null); // when - httpApplicationSettings.getAmpStoredData(singleton("id1"), singleton("id2"), timeout); + httpApplicationSettings.getAmpStoredData(null, singleton("id1"), singleton("id2"), timeout); // then final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); diff --git a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java index 301efbb5acf..9e955bdc811 100644 --- a/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/JdbcApplicationSettingsTest.java @@ -24,7 +24,14 @@ import org.prebid.server.metric.Metrics; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountBidValidationConfig; +import org.prebid.server.settings.model.AccountCookieSyncConfig; +import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; +import org.prebid.server.settings.model.AccountStatus; +import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.settings.model.EnabledForRequestType; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; @@ -58,30 +65,37 @@ public class JdbcApplicationSettingsTest extends VertxTest { private static final String JDBC_URL = "jdbc:h2:mem:test"; + private static final String SELECT_ACCOUNT_QUERY = + "SELECT config FROM accounts_account where uuid = %ACCOUNT_ID% LIMIT 1"; + private static final String SELECT_QUERY = - "SELECT reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) " + "SELECT accountId, reqid, requestData, 'request' as dataType FROM stored_requests " + + "WHERE reqid IN (%REQUEST_ID_LIST%) " + "UNION ALL " - + "SELECT impid, impData, 'imp' as dataType FROM stored_imps WHERE impid IN (%IMP_ID_LIST%)"; + + "SELECT accountId, impid, impData, 'imp' as dataType FROM stored_imps " + + "WHERE impid IN (%IMP_ID_LIST%)"; private static final String SELECT_UNION_QUERY = - "SELECT reqid, requestData, 'request' as dataType FROM stored_requests WHERE reqid IN (%REQUEST_ID_LIST%) " + "SELECT accountId, reqid, requestData, 'request' as dataType FROM stored_requests " + + "WHERE reqid IN (%REQUEST_ID_LIST%) " + "UNION ALL " - + "SELECT reqid, requestData, 'request' as dataType FROM stored_requests2 " + + "SELECT accountId, reqid, requestData, 'request' as dataType FROM stored_requests2 " + "WHERE reqid IN (%REQUEST_ID_LIST%) " + "UNION ALL " - + "SELECT impid, impData, 'imp' as dataType FROM stored_imps WHERE impid IN (%IMP_ID_LIST%) " + + "SELECT accountId, impid, impData, 'imp' as dataType FROM stored_imps " + + "WHERE impid IN (%IMP_ID_LIST%) " + "UNION ALL " - + "SELECT impid, impData, 'imp' as dataType FROM stored_imps2 WHERE impid IN (%IMP_ID_LIST%)"; + + "SELECT accountId, impid, impData, 'imp' as dataType FROM stored_imps2 " + + "WHERE impid IN (%IMP_ID_LIST%)"; - private static final String SELECT_FROM_ONE_COLUMN_TABLE_QUERY = - "SELECT reqid FROM one_column_table WHERE reqid IN " - + "(%REQUEST_ID_LIST%)"; + private static final String SELECT_FROM_ONE_COLUMN_TABLE_QUERY = "SELECT reqid FROM one_column_table " + + "WHERE reqid IN (%REQUEST_ID_LIST%)"; - private static final String SELECT_RESPONSE_QUERY = "SELECT responseId, responseData FROM stored_responses" - + " WHERE responseId IN (%RESPONSE_ID_LIST%)"; + private static final String SELECT_RESPONSE_QUERY = "SELECT responseId, responseData FROM stored_responses " + + "WHERE responseId IN (%RESPONSE_ID_LIST%)"; - private static final String SELECT_ONE_COLUMN_RESPONSE_QUERY = "SELECT responseId FROM stored_responses" - + " WHERE responseId IN (%RESPONSE_ID_LIST%)"; + private static final String SELECT_ONE_COLUMN_RESPONSE_QUERY = "SELECT responseId FROM stored_responses " + + "WHERE responseId IN (%RESPONSE_ID_LIST%)"; private static Connection connection; @@ -98,46 +112,81 @@ public class JdbcApplicationSettingsTest extends VertxTest { @BeforeClass public static void beforeClass() throws SQLException { connection = DriverManager.getConnection(JDBC_URL); - connection.createStatement().execute("CREATE TABLE accounts_account (id SERIAL PRIMARY KEY, " - + "uuid varchar(40) NOT NULL, price_granularity varchar(6), granularityMultiplier numeric(9,3), " - + "banner_cache_ttl INT, video_cache_ttl INT, events_enabled BIT, enforce_ccpa BIT, " - + "tcf_config varchar(512), analytics_sampling_factor INT, truncate_target_attr INT, " - + "default_integration varchar(64), analytics_config varchar(512));"); - connection.createStatement().execute("CREATE TABLE s2sconfig_config (id SERIAL PRIMARY KEY, uuid varchar(40) " - + "NOT NULL, config varchar(512));"); - connection.createStatement().execute("CREATE TABLE stored_requests (id SERIAL PRIMARY KEY, reqid varchar(40) " - + "NOT NULL, requestData varchar(512));"); - connection.createStatement().execute("CREATE TABLE stored_requests2 (id SERIAL PRIMARY KEY, reqid varchar(40) " - + "NOT NULL, requestData varchar(512));"); - connection.createStatement().execute("CREATE TABLE stored_imps (id SERIAL PRIMARY KEY, impid varchar(40) " - + "NOT NULL, impData varchar(512));"); - connection.createStatement().execute("CREATE TABLE stored_imps2 (id SERIAL PRIMARY KEY, impid varchar(40) " - + "NOT NULL, impData varchar(512));"); + connection.createStatement().execute( + "CREATE TABLE accounts_account (" + + "id SERIAL PRIMARY KEY, " + + "uuid varchar(40) NOT NULL, " + + "config varchar(4096)" + + ");"); + connection.createStatement().execute("CREATE TABLE stored_requests (id SERIAL PRIMARY KEY, " + + "accountId varchar(40) NOT NULL, reqid varchar(40) NOT NULL, requestData varchar(512));"); + connection.createStatement().execute("CREATE TABLE stored_requests2 (id SERIAL PRIMARY KEY, " + + "accountId varchar(40) NOT NULL, reqid varchar(40) NOT NULL, requestData varchar(512));"); + connection.createStatement().execute("CREATE TABLE stored_imps (id SERIAL PRIMARY KEY, " + + "accountId varchar(40) NOT NULL, impid varchar(40) NOT NULL, impData varchar(512));"); + connection.createStatement().execute("CREATE TABLE stored_imps2 (id SERIAL PRIMARY KEY, " + + "accountId varchar(40) NOT NULL, impid varchar(40) NOT NULL, impData varchar(512));"); connection.createStatement().execute( "CREATE TABLE stored_responses (id SERIAL PRIMARY KEY, responseId varchar(40) NOT NULL," + " responseData varchar(512));"); - connection.createStatement().execute("CREATE TABLE one_column_table (id SERIAL PRIMARY KEY, reqid varchar(40)" - + " NOT NULL);"); - connection.createStatement().execute("insert into accounts_account " - + "(uuid, price_granularity, banner_cache_ttl, video_cache_ttl, events_enabled, enforce_ccpa, " - + "tcf_config, analytics_sampling_factor, truncate_target_attr, default_integration, analytics_config) " - + "values ('accountId','med', 100, 100, TRUE, TRUE, '{\"enabled\": true, " - + "\"integration-enabled\": {\"amp\": true, \"app\": true, \"video\": true, \"web\": true}}', 1, 0, " - + "'web', '{\"auction-events\": {\"amp\": true}}');"); - connection.createStatement().execute("insert into s2sconfig_config (uuid, config)" - + " values ('adUnitConfigId', 'config');"); - connection.createStatement().execute("insert into stored_requests (reqid, requestData) values ('1','value1');"); - connection.createStatement().execute("insert into stored_requests (reqid, requestData) values ('2','value2');"); + connection.createStatement().execute("CREATE TABLE one_column_table (id SERIAL PRIMARY KEY, reqid " + + "varchar(40) NOT NULL);"); + connection.createStatement().execute("insert into accounts_account (uuid, config) values (" + + "'1001'," + + "'{" + + "\"id\": \"1001\"," + + "\"status\": \"active\"," + + "\"auction\": {" + + "\"price-granularity\": \"med\"," + + "\"banner-cache-ttl\": 100," + + "\"video-cache-ttl\": 100," + + "\"truncate-target-attr\": 0," + + "\"default-integration\": \"web\"," + + "\"bid-validations\": {" + + "\"banner-creative-max-size\": \"enforce\"" + + "}," + + "\"events\": {" + + "\"enabled\": true" + + "}" + + "}," + + "\"privacy\": {" + + "\"enforce-ccpa\": true," + + "\"gdpr\": {" + + "\"enabled\": true," + + "\"integration-enabled\": {\"amp\": true, \"app\": true, \"video\": true, \"web\": true}" + + "}" + + "}," + + "\"analytics\": {" + + "\"auction-events\": {\"amp\": true}," + + "\"modules\": {\"some-analytics\": {\"supported-endpoints\": [\"auction\"]}}" + + "}," + + "\"cookie-sync\": {" + + "\"default-limit\": 5," + + "\"max-limit\": 8," + + "\"default-coop-sync\": true" + + "}" + + "}'" + + ");"); + connection.createStatement().execute( + "insert into stored_requests (accountId, reqid, requestData) values ('1001', '1','value1');"); + connection.createStatement().execute( + "insert into stored_requests (accountId, reqid, requestData) values ('1001', '2','value2');"); + connection.createStatement().execute( + "insert into stored_requests2 (accountId, reqid, requestData) values ('1001', '3','value3');"); + connection.createStatement().execute( + "insert into stored_imps (accountId, impid, impData) values ('1001', '4','value4');"); + connection.createStatement().execute( + "insert into stored_imps (accountId, impid, impData) values ('1001', '5','value5');"); + connection.createStatement().execute( + "insert into stored_imps2 (accountId, impid, impData) values ('1001', '6','value6');"); + connection.createStatement().execute( + "insert into stored_responses (responseId, responseData) " + + "values ('1', 'response1');"); connection.createStatement().execute( - "insert into stored_requests2 (reqid, requestData) values ('3','value3');"); - connection.createStatement().execute("insert into stored_imps (impid, impData) values ('4','value4');"); - connection.createStatement().execute("insert into stored_imps (impid, impData) values ('5','value5');"); - connection.createStatement().execute("insert into stored_imps2 (impid, impData) values ('6','value6');"); - connection.createStatement().execute("insert into stored_responses (responseId, responseData) " - + "values ('1','response1');"); - connection.createStatement().execute("insert into stored_responses (responseId, responseData) " - + "values ('2','response2');"); - connection.createStatement().execute("insert into one_column_table (reqid) values ('3');"); + "insert into stored_responses (responseId, responseData) " + + "values ('2', 'response2');"); + connection.createStatement().execute( + "insert into one_column_table (reqid) values ('3');"); } @AfterClass @@ -150,7 +199,12 @@ public void setUp() { vertx = Vertx.vertx(); clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); timeout = new TimeoutFactory(clock).create(5000L); - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, SELECT_QUERY, SELECT_QUERY, + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_QUERY, + SELECT_QUERY, SELECT_RESPONSE_QUERY); } @@ -162,26 +216,37 @@ public void tearDown(TestContext context) { @Test public void getAccountByIdShouldReturnAccountWithAllFieldsPopulated(TestContext context) { // when - final Future future = jdbcApplicationSettings.getAccountById("accountId", timeout); + final Future future = jdbcApplicationSettings.getAccountById("1001", timeout); // then final Async async = context.async(); future.setHandler(context.asyncAssertSuccess(account -> { assertThat(account).isEqualTo(Account.builder() - .id("accountId") - .priceGranularity("med") - .bannerCacheTtl(100) - .videoCacheTtl(100) - .analyticsSamplingFactor(1) - .eventsEnabled(true) - .enforceCcpa(true) - .gdpr(AccountGdprConfig.builder() - .enabled(true) - .enabledForRequestType(EnabledForRequestType.of(true, true, true, true)) + .id("1001") + .status(AccountStatus.active) + .auction(AccountAuctionConfig.builder() + .priceGranularity("med") + .bannerCacheTtl(100) + .videoCacheTtl(100) + .truncateTargetAttr(0) + .defaultIntegration("web") + .bidValidations(AccountBidValidationConfig.of(BidValidationEnforcement.enforce)) + .events(AccountEventsConfig.of(true)) .build()) - .truncateTargetAttr(0) - .defaultIntegration("web") - .analyticsConfig(AccountAnalyticsConfig.of(singletonMap("amp", true))) + .privacy(AccountPrivacyConfig.of( + true, + AccountGdprConfig.builder() + .enabled(true) + .enabledForRequestType(EnabledForRequestType.of(true, true, true, true)) + .build(), + null)) + .analytics(AccountAnalyticsConfig.of( + singletonMap("amp", true), + singletonMap( + "some-analytics", + mapper.createObjectNode() + .set("supported-endpoints", mapper.createArrayNode().add("auction"))))) + .cookieSync(AccountCookieSyncConfig.of(5, 8, true)) .build()); async.complete(); })); @@ -201,38 +266,11 @@ public void getAccountByIdShouldFailIfAccountNotFound(TestContext context) { })); } - @Test - public void getAdUnitConfigByIdShouldReturnConfig(TestContext context) { - // when - final Future future = jdbcApplicationSettings.getAdUnitConfigById("adUnitConfigId", timeout); - - // then - final Async async = context.async(); - future.setHandler(context.asyncAssertSuccess(config -> { - assertThat(config).isEqualTo("config"); - async.complete(); - })); - } - - @Test - public void getAdUnitConfigByIdShouldFailIfConfigNotFound(TestContext context) { - // when - final Future future = jdbcApplicationSettings.getAdUnitConfigById("non-existing", timeout); - - // then - final Async async = context.async(); - future.setHandler(context.asyncAssertFailure(exception -> { - assertThat(exception).isInstanceOf(PreBidException.class) - .hasMessage("AdUnitConfig not found: non-existing"); - async.complete(); - })); - } - @Test public void getStoredDataShouldReturnExpectedResult(TestContext context) { // when final Future future = jdbcApplicationSettings.getStoredData( - new HashSet<>(asList("1", "2")), new HashSet<>(asList("4", "5")), timeout); + "1001", new HashSet<>(asList("1", "2")), new HashSet<>(asList("4", "5")), timeout); // then final Async async = context.async(); @@ -253,7 +291,7 @@ public void getStoredDataShouldReturnExpectedResult(TestContext context) { public void getAmpStoredDataShouldReturnExpectedResult(TestContext context) { // when final Future future = jdbcApplicationSettings.getAmpStoredData( - new HashSet<>(asList("1", "2")), new HashSet<>(asList("3", "4")), timeout); + "1001", new HashSet<>(asList("1", "2")), new HashSet<>(asList("3", "4")), timeout); // then final Async async = context.async(); @@ -270,7 +308,7 @@ public void getAmpStoredDataShouldReturnExpectedResult(TestContext context) { @Test public void getVideoStoredDataShouldReturnExpectedResult(TestContext context) { // when - final Future future = jdbcApplicationSettings.getVideoStoredData( + final Future future = jdbcApplicationSettings.getVideoStoredData("1001", new HashSet<>(asList("1", "2")), new HashSet<>(asList("4", "5")), timeout); // then @@ -291,12 +329,17 @@ public void getVideoStoredDataShouldReturnExpectedResult(TestContext context) { @Test public void getVideoStoredDataShouldReturnStoredRequests(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, SELECT_UNION_QUERY, - SELECT_UNION_QUERY, SELECT_RESPONSE_QUERY); + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_UNION_QUERY, + SELECT_UNION_QUERY, + SELECT_RESPONSE_QUERY); // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getVideoStoredData(new HashSet<>(asList("1", "2", "3")), + jdbcApplicationSettings.getVideoStoredData("1001", new HashSet<>(asList("1", "2", "3")), new HashSet<>(asList("4", "5", "6")), timeout); // then @@ -319,12 +362,17 @@ public void getVideoStoredDataShouldReturnStoredRequests(TestContext context) { @Test public void getStoredDataUnionSelectByIdShouldReturnStoredRequests(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, SELECT_UNION_QUERY, - SELECT_UNION_QUERY, SELECT_RESPONSE_QUERY); + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_UNION_QUERY, + SELECT_UNION_QUERY, + SELECT_RESPONSE_QUERY); // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getStoredData(new HashSet<>(asList("1", "2", "3")), + jdbcApplicationSettings.getStoredData("1001", new HashSet<>(asList("1", "2", "3")), new HashSet<>(asList("4", "5", "6")), timeout); // then @@ -347,12 +395,17 @@ public void getStoredDataUnionSelectByIdShouldReturnStoredRequests(TestContext c @Test public void getAmpStoredDataUnionSelectByIdShouldReturnStoredRequests(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, SELECT_UNION_QUERY, - SELECT_UNION_QUERY, SELECT_RESPONSE_QUERY); + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_UNION_QUERY, + SELECT_UNION_QUERY, + SELECT_RESPONSE_QUERY); // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getAmpStoredData(new HashSet<>(asList("1", "2", "3")), + jdbcApplicationSettings.getAmpStoredData("1001", new HashSet<>(asList("1", "2", "3")), new HashSet<>(asList("4", "5", "6")), timeout); // then @@ -372,7 +425,7 @@ public void getAmpStoredDataUnionSelectByIdShouldReturnStoredRequests(TestContex public void getStoredDataShouldReturnResultWithErrorIfNoStoredRequestFound(TestContext context) { // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getStoredData(new HashSet<>(asList("1", "3")), emptySet(), timeout); + jdbcApplicationSettings.getStoredData("1001", new HashSet<>(asList("1", "3")), emptySet(), timeout); // then final Async async = context.async(); @@ -387,7 +440,7 @@ public void getStoredDataShouldReturnResultWithErrorIfNoStoredRequestFound(TestC public void getStoredDataShouldReturnResultWithErrorIfNoStoredImpFound(TestContext context) { // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getStoredData(emptySet(), new HashSet<>(asList("4", "6")), timeout); + jdbcApplicationSettings.getStoredData("1001", emptySet(), new HashSet<>(asList("4", "6")), timeout); // then final Async async = context.async(); @@ -402,7 +455,8 @@ public void getStoredDataShouldReturnResultWithErrorIfNoStoredImpFound(TestConte public void getAmpStoredDataShouldReturnResultWithErrorIfNoStoredRequestFound(TestContext context) { // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getAmpStoredData(new HashSet<>(asList("1", "3")), emptySet(), timeout); + jdbcApplicationSettings.getAmpStoredData("1001", new HashSet<>(asList("1", "3")), emptySet(), + timeout); // then final Async async = context.async(); @@ -416,18 +470,24 @@ public void getAmpStoredDataShouldReturnResultWithErrorIfNoStoredRequestFound(Te @Test public void getStoredDataShouldReturnErrorIfResultContainsLessColumnsThanExpected(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, - SELECT_FROM_ONE_COLUMN_TABLE_QUERY, SELECT_FROM_ONE_COLUMN_TABLE_QUERY, SELECT_RESPONSE_QUERY); + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_FROM_ONE_COLUMN_TABLE_QUERY, + SELECT_FROM_ONE_COLUMN_TABLE_QUERY, + SELECT_RESPONSE_QUERY); // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getStoredData(new HashSet<>(asList("1", "2", "3")), emptySet(), timeout); + jdbcApplicationSettings.getStoredData("1001", new HashSet<>(asList("1", "2", "3")), emptySet(), + timeout); // then final Async async = context.async(); storedRequestResultFuture.setHandler(context.asyncAssertSuccess(storedRequestResult -> { assertThat(storedRequestResult).isEqualTo(StoredDataResult.of(emptyMap(), emptyMap(), - singletonList("Result set column number is less than expected"))); + singletonList("Error occurred while mapping stored request data"))); async.complete(); })); } @@ -435,19 +495,24 @@ public void getStoredDataShouldReturnErrorIfResultContainsLessColumnsThanExpecte @Test public void getAmpStoredDataShouldReturnErrorIfResultContainsLessColumnsThanExpected(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_FROM_ONE_COLUMN_TABLE_QUERY, SELECT_FROM_ONE_COLUMN_TABLE_QUERY, - SELECT_FROM_ONE_COLUMN_TABLE_QUERY, SELECT_RESPONSE_QUERY); + SELECT_RESPONSE_QUERY); // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getAmpStoredData(new HashSet<>(asList("1", "2", "3")), emptySet(), timeout); + jdbcApplicationSettings.getAmpStoredData("1001", new HashSet<>(asList("1", "2", "3")), emptySet(), + timeout); // then final Async async = context.async(); storedRequestResultFuture.setHandler(context.asyncAssertSuccess(storedRequestResult -> { assertThat(storedRequestResult).isEqualTo(StoredDataResult.of(emptyMap(), emptyMap(), - singletonList("Result set column number is less than expected"))); + singletonList("Error occurred while mapping stored request data"))); async.complete(); })); } @@ -456,7 +521,7 @@ public void getAmpStoredDataShouldReturnErrorIfResultContainsLessColumnsThanExpe public void getStoredDataShouldReturnErrorAndEmptyResult(TestContext context) { // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getStoredData(new HashSet<>(asList("3", "4")), + jdbcApplicationSettings.getStoredData("1001", new HashSet<>(asList("3", "4")), new HashSet<>(asList("6", "7")), timeout); // then @@ -472,7 +537,8 @@ public void getStoredDataShouldReturnErrorAndEmptyResult(TestContext context) { public void getAmpStoredDataShouldReturnErrorAndEmptyResult(TestContext context) { // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getAmpStoredData(new HashSet<>(asList("3", "4")), emptySet(), timeout); + jdbcApplicationSettings.getAmpStoredData("1001", new HashSet<>(asList("3", "4")), emptySet(), + timeout); // then final Async async = context.async(); @@ -487,7 +553,7 @@ public void getAmpStoredDataShouldReturnErrorAndEmptyResult(TestContext context) public void getAmpStoredDataShouldIgnoreImpIdsArgument(TestContext context) { // when final Future storedRequestResultFuture = - jdbcApplicationSettings.getAmpStoredData(singleton("1"), singleton("4"), timeout); + jdbcApplicationSettings.getAmpStoredData("1001", singleton("1"), singleton("4"), timeout); // then final Async async = context.async(); @@ -535,7 +601,12 @@ public void getStoredResponseShouldReturnResultWithErrorIfNotAllStoredResponsesW @Test public void getStoredResponseShouldReturnErrorIfResultContainsLessColumnsThanExpected(TestContext context) { // given - jdbcApplicationSettings = new JdbcApplicationSettings(jdbcClient(), jacksonMapper, SELECT_QUERY, SELECT_QUERY, + jdbcApplicationSettings = new JdbcApplicationSettings( + jdbcClient(), + jacksonMapper, + SELECT_ACCOUNT_QUERY, + SELECT_QUERY, + SELECT_QUERY, SELECT_ONE_COLUMN_RESPONSE_QUERY); // when @@ -571,7 +642,6 @@ private JdbcClient jdbcClient() { new JsonObject() .put("url", JDBC_URL) .put("driver_class", "org.h2.Driver") - .put("max_pool_size", 10)), metrics, clock - ); + .put("max_pool_size", 10)), metrics, clock); } } diff --git a/src/test/java/org/prebid/server/settings/SettingsCacheTest.java b/src/test/java/org/prebid/server/settings/SettingsCacheTest.java index 251c99af191..cdf602d21fe 100644 --- a/src/test/java/org/prebid/server/settings/SettingsCacheTest.java +++ b/src/test/java/org/prebid/server/settings/SettingsCacheTest.java @@ -2,7 +2,9 @@ import org.junit.Before; import org.junit.Test; +import org.prebid.server.settings.model.StoredItem; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; @@ -26,6 +28,26 @@ public void getImpCacheShouldReturnEmptyMap() { assertThat(settingsCache.getImpCache()).isEmpty(); } + @Test + public void saveRequestCacheShouldAddNewRequestsToCache() { + // when + settingsCache.saveRequestCache("1001", "reqId1", "reqValue1"); + + // then + assertThat(settingsCache.getRequestCache()).hasSize(1) + .containsEntry("reqId1", singleton(StoredItem.of("1001", "reqValue1"))); + } + + @Test + public void saveImpCacheShouldAddNewImpsToCache() { + // when + settingsCache.saveImpCache("1001", "impId1", "impValue1"); + + // then + assertThat(settingsCache.getImpCache()).hasSize(1) + .containsEntry("impId1", singleton(StoredItem.of("1001", "impValue1"))); + } + @Test public void saveShouldAddNewItemsToCache() { // when @@ -33,9 +55,9 @@ public void saveShouldAddNewItemsToCache() { // then assertThat(settingsCache.getRequestCache()).hasSize(1) - .containsEntry("reqId1", "reqValue1"); + .containsEntry("reqId1", singleton(StoredItem.of(null, "reqValue1"))); assertThat(settingsCache.getImpCache()).hasSize(1) - .containsEntry("impId1", "impValue1"); + .containsEntry("impId1", singleton(StoredItem.of(null, "impValue1"))); } @Test @@ -49,8 +71,8 @@ public void invalidateShouldRemoveItemsFromCache() { // then assertThat(settingsCache.getRequestCache()).hasSize(1) - .containsEntry("reqId2", "reqValue2"); + .containsEntry("reqId2", singleton(StoredItem.of(null, "reqValue2"))); assertThat(settingsCache.getImpCache()).hasSize(1) - .containsEntry("impId2", "impValue2"); + .containsEntry("impId2", singleton(StoredItem.of(null, "impValue2"))); } } diff --git a/src/test/java/org/prebid/server/settings/helper/JdbcStoredDataResultMapperTest.java b/src/test/java/org/prebid/server/settings/helper/JdbcStoredDataResultMapperTest.java new file mode 100644 index 00000000000..0ed1dd87a52 --- /dev/null +++ b/src/test/java/org/prebid/server/settings/helper/JdbcStoredDataResultMapperTest.java @@ -0,0 +1,292 @@ +package org.prebid.server.settings.helper; + +import io.vertx.core.json.JsonArray; +import io.vertx.ext.sql.ResultSet; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.settings.model.StoredDataResult; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.BDDMockito.given; + +public class JdbcStoredDataResultMapperTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private ResultSet resultSet; + + @Test + public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasEmptyResult() { + // given + given(resultSet.getResults()).willReturn(emptyList()); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, null, emptySet(), emptySet()); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("No stored requests or imps were found"); + } + + @Test + public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasEmptyResultForGivenIds() { + // given + given(resultSet.getResults()).willReturn(emptyList()); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, null, + singleton("reqId"), singleton("impId")); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("No stored requests for ids [reqId] and stored imps for ids [impId] were found"); + } + + @Test + public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasLessColumns() { + // given + given(resultSet.getResults()).willReturn(singletonList( + new JsonArray(asList("accountId", "id1", "data")))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, "accountId", + singleton("reqId"), emptySet()); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Error occurred while mapping stored request data"); + } + + @Test + public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasUnexpectedColumnType() { + // given + given(resultSet.getResults()).willReturn(singletonList( + new JsonArray(asList("accountId", "id1", "data", 123)))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, "accountId", + singleton("reqId"), emptySet()); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Error occurred while mapping stored request data"); + } + + @Test + public void mapShouldSkipStoredResultWithInvalidType() { + // given + given(resultSet.getResults()).willReturn(asList( + new JsonArray(asList("accountId", "id1", "data1", "request")), + new JsonArray(asList("accountId", "id1", "data2", "invalid")))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, "accountId", + singleton("id1"), emptySet()); + + // then + assertThat(result.getStoredIdToRequest()).hasSize(1) + .containsOnly(entry("id1", "data1")); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void mapShouldReturnStoredResultWithErrorForMissingId() { + // given + given(resultSet.getResults()).willReturn(singletonList( + new JsonArray(asList("accountId", "id1", "data1", "request")))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, "accountId", singleton("id1"), + singleton("id2")); + + // then + assertThat(result.getStoredIdToRequest()).hasSize(1) + .containsOnly(entry("id1", "data1")); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("No stored imp found for id: id2"); + } + + @Test + public void mapShouldReturnEmptyStoredResultWithErrorsForMissingIdsIfAccountDiffers() { + // given + given(resultSet.getResults()).willReturn(asList( + new JsonArray(asList("accountId", "id1", "data1", "request")), + new JsonArray(asList("accountId", "id2", "data2", "imp")))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, "otherAccountId", + singleton("id1"), singleton("id2")); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(2) + .containsOnly( + "No stored request found for id: id1 for account: otherAccountId", + "No stored imp found for id: id2 for account: otherAccountId"); + } + + @Test + public void mapShouldReturnEmptyStoredResultWithErrorIfMultipleStoredItemsFoundButNoAccountIdIsDefined() { + // given + given(resultSet.getResults()).willReturn(asList( + new JsonArray(asList("accountId1", "id1", "data1", "request")), + new JsonArray(asList("accountId2", "id1", "data2", "request")))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, null, + singleton("id1"), emptySet()); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Multiple stored requests found for id: id1 but no account was specified"); + } + + @Test + public void mapShouldReturnEmptyStoredResultWithErrorIfMultipleStoredItemsFoundButNoAccountIdIsDiffers() { + // given + given(resultSet.getResults()).willReturn(asList( + new JsonArray(asList("accountId1", "id1", "data-accountId", "request")), + new JsonArray(asList("accountId2", "id1", "data-otherAccountId", "request")), + new JsonArray(asList("accountId1", "id2", "data-accountId", "imp")), + new JsonArray(asList("accountId2", "id2", "data-otherAccountId", "imp")))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, "otherAccountId", + singleton("id1"), singleton("id2")); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(2) + .containsOnly( + "No stored request found among multiple id: id1 for account: otherAccountId", + "No stored imp found among multiple id: id2 for account: otherAccountId"); + } + + @Test + public void mapShouldReturnExpectedStoredResultForGivenAccount() { + // given + given(resultSet.getResults()).willReturn(asList( + new JsonArray(asList("accountId", "id1", "data-accountId", "request")), + new JsonArray(asList("otherAccountId", "id1", "data-otherAccountId", "request")), + new JsonArray(asList("accountId", "id2", "data-accountId", "imp")), + new JsonArray(asList("otherAccountId", "id2", "data-otherAccountId", "imp")))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, "accountId", + singleton("id1"), singleton("id2")); + + // then + assertThat(result.getStoredIdToRequest()).hasSize(1) + .containsOnly(entry("id1", "data-accountId")); + assertThat(result.getStoredIdToImp()).hasSize(1) + .containsOnly(entry("id2", "data-accountId")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void mapWithoutParamsShouldReturnEmptyStoredResultWithErrorWhenResultSetHasEmptyResult() { + // given + given(resultSet.getResults()).willReturn(emptyList()); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("No stored requests or imps were found"); + } + + @Test + public void mapWithoutParamsShouldSkipStoredResultWithInvalidType() { + // given + given(resultSet.getResults()).willReturn(asList( + new JsonArray(asList("accountId", "id1", "data1", "request")), + new JsonArray(asList("accountId", "id2", "data2", "invalid")))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet); + + // then + assertThat(result.getStoredIdToRequest()).hasSize(1) + .containsOnly(entry("id1", "data1")); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void mapWithoutParamsShouldReturnEmptyStoredResultWithErrorWhenResultSetHasLessColumns() { + // given + given(resultSet.getResults()).willReturn(singletonList( + new JsonArray(asList("accountId", "id1", "data")))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Error occurred while mapping stored request data"); + } + + @Test + public void mapWithoutParamsShouldReturnEmptyStoredResultWithErrorWhenResultSetHasUnexpectedColumnType() { + // given + given(resultSet.getResults()).willReturn(singletonList( + new JsonArray(asList("accountId", "id1", "data", 123)))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet); + + // then + assertThat(result.getStoredIdToRequest()).isEmpty(); + assertThat(result.getStoredIdToImp()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Error occurred while mapping stored request data"); + } + + @Test + public void mapWithoutParamsShouldReturnExpectedStoredResult() { + // given + given(resultSet.getResults()).willReturn(asList( + new JsonArray(asList("accountId", "id1", "data1", "request")), + new JsonArray(asList("accountId", "id2", "data2", "imp")))); + + // when + final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet); + + // then + assertThat(result.getStoredIdToRequest()).hasSize(1) + .containsOnly(entry("id1", "data1")); + assertThat(result.getStoredIdToImp()).hasSize(1) + .containsOnly(entry("id2", "data2")); + assertThat(result.getErrors()).isEmpty(); + } +} diff --git a/src/test/java/org/prebid/server/settings/mapper/JdbcStoredResponseResultMapperTest.java b/src/test/java/org/prebid/server/settings/helper/JdbcStoredResponseResultMapperTest.java similarity index 90% rename from src/test/java/org/prebid/server/settings/mapper/JdbcStoredResponseResultMapperTest.java rename to src/test/java/org/prebid/server/settings/helper/JdbcStoredResponseResultMapperTest.java index b438d3ba8e8..ba9d7e18d1f 100644 --- a/src/test/java/org/prebid/server/settings/mapper/JdbcStoredResponseResultMapperTest.java +++ b/src/test/java/org/prebid/server/settings/helper/JdbcStoredResponseResultMapperTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.settings.mapper; +package org.prebid.server.settings.helper; import io.vertx.core.json.JsonArray; import io.vertx.ext.sql.ResultSet; @@ -37,7 +37,7 @@ public void mapShouldReturnEmptyStoredResponseResultWithErrorWhenResultSetIsEmpt final StoredResponseDataResult result = JdbcStoredResponseResultMapper.map(resultSet, emptySet()); // then - assertThat(result.getStoredSeatBid()).isEmpty(); + assertThat(result.getIdToStoredResponses()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .containsOnly("No stored responses found"); } @@ -51,7 +51,7 @@ public void mapShouldReturnEmptyStoredResponseResultWithErrorWhenResultSetHasLes final StoredResponseDataResult result = JdbcStoredResponseResultMapper.map(resultSet, emptySet()); // then - assertThat(result.getStoredSeatBid()).isEmpty(); + assertThat(result.getIdToStoredResponses()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .containsOnly("Result set column number is less than expected"); } @@ -66,7 +66,7 @@ public void mapShouldReturnStoredResponseResultWithErrorForMissingID() { .map(resultSet, new HashSet<>(asList("id1", "id2"))); // then - assertThat(result.getStoredSeatBid()).hasSize(1) + assertThat(result.getIdToStoredResponses()).hasSize(1) .containsOnly(new AbstractMap.SimpleEntry<>("id1", "data")); assertThat(result.getErrors()).hasSize(1) .containsOnly("No stored response found for id: id2"); @@ -81,7 +81,7 @@ public void mapShouldReturnEmptyStoredResponseResultWithErrorWhenResultSetHasEmp final StoredResponseDataResult result = JdbcStoredResponseResultMapper.map(resultSet, singleton("id")); // then - assertThat(result.getStoredSeatBid()).isEmpty(); + assertThat(result.getIdToStoredResponses()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .containsOnly("No stored responses were found for ids: id"); } @@ -98,7 +98,7 @@ public void mapShouldReturnFilledStoredResponseResultWithoutErrors() { new HashSet<>(asList("id1", "id2"))); // then - assertThat(result.getStoredSeatBid()).hasSize(2) + assertThat(result.getIdToStoredResponses()).hasSize(2) .contains(new AbstractMap.SimpleEntry<>("id1", "data1"), new AbstractMap.SimpleEntry<>("id2", "data2")); assertThat(result.getErrors()).isEmpty(); } diff --git a/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java b/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java new file mode 100644 index 00000000000..eb768c43689 --- /dev/null +++ b/src/test/java/org/prebid/server/settings/helper/StoredItemResolverTest.java @@ -0,0 +1,133 @@ +package org.prebid.server.settings.helper; + +import org.junit.Test; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.settings.model.StoredDataType; +import org.prebid.server.settings.model.StoredItem; + +import java.util.HashSet; +import java.util.Set; + +import static java.util.Collections.emptySet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class StoredItemResolverTest { + + @Test + public void resolveShouldFailWhenNoStoredData() { + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp, null, "id", emptySet())) + .withMessage("No stored imp found for id: id"); + } + + @Test + public void resolveShouldFailWhenMultipleStoredDataButNoAccountInRequest() { + // given + final Set storedItems = givenMultipleStoredData(); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp, null, "id", storedItems)) + .withMessage("Multiple stored imps found for id: id but no account was specified"); + } + + @Test + public void resolveShouldFailWhenMultipleStoredDataButAccountDiffers() { + // given + final Set storedItems = givenMultipleStoredData(); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp, "1003", "id", storedItems)) + .withMessage("No stored imp found among multiple id: id for account: 1003"); + } + + @Test + public void resolveShouldReturnResultWhenMultipleStoredDataForAppropriateAccount() { + // given + final Set storedItems = givenMultipleStoredData(); + + // when + final StoredItem storedItem = StoredItemResolver.resolve(StoredDataType.imp, "1002", "id", storedItems); + + // then + assertThat(storedItem).isEqualTo(StoredItem.of("1002", "data2")); + } + + @Test + public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountInRequest() { + // given + final Set storedItems = new HashSet<>(); + storedItems.add(StoredItem.of("1001", "data1")); + + // when + final StoredItem storedItem = StoredItemResolver.resolve(StoredDataType.imp, "1001", "", storedItems); + + // then + assertThat(storedItem).isEqualTo(StoredItem.of("1001", "data1")); + } + + @Test + public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountInStoredData() { + // given + final Set storedItems = new HashSet<>(); + storedItems.add(StoredItem.of(null, "data1")); + + // when + final StoredItem storedItem = StoredItemResolver.resolve(StoredDataType.imp, "1001", "id", storedItems); + + // then + assertThat(storedItem).isEqualTo(StoredItem.of(null, "data1")); + } + + @Test + public void resolveShouldReturnResultWhenSingleStoredDataButNoAccountBothInRequestAndStoredData() { + // given + final Set storedItems = new HashSet<>(); + storedItems.add(StoredItem.of(null, "data1")); + + // when + final StoredItem storedItem = StoredItemResolver.resolve(StoredDataType.imp, null, "id", storedItems); + + // then + assertThat(storedItem).isEqualTo(StoredItem.of(null, "data1")); + } + + @Test + public void resolveShouldFailWhenSingleStoredDataForAppropriateAccount() { + // given + final Set storedItems = givenSingleStoredData(); + + // when + final StoredItem storedItem = StoredItemResolver.resolve(StoredDataType.imp, "1001", "id", storedItems); + + // then + assertThat(storedItem).isEqualTo(StoredItem.of("1001", "data1")); + } + + @Test + public void resolveShouldFailWhenSingleStoredDataButAccountDiffers() { + // given + final Set storedItems = givenSingleStoredData(); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> StoredItemResolver.resolve(StoredDataType.imp, "1002", "id", storedItems)) + .withMessage("No stored imp found for id: id for account: 1002"); + } + + private static Set givenSingleStoredData() { + final Set storedItems = new HashSet<>(); + storedItems.add(StoredItem.of("1001", "data1")); + return storedItems; + } + + private static Set givenMultipleStoredData() { + final Set storedItems = new HashSet<>(); + storedItems.add(StoredItem.of("1001", "data1")); + storedItems.add(StoredItem.of("1002", "data2")); + return storedItems; + } +} diff --git a/src/test/java/org/prebid/server/settings/mapper/JdbcStoredDataResultMapperTest.java b/src/test/java/org/prebid/server/settings/mapper/JdbcStoredDataResultMapperTest.java deleted file mode 100644 index 33bbf1a4905..00000000000 --- a/src/test/java/org/prebid/server/settings/mapper/JdbcStoredDataResultMapperTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.prebid.server.settings.mapper; - -import io.vertx.core.json.JsonArray; -import io.vertx.ext.sql.ResultSet; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.prebid.server.settings.model.StoredDataResult; - -import java.util.AbstractMap; -import java.util.Arrays; -import java.util.List; - -import static java.util.Collections.emptyList; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; - -public class JdbcStoredDataResultMapperTest { - - @Rule - public final MockitoRule mockitoRule = MockitoJUnit.rule(); - - @Mock - private ResultSet resultSet; - - @Test - public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasEmptyResult() { - // given - given(resultSet.getResults()).willReturn(emptyList()); - - // when - final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet); - - // then - assertThat(result.getStoredIdToRequest()).isEmpty(); - assertThat(result.getStoredIdToImp()).isEmpty(); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("No stored requests or imps found"); - } - - @Test - public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasEmptyResultForGivenIDs() { - // given - given(resultSet.getResults()).willReturn(emptyList()); - - // when - final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, - singleton("reqId"), singleton("impId")); - - // then - assertThat(result.getStoredIdToRequest()).isEmpty(); - assertThat(result.getStoredIdToImp()).isEmpty(); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("No stored requests for ids [reqId] and stored imps for ids [impId] were found"); - } - - @Test - public void mapShouldReturnEmptyStoredResultWithErrorWhenResultSetHasLessColumns() { - // given - given(resultSet.getResults()).willReturn(singletonList(new JsonArray(Arrays.asList("id1", "data")))); - - // when - final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet); - - // then - assertThat(result.getStoredIdToRequest()).isEmpty(); - assertThat(result.getStoredIdToImp()).isEmpty(); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Result set column number is less than expected"); - } - - @Test - public void mapShouldReturnStoredResultWithErrorForMissingID() { - // given - given(resultSet.getResults()).willReturn(singletonList(new JsonArray(Arrays.asList("id1", "data", "request")))); - - // when - final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet, singleton("id1"), singleton("id2")); - - // then - assertThat(result.getStoredIdToImp()).isEmpty(); - assertThat(result.getStoredIdToRequest()).hasSize(1) - .containsOnly(new AbstractMap.SimpleEntry<>("id1", "data")); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("No stored imp found for id: id2"); - } - - @Test - public void mapSkipResultSetWithInvalidType() { - // given - final List jsonArrays = Arrays.asList( - new JsonArray(Arrays.asList("id1", "data", "request")), - new JsonArray(Arrays.asList("id2", "data", "invalid"))); - - given(resultSet.getResults()).willReturn(jsonArrays); - - // when - final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getStoredIdToImp()).isEmpty(); - assertThat(result.getStoredIdToRequest()).hasSize(1) - .containsOnly(new AbstractMap.SimpleEntry<>("id1", "data")); - } - - @Test - public void mapShouldReturnStoredResultWithExpectedResult() { - // given - final List jsonArrays = Arrays.asList( - new JsonArray(Arrays.asList("id1", "data", "request")), - new JsonArray(Arrays.asList("id2", "data", "imp"))); - - given(resultSet.getResults()).willReturn(jsonArrays); - - // when - final StoredDataResult result = JdbcStoredDataResultMapper.map(resultSet); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getStoredIdToImp()).hasSize(1) - .containsOnly(new AbstractMap.SimpleEntry<>("id2", "data")); - assertThat(result.getStoredIdToRequest()).hasSize(1) - .containsOnly(new AbstractMap.SimpleEntry<>("id1", "data")); - } -} diff --git a/src/test/java/org/prebid/server/settings/service/HttpPeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/HttpPeriodicRefreshServiceTest.java index 3f62fb2d073..d87d511465d 100644 --- a/src/test/java/org/prebid/server/settings/service/HttpPeriodicRefreshServiceTest.java +++ b/src/test/java/org/prebid/server/settings/service/HttpPeriodicRefreshServiceTest.java @@ -101,7 +101,6 @@ public void shouldCallInvalidateAndSaveWithExpectedParameters() { verify(cacheNotificationListener).save(expectedRequests, expectedImps); verify(cacheNotificationListener).invalidate(singletonList("id1"), emptyList()); verify(cacheNotificationListener).save(emptyMap(), expectedImps); - } @Test diff --git a/src/test/java/org/prebid/server/settings/service/JdbcPeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/JdbcPeriodicRefreshServiceTest.java index 2110ecd80a1..5e49e305238 100644 --- a/src/test/java/org/prebid/server/settings/service/JdbcPeriodicRefreshServiceTest.java +++ b/src/test/java/org/prebid/server/settings/service/JdbcPeriodicRefreshServiceTest.java @@ -11,6 +11,8 @@ import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; import org.prebid.server.settings.CacheNotificationListener; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.vertx.jdbc.JdbcClient; @@ -24,7 +26,6 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; @@ -37,21 +38,22 @@ public class JdbcPeriodicRefreshServiceTest { - private static TimeoutFactory timeoutFactory = new TimeoutFactory( - Clock.fixed(Instant.now(), ZoneId.systemDefault())); - @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private CacheNotificationListener cacheNotificationListener; @Mock + private Vertx vertx; + @Mock private JdbcClient jdbcClient; + private final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + private final TimeoutFactory timeoutFactory = new TimeoutFactory(clock); @Mock - private Vertx vertx; + private Metrics metrics; - private Map expectedRequests = singletonMap("id1", "value1"); - private Map expectedImps = singletonMap("id2", "value2"); + private final Map expectedRequests = singletonMap("id1", "value1"); + private final Map expectedImps = singletonMap("id2", "value2"); @Before public void setUp() { @@ -66,31 +68,10 @@ public void setUp() { .willReturn(Future.succeededFuture(updateResult)); } - @Test - public void creationShouldFailOnNullArgumentsAndBlankQuery() { - assertThatNullPointerException().isThrownBy(() -> createAndInitService( - null, null, null, 0, null, null, null, 0)); - assertThatNullPointerException().isThrownBy(() -> createAndInitService( - cacheNotificationListener, null, null, 0, null, null, null, 0)); - assertThatNullPointerException().isThrownBy(() -> createAndInitService( - cacheNotificationListener, vertx, null, 0, null, null, null, 0)); - assertThatNullPointerException().isThrownBy(() -> createAndInitService( - cacheNotificationListener, vertx, jdbcClient, 0, null, null, null, 0)); - assertThatNullPointerException().isThrownBy(() -> createAndInitService( - cacheNotificationListener, vertx, jdbcClient, 0, "init_query", null, null, 0)); - assertThatNullPointerException().isThrownBy(() -> createAndInitService( - cacheNotificationListener, vertx, jdbcClient, 0, "init_query", "update_query", null, 0)); - assertThatNullPointerException().isThrownBy(() -> createAndInitService( - cacheNotificationListener, vertx, jdbcClient, 0, " ", null, timeoutFactory, 0)); - assertThatNullPointerException().isThrownBy(() -> createAndInitService( - cacheNotificationListener, vertx, jdbcClient, 0, "init_query", " ", timeoutFactory, 0)); - } - @Test public void shouldCallSaveWithExpectedParameters() { // when - createAndInitService(cacheNotificationListener, vertx, jdbcClient, 1000, - "init_query", "update_query", timeoutFactory, 2000); + createAndInitService(1000); // then verify(cacheNotificationListener).save(expectedRequests, expectedImps); @@ -103,8 +84,7 @@ public void shouldCallInvalidateAndSaveWithExpectedParameters() { .willAnswer(withSelfAndPassObjectToHandler(1L)); // when - createAndInitService(cacheNotificationListener, vertx, jdbcClient, 1000, - "init_query", "update_query", timeoutFactory, 2000); + createAndInitService(1000); // then verify(cacheNotificationListener).save(expectedRequests, expectedImps); @@ -119,8 +99,7 @@ public void initializeShouldMakeOneInitialRequestAndTwoScheduledRequestsWithPara .willAnswer(withSelfAndPassObjectToHandler(1L, 2L)); // when - createAndInitService(cacheNotificationListener, vertx, jdbcClient, 1000, - "init_query", "update_query", timeoutFactory, 2000); + createAndInitService(1000); // then verify(jdbcClient).executeQuery(eq("init_query"), eq(emptyList()), any(), any()); @@ -130,21 +109,54 @@ public void initializeShouldMakeOneInitialRequestAndTwoScheduledRequestsWithPara @Test public void initializeShouldMakeOnlyOneInitialRequestIfRefreshPeriodIsNegative() { // when - createAndInitService(cacheNotificationListener, vertx, jdbcClient, -1, - "init_query", "update_query", timeoutFactory, 2000); + createAndInitService(-1); // then verify(vertx, never()).setPeriodic(anyLong(), any()); verify(jdbcClient).executeQuery(anyString(), anyList(), any(), any()); } - private static void createAndInitService(CacheNotificationListener cacheNotificationListener, - Vertx vertx, JdbcClient jdbcClient, long refresh, - String query, String updateQuery, - TimeoutFactory timeoutFactory, long timeout) { - final JdbcPeriodicRefreshService jdbcPeriodicRefreshService = - new JdbcPeriodicRefreshService(cacheNotificationListener, vertx, jdbcClient, refresh, - query, updateQuery, timeoutFactory, timeout); + @Test + public void shouldUpdateTimerMetric() { + // when + createAndInitService(1000); + + // then + verify(metrics).updateSettingsCacheRefreshTime( + eq(MetricName.stored_request), eq(MetricName.initialize), anyLong()); + } + + @Test + public void shouldUpdateTimerAndErrorMetric() { + // given + given(jdbcClient.executeQuery(eq("init_query"), anyList(), any(), any())) + .willReturn(Future.failedFuture("Query error")); + + // when + createAndInitService(1000); + + // then + verify(metrics).updateSettingsCacheRefreshTime( + eq(MetricName.stored_request), eq(MetricName.initialize), anyLong()); + verify(metrics).updateSettingsCacheRefreshErrorMetric( + eq(MetricName.stored_request), eq(MetricName.initialize)); + } + + private void createAndInitService(long refresh) { + + final JdbcPeriodicRefreshService jdbcPeriodicRefreshService = new JdbcPeriodicRefreshService( + "init_query", + "update_query", + refresh, + 2000, + MetricName.stored_request, + cacheNotificationListener, + vertx, + jdbcClient, + timeoutFactory, + metrics, + clock); + jdbcPeriodicRefreshService.initialize(); } diff --git a/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java b/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java new file mode 100644 index 00000000000..95d6aee6d55 --- /dev/null +++ b/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java @@ -0,0 +1,119 @@ +package org.prebid.server.spring.config.bidder.util; + +import org.junit.Test; +import org.prebid.server.bidder.Usersyncer; +import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties; + +import java.net.MalformedURLException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class UsersyncerCreatorTest { + + @Test + public void createShouldReturnUsersyncerWithConcatenatedExternalAndRedirectUrl() { + // given + final UsersyncConfigurationProperties config = new UsersyncConfigurationProperties(); + config.setCookieFamilyName("rubicon"); + config.setUrl("//usersync-url"); + config.setRedirectUrl("/redirect-url"); + config.setType("redirect"); + config.setSupportCors(true); + + // when and then + assertThat(UsersyncerCreator.create("http://localhost:8000").apply(config)) + .extracting(usersyncer -> usersyncer.getPrimaryMethod().getRedirectUrl()) + .containsOnly("http://localhost:8000/redirect-url"); + } + + @Test + public void createShouldReturnUsersyncerWithEmptyRedirectUrlWhenItWasNotDefined() { + // given + final UsersyncConfigurationProperties config = new UsersyncConfigurationProperties(); + config.setCookieFamilyName("rubicon"); + config.setUrl("//usersync-url"); + config.setType("redirect"); + config.setSupportCors(true); + + // when and then + assertThat(UsersyncerCreator.create(null).apply(config)) + .extracting(usersyncer -> usersyncer.getPrimaryMethod().getRedirectUrl()) + .containsOnly(""); + } + + @Test + public void createShouldValidateExternalUrl() { + // given + final UsersyncConfigurationProperties config = new UsersyncConfigurationProperties(); + config.setUrl("//usersync-url"); + config.setRedirectUrl("not-valid-url"); + config.setType("redirect"); + config.setSupportCors(true); + + // given, when and then + assertThatThrownBy(() -> UsersyncerCreator.create(null).apply(config)) + .hasCauseExactlyInstanceOf(MalformedURLException.class) + .hasMessage("URL supplied is not valid: null"); + } + + @Test + public void createShouldReturnUsersyncerWithPrimaryAndSecondaryMethods() { + // given + final UsersyncConfigurationProperties config = new UsersyncConfigurationProperties(); + config.setCookieFamilyName("rubicon"); + config.setUrl("//usersync-url"); + config.setRedirectUrl("/redirect-url"); + config.setType("redirect"); + config.setSupportCors(true); + + final UsersyncConfigurationProperties.SecondaryConfigurationProperties secondaryMethodConfig = + new UsersyncConfigurationProperties.SecondaryConfigurationProperties(); + secondaryMethodConfig.setUrl("//usersync-url-secondary"); + secondaryMethodConfig.setRedirectUrl("/redirect-url-secondary"); + secondaryMethodConfig.setType("iframe"); + secondaryMethodConfig.setSupportCors(false); + + config.setSecondary(secondaryMethodConfig); + + // when and then + assertThat(UsersyncerCreator.create("http://localhost:8000").apply(config)).isEqualTo( + Usersyncer.of( + "rubicon", + Usersyncer.UsersyncMethod.of( + "redirect", + "//usersync-url", + "http://localhost:8000/redirect-url", + true), + Usersyncer.UsersyncMethod.of( + "iframe", + "//usersync-url-secondary", + "http://localhost:8000/redirect-url-secondary", + false))); + } + + @Test + public void createShouldFailWhenSecondaryMethodPresentAndPrimaryAbsent() { + // given + final UsersyncConfigurationProperties config = new UsersyncConfigurationProperties(); + config.setUrl(""); + config.setCookieFamilyName("rubicon"); + config.setType("redirect"); + config.setSupportCors(true); + + final UsersyncConfigurationProperties.SecondaryConfigurationProperties secondaryMethodConfig = + new UsersyncConfigurationProperties.SecondaryConfigurationProperties(); + secondaryMethodConfig.setUrl("//usersync-url-secondary"); + secondaryMethodConfig.setRedirectUrl("/redirect-url-secondary"); + secondaryMethodConfig.setType("iframe"); + secondaryMethodConfig.setSupportCors(false); + + config.setSecondary(secondaryMethodConfig); + + // when and then + assertThatThrownBy(() -> UsersyncerCreator.create("http://localhost:8000").apply(config)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Invalid usersync configuration: primary method is missing while secondary is" + + " present. Configuration:"); + } +} diff --git a/src/test/java/org/prebid/server/util/HttpUtilTest.java b/src/test/java/org/prebid/server/util/HttpUtilTest.java index 77242c33ca2..c4b6fd6d760 100644 --- a/src/test/java/org/prebid/server/util/HttpUtilTest.java +++ b/src/test/java/org/prebid/server/util/HttpUtilTest.java @@ -2,21 +2,36 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.Cookie; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.HttpRequestContext; +import java.time.ZonedDateTime; import java.util.Map; +import java.util.function.Consumer; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; 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.Mockito.verifyNoMoreInteractions; public class HttpUtilTest { @@ -25,15 +40,12 @@ public class HttpUtilTest { @Mock private RoutingContext routingContext; + @Mock + private HttpServerResponse httpResponse; - @Test - public void isSafariShouldReturnTrue() { - assertThat(HttpUtil.isSafari("Useragent with Safari browser and AppleWebKit built-in.")).isTrue(); - } - - @Test - public void isSafariShouldReturnFalse() { - assertThat(HttpUtil.isSafari("Useragent with Safari browser but Chromium forked by.")).isFalse(); + @Before + public void setUp() { + given(routingContext.response()).willReturn(httpResponse); } @Test @@ -104,21 +116,21 @@ public void addHeaderIfValueIsNotEmptyShouldNotAddHeaderIfValueIsNull() { } @Test - public void getDomainFromUrlShouldReturnDomain() { + public void getHostFromUrlShouldReturnDomain() { // given and when - final String domain = HttpUtil.getDomainFromUrl("http://rubicon.com/ad"); + final String host = HttpUtil.getHostFromUrl("http://www.domain.com/ad"); // then - assertThat(domain).isEqualTo("rubicon.com"); + assertThat(host).isEqualTo("www.domain.com"); } @Test - public void getDomainFromUrlShouldReturnNullIfUrlIsMalformed() { + public void getHostFromUrlShouldReturnNullIfUrlIsMalformed() { // given and when - final String domain = HttpUtil.getDomainFromUrl("rubicon.com"); + final String host = HttpUtil.getHostFromUrl("www.domain.com"); // then - assertThat(domain).isNull(); + assertThat(host).isNull(); } @Test @@ -134,17 +146,125 @@ public void cookiesAsMapShouldReturnExpectedResult() { .containsOnly(entry("name", "value")); } + @Test + public void cookiesAsMapFromRequestShouldReturnExpectedResult() { + // given + final HttpRequestContext httpRequest = HttpRequestContext.builder() + .headers(CaseInsensitiveMultiMap.builder() + .add(HttpHeaders.COOKIE, Cookie.cookie("name", "value").encode()) + .build()) + .build(); + + // when + final Map cookies = HttpUtil.cookiesAsMap(httpRequest); + + // then + assertThat(cookies).hasSize(1) + .containsOnly(entry("name", "value")); + } + @Test public void toSetCookieHeaderValueShouldReturnExpectedString() { // given final Cookie cookie = Cookie.cookie("cookie", "value") .setPath("/") - .setDomain("rubicon.com"); + .setDomain("domain.com"); // when final String setCookieHeaderValue = HttpUtil.toSetCookieHeaderValue(cookie); // then - assertThat(setCookieHeaderValue).isEqualTo("cookie=value; Path=/; Domain=rubicon.com; SameSite=None; Secure"); + assertThat(setCookieHeaderValue).isEqualTo("cookie=value; Path=/; Domain=domain.com; SameSite=None; Secure"); + } + + @SuppressWarnings("unchecked") + @Test + public void executeSafelyShouldSkipResponseIfClientClosedConnection() { + // given + given(httpResponse.closed()).willReturn(true); + final Consumer responseConsumer = mock(Consumer.class); + + // when + HttpUtil.executeSafely(routingContext, "endpoint", responseConsumer); + + // then + verifyNoMoreInteractions(responseConsumer); + } + + @SuppressWarnings("unchecked") + @Test + public void executeSafelyShouldRespondToClient() { + // given + final Consumer responseConsumer = mock(Consumer.class); + + // when + final boolean result = HttpUtil.executeSafely(routingContext, "endpoint", responseConsumer); + + // then + verify(responseConsumer).accept(eq(httpResponse)); + assertThat(result).isTrue(); + } + + @SuppressWarnings("unchecked") + @Test + public void executeSafelyShouldReturnFalseIfResponseFailed() { + // given + final Consumer responseConsumer = mock(Consumer.class); + doThrow(new RuntimeException("error")).when(responseConsumer).accept(any()); + + // when + final boolean result = HttpUtil.executeSafely(routingContext, "endpoint", responseConsumer); + + // then + assertThat(result).isFalse(); + } + + @Test + public void getDateFromHeaderShouldReturnDate() { + // given + final MultiMap headers = MultiMap.caseInsensitiveMultiMap().add("date-header", + "2019-11-04T13:31:24.365+02:00[Europe/Kiev]"); + + // when + final ZonedDateTime result = HttpUtil.getDateFromHeader(headers, "date-header"); + + // then + assertThat(result).isEqualTo(ZonedDateTime.parse("2019-11-04T13:31:24.365+02:00[Europe/Kiev]")); + } + + @Test + public void getDateFromHeaderShouldReturnNullWhenHeaderWasNotFound() { + // given + final MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + + // when + final ZonedDateTime result = HttpUtil.getDateFromHeader(headers, "not-exist"); + + // then + assertThat(result).isNull(); + } + + @Test + public void getDateFromHeaderShouldThrowExceptionWhenHeaderHasIncorrectFormat() { + // given + final MultiMap headers = MultiMap.caseInsensitiveMultiMap().add("date-header", "invalid"); + + // when and then + assertThatThrownBy(() -> HttpUtil.getDateFromHeader(headers, "date-header")) + .isInstanceOf(PreBidException.class) + .hasMessage("date-header header is not compatible to ISO-8601 format: invalid"); + } + + @Test + public void getDateFromHeaderShouldReturnDateFromHeaders() { + // given + final MultiMap headers = MultiMap.caseInsensitiveMultiMap().add("date-header", + "2019-11-04T13:31:24.365+02:00[Europe/Kiev]"); + + // when + final ZonedDateTime result = HttpUtil.getDateFromHeader(headers, "date-header"); + + // then + assertThat(result).isEqualTo(ZonedDateTime.parse("2019-11-04T13:31:24.365+02:00[Europe/Kiev]")); } } diff --git a/src/test/java/org/prebid/server/util/JsonMergeUtilTest.java b/src/test/java/org/prebid/server/util/JsonMergeUtilTest.java deleted file mode 100644 index b355617bfad..00000000000 --- a/src/test/java/org/prebid/server/util/JsonMergeUtilTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.prebid.server.util; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Publisher; -import com.iab.openrtb.request.Site; -import org.junit.Before; -import org.junit.Test; -import org.prebid.server.VertxTest; -import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigFpd; - -import static org.assertj.core.api.Assertions.assertThat; - -public class JsonMergeUtilTest extends VertxTest { - - private JsonMergeUtil target; - - @Before - public void setUp() { - target = new JsonMergeUtil(jacksonMapper); - } - - @Test - public void mergeShouldReturnMergedObject() { - // given - final ObjectNode siteWithPage = mapper.valueToTree(Site.builder().page("testPage").build()); - final Publisher publisherWithId = Publisher.builder().id("testId").build(); - final ObjectNode appWithPublisherId = mapper.valueToTree(App.builder().publisher(publisherWithId).build()); - final ExtBidderConfigFpd firstBidderConfigFpd = ExtBidderConfigFpd.of(siteWithPage, appWithPublisherId, null); - - final ObjectNode siteWithDomain = mapper.valueToTree(Site.builder().domain("testDomain").build()); - final Publisher publisherWithIdAndDomain = Publisher.builder().id("shouldNotBe").domain("domain").build(); - final ObjectNode appWithUpdatedPublisher = mapper.valueToTree(App.builder() - .publisher(publisherWithIdAndDomain).build()); - final ExtBidderConfigFpd secondBidderConfigFpd = ExtBidderConfigFpd.of(siteWithDomain, appWithUpdatedPublisher, - null); - - // when - final ExtBidderConfigFpd result = target.merge(firstBidderConfigFpd, secondBidderConfigFpd, - ExtBidderConfigFpd.class); - - // then - final ObjectNode mergedSite = mapper.valueToTree(Site.builder().page("testPage").domain("testDomain").build()); - final Publisher mergedPublisher = Publisher.builder().id("testId").domain("domain").build(); - final ObjectNode mergedApp = mapper.valueToTree(App.builder().publisher(mergedPublisher).build()); - final ExtBidderConfigFpd mergedConfigFpd = ExtBidderConfigFpd.of(mergedSite, mergedApp, null); - - assertThat(result).isEqualTo(mergedConfigFpd); - } - - @Test - public void mergeShouldReturnOriginalObjectWhenMergedObjectIsNull() { - // given - final Site site = Site.builder().build(); - - // when - final Site result = target.merge(site, null, Site.class); - - // then - assertThat(result).isEqualTo(site); - } - - @Test - public void mergeShouldReturnMergedObjectWhenOriginalObjectIsNull() { - // given - final Site site = Site.builder().build(); - - // when - final Site result = target.merge(null, site, Site.class); - - // then - assertThat(result).isEqualTo(site); - } - -} diff --git a/src/test/java/org/prebid/server/util/VersionInfoTest.java b/src/test/java/org/prebid/server/util/VersionInfoTest.java new file mode 100644 index 00000000000..13caeeaca41 --- /dev/null +++ b/src/test/java/org/prebid/server/util/VersionInfoTest.java @@ -0,0 +1,50 @@ +package org.prebid.server.util; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; + +import static org.assertj.core.api.Assertions.assertThat; + +public class VersionInfoTest extends VertxTest { + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Test + public void shouldCreateVersionWithUndefinedForAllFieldsIfFileWasNotFound() { + // when + VersionInfo versionInfo = VersionInfo.create("not_found.json", jacksonMapper); + + // then + assertThat(versionInfo) + .extracting(VersionInfo::getVersion, VersionInfo::getCommitHash) + .containsOnly("undefined", "undefined"); + } + + @Test + public void shouldCreateVersionInfoWithAllProperties() { + // when + VersionInfo versionInfo = VersionInfo.create( + "org/prebid/server/util/resource/version/version.json", jacksonMapper); + + // then + assertThat(versionInfo) + .extracting(VersionInfo::getVersion, VersionInfo::getCommitHash) + .containsOnly("1.41.0", "4df3f6192d7938ccdaac04df783c46c7e8847d08"); + } + + @Test + public void shouldCreateVersionWithUndefinedForEachMissingPropertyInFile() { + // when + VersionInfo versionInfo = VersionInfo.create( + "org/prebid/server/util/resource/version/empty.json", jacksonMapper); + + // then + assertThat(versionInfo) + .extracting(VersionInfo::getVersion, VersionInfo::getCommitHash) + .containsOnly("undefined", "undefined"); + } +} diff --git a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java index b240b9ef8c3..2751185b181 100644 --- a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java @@ -20,6 +20,7 @@ import org.prebid.server.proto.openrtb.ext.request.rubicon.ExtImpRubicon; import org.prebid.server.proto.openrtb.ext.request.somoaudience.ExtImpSomoaudience; import org.prebid.server.proto.openrtb.ext.request.sovrn.ExtImpSovrn; +import org.prebid.server.proto.response.BidderInfo; import org.prebid.server.util.ResourceUtil; import java.io.IOException; @@ -31,12 +32,15 @@ import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; public class BidderParamValidatorTest extends VertxTest { private static final String RUBICON = "rubicon"; private static final String APPNEXUS = "appnexus"; + private static final String APPNEXUS_ALIAS = "appnexusAlias"; private static final String ADFORM = "adform"; private static final String BRIGHTROLL = "brightroll"; private static final String SOVRN = "sovrn"; @@ -57,9 +61,21 @@ public class BidderParamValidatorTest extends VertxTest { @Before public void setUp() { - given(bidderCatalog.names()).willReturn(new HashSet<>( - asList(RUBICON, APPNEXUS, ADFORM, BRIGHTROLL, SOVRN, ADTELLIGENT, FACEBOOK, OPENX, EPLANNING, - SOMOAUDIENCE, BEACHFRONT))); + given(bidderCatalog.names()).willReturn(new HashSet<>(asList( + RUBICON, + APPNEXUS, + APPNEXUS_ALIAS, + ADFORM, + BRIGHTROLL, + SOVRN, + ADTELLIGENT, + FACEBOOK, + OPENX, + EPLANNING, + SOMOAUDIENCE, + BEACHFRONT))); + given(bidderCatalog.bidderInfoByName(anyString())).willReturn(givenBidderInfo()); + given(bidderCatalog.bidderInfoByName(eq(APPNEXUS_ALIAS))).willReturn(givenBidderInfo(APPNEXUS)); bidderParamValidator = BidderParamValidator.create(bidderCatalog, "static/bidder-params", jacksonMapper); } @@ -138,6 +154,34 @@ public void validateShouldNotReturnValidationMessagesWhenAppnexusImpExtIsOk() { assertThat(messages).isEmpty(); } + @Test + public void validateShouldReturnValidationMessagesWhenAppnexusAliasImpExtNotValid() { + // given + final ExtImpAppnexus ext = ExtImpAppnexus.builder().member("memberId").build(); + + final JsonNode node = mapper.convertValue(ext, JsonNode.class); + + // when + final Set messages = bidderParamValidator.validate(APPNEXUS_ALIAS, node); + + // then + assertThat(messages.size()).isEqualTo(4); + } + + @Test + public void validateShouldNotReturnValidationMessagesWhenAppnexusAliasImpExtIsOk() { + // given + final ExtImpAppnexus ext = ExtImpAppnexus.builder().placementId(1).build(); + + final JsonNode node = mapper.convertValue(ext, JsonNode.class); + + // when + final Set messages = bidderParamValidator.validate(APPNEXUS_ALIAS, node); + + // then + assertThat(messages).isEmpty(); + } + @Test public void validateShouldNotReturnValidationMessagesWhenAdformImpExtIsOk() { // given @@ -376,7 +420,7 @@ public void validateShouldReturnValidationMessagesWhenBeachfrontExtNotValid() { final Set messages = bidderParamValidator.validate(BEACHFRONT, node); // then - assertThat(messages.size()).isEqualTo(3); + assertThat(messages.size()).isEqualTo(2); } @Test @@ -394,4 +438,23 @@ public void schemaShouldReturnSchemasString() throws IOException { assertThat(result).isEqualTo(ResourceUtil.readFromClasspath( "org/prebid/server/validation/schema//valid/test-schemas.json")); } + + private static BidderInfo givenBidderInfo(String aliasOf) { + return BidderInfo.create( + true, + "https://endpoint.com", + aliasOf, + null, + null, + null, + null, + 0, + true, + true, + false); + } + + private static BidderInfo givenBidderInfo() { + return givenBidderInfo(null); + } } diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index 5f2331643c7..7846c121ebc 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -1,6 +1,7 @@ package org.prebid.server.validation; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.App.AppBuilder; @@ -36,19 +37,26 @@ import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.proto.openrtb.ext.request.BidAdjustmentMediaType; import org.prebid.server.proto.openrtb.ext.request.ExtDevice; import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidadjustmentfactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ExtUserDigiTrust; import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserEidUid; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; @@ -56,6 +64,7 @@ import java.math.BigDecimal; import java.util.Collections; +import java.util.EnumMap; import java.util.LinkedHashSet; import java.util.function.Function; @@ -68,6 +77,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; public class RequestValidatorTest extends VertxTest { @@ -87,6 +97,7 @@ public class RequestValidatorTest extends VertxTest { public void setUp() { given(bidderParamValidator.validate(any(), any())).willReturn(Collections.emptySet()); given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(true); + given(bidderCatalog.isActive(eq(RUBICON))).willReturn(true); requestValidator = new RequestValidator(bidderCatalog, bidderParamValidator, jacksonMapper); } @@ -278,7 +289,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasNullFormatAndNoSiz .banner(Banner.builder() .format(null) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -302,7 +314,8 @@ public void validateShouldReturnEmptyValidationMessagesWhenBannerHasNullFormatAn .h(250) .format(null) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -322,7 +335,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoSi .banner(Banner.builder() .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -345,7 +359,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoHe .w(300) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -368,7 +383,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoWi .h(600) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -392,7 +408,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZero .h(0) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -416,7 +433,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasZeroHeight() { .h(0) .format(singletonList(Format.builder().build())) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -439,7 +457,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZero .w(0) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -463,7 +482,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasZeroWidth() { .w(0) .format(singletonList(Format.builder().build())) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -486,7 +506,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNega .w(-300) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -510,7 +531,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasNegativeWidth() { .w(-300) .format(singletonList(Format.builder().build())) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -533,7 +555,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNega .w(600) .format(emptyList()) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -557,7 +580,8 @@ public void validateShouldReturnValidationMessageWhenBannerHasNegativeHeight() { .w(600) .format(singletonList(Format.builder().build())) .build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) .build())) .build(); @@ -1003,33 +1027,16 @@ public void validateShouldReturnValidationMessageWhenSiteExtAmpIsGreaterThanOne( @Test public void validateShouldReturnValidationMessageWhenRequestAppAndRequestSiteBothMissed() { // given - final BidRequest.BidRequestBuilder bidRequestBuilder = overwriteSite(validBidRequestBuilder(), - Function.identity()); - - final BidRequest bidRequest = overwriteApp(bidRequestBuilder, Function.identity()).build(); - - // when - final ValidationResult result = requestValidator.validate(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site or request.app must be defined, but not both"); - } - - @Test - public void validateShouldReturnValidationMessageWhenRequestAppAndRequestSiteBothPresent() { - // given - final BidRequest.BidRequestBuilder bidRequestBuilder = overwriteSite(validBidRequestBuilder(), - siteBuilder -> Site.builder().id("1").page("2")); - - final BidRequest bidRequest = overwriteApp(bidRequestBuilder, appBuilder -> App.builder().id("3")).build(); + final BidRequest bidRequest = validBidRequestBuilder() + .site(null) + .app(null) + .build(); // when final ValidationResult result = requestValidator.validate(bidRequest); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site or request.app must be defined, but not both"); + assertThat(result.getErrors()).hasSize(1).containsOnly("request.site or request.app must be defined"); } @Test @@ -1037,7 +1044,7 @@ public void validateShouldReturnValidationMessageWhenMinWidthPercIsNull() { // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(null, null)))) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(null, null)))) .build()) .build(); @@ -1054,7 +1061,7 @@ public void validateShouldReturnValidationMessageWhenMinWidthPercIsLessThanZero( // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(-1, null)))) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(-1, null)))) .build()) .build(); @@ -1071,7 +1078,7 @@ public void validateShouldReturnValidationMessageWhenMinWidthPercGreaterThanHund // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(101, null)))) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(101, null)))) .build()) .build(); @@ -1088,7 +1095,7 @@ public void validateShouldReturnValidationMessageWhenMinHeightPercIsNull() { // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(50, null)))) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, null)))) .build()) .build(); @@ -1106,7 +1113,7 @@ public void validateShouldReturnValidationMessageWhenMinHeightPercIsLessThanZero // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(50, -1)))) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, -1)))) .build()) .build(); @@ -1124,7 +1131,7 @@ public void validateShouldReturnValidationMessageWhenMinHeightPercGreaterThanHun // given final BidRequest bidRequest = validBidRequestBuilder() .device(Device.builder() - .ext(ExtDevice.of(ExtDevicePrebid.of(ExtDeviceInt.of(50, 101)))) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, 101)))) .build()) .build(); @@ -1150,7 +1157,7 @@ public void validateShouldReturnEmptyValidationMessagesWhenBidRequestIsOk() { } @Test - public void validateShouldReturnValidationMessageWhenNoImpExtBiddersPresent() { + public void validateShouldReturnValidationMessageWhenNoImpExtPrebidPresent() { // given final BidRequest bidRequest = validBidRequestBuilder() .imp(singletonList(validImpBuilder().ext(null).build())) @@ -1161,29 +1168,157 @@ public void validateShouldReturnValidationMessageWhenNoImpExtBiddersPresent() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext must contain at least one bidder"); + .containsOnly("request.imp[0].ext.prebid must be defined"); } @Test - public void validateShouldReturnValidationMessagesWhenImpExtBidderIsUnknown() { + public void validateShouldReturnValidationMessageWhenImpExtPrebidIsNotObject() { // given - final BidRequest bidRequest = validBidRequestBuilder().build(); - given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(false); + final BidRequest bidRequest = validBidRequestBuilder() + .imp(singletonList(validImpBuilder().ext(mapper.valueToTree(singletonMap("prebid", "test"))).build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.imp[0].ext.prebid must an object type"); + } + + @Test + public void validateShouldReturnValidationMessagesWhenExtImpPrebidBidderWasNotDefined() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .imp(singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("attr", "value")))).build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.imp[0].ext.prebid.bidder must be defined"); + } + + @Test + public void validateShouldReturnValidationMessageWhenImpExtPrebidBiddersNotDefinedForStoredBidResponse() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) + .storedAuctionResponse(ExtStoredAuctionResponse.of("id")) + .build()); + + final BidRequest bidRequest = validBidRequestBuilder() + .imp(singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.imp[0].ext.prebid.bidder should be defined for storedbidresponse"); + } + + @Test + public void validateShouldReturnValidationMessageWhenStoredBidResponseBidderMissed() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of(null, "id"))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + final BidRequest bidRequest = validBidRequestBuilder() + .imp(singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.imp[0].ext.prebid.storedbidresponse.bidder was not defined"); + } + + @Test + public void validateShouldReturnValidationMessageWhenStoredBidResponseIdMissed() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", null))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + final BidRequest bidRequest = validBidRequestBuilder() + .imp(singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Id was not defined for request.imp[0].ext.prebid.storedbidresponse.id"); + } + + @Test + public void validateShouldReturnValidationMessageWhenStoredBidResponseBidderIsNotValidBidder() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + given(bidderCatalog.isValidName(eq("bidder"))).willReturn(false); + + final BidRequest bidRequest = validBidRequestBuilder() + .imp(singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.imp[0].ext.prebid.storedbidresponse.bidder is not valid bidder"); + } + + @Test + public void validateShouldReturnValidationMessageWhenStoredBidResponseBidderIsNotInImpExtPrebidBidder() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + given(bidderCatalog.isValidName(eq("bidder"))).willReturn(true); + + final BidRequest bidRequest = validBidRequestBuilder() + .imp(singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) + .build(); // when final ValidationResult result = requestValidator.validate(bidRequest); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext contains unknown bidder: rubicon"); + .containsOnly("request.imp[0].ext.prebid.storedbidresponse.bidder does not have correspondent" + + " bidder parameters"); } @Test - public void validateShouldReturnEmptyValidationMessagesWhenOnlyPrebidImpExtExist() { + public void validateShouldReturnEmptyMessagesWhenExtImpPrebidBidderWasMissedAndHasStoredAuctionResponseWas() { // given final BidRequest bidRequest = validBidRequestBuilder() .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", "test"))).build())) + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("storedauctionresponse", + mapper.createObjectNode().put("id", "1"))))).build())) .build(); // when @@ -1193,6 +1328,37 @@ public void validateShouldReturnEmptyValidationMessagesWhenOnlyPrebidImpExtExist assertThat(result.getErrors()).isEmpty(); } + @Test + public void validateShouldReturnValidationMessageWhenImpExtPrebidBidderIsNotObject() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .imp(singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", "test")))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.imp[0].ext.prebid.bidder must be an object type"); + } + + @Test + public void validateShouldReturnValidationMessagesWhenImpExtPrebidBidderIsUnknown() { + // given + final BidRequest bidRequest = validBidRequestBuilder().build(); + given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(false); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.imp[0].ext.prebid.bidder contains unknown bidder: rubicon"); + } + @Test public void validateShouldReturnValidationMessageWhenBidderExtIsInvalid() { // given @@ -1205,7 +1371,8 @@ public void validateShouldReturnValidationMessageWhenBidderExtIsInvalid() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.rubicon failed validation.\nerrorMessage1\nerrorMessage2"); + .containsOnly( + "request.imp[0].ext.prebid.bidder.rubicon failed validation.\nerrorMessage1\nerrorMessage2"); } @Test @@ -1245,7 +1412,6 @@ public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsNoVal .user(User.builder() .ext(ExtUser.builder() .prebid(ExtUserPrebid.of(emptyMap())) - .digitrust(ExtUserDigiTrust.of(null, null, 0)) .build()) .build()) .build(); @@ -1259,6 +1425,181 @@ public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsNoVal + " If none exist, then request.user.ext.prebid should not be defined"); } + @Test + public void validateShouldReturnValidationMessageWhenEidsPermissionsHasNullElement() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, singletonList(null))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.ext.prebid.data.eidpermissions[] can't be null"); + } + + @Test + public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsNull() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList(ExtRequestPrebidDataEidPermissions.of("source", null)))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.ext.prebid.data.eidpermissions[].bidders[] required values but was empty or" + + " null"); + } + + @Test + public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsEmpty() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList(ExtRequestPrebidDataEidPermissions.of("source", emptyList())))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.ext.prebid.data.eidpermissions[].bidders[] required values but was empty or" + + " null"); + } + + @Test + public void validateShouldReturnValidationMessageWhenEidsPermissionsBidderIsNotRecognizedBidder() { + // given + given(bidderCatalog.isValidName(eq("bidder1"))).willReturn(false); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly( + "request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: 'bidder1'"); + } + + @Test + public void validateShouldReturnValidationMessageWhenEidsPermissionsBidderHasBlankValue() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList(" "))))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: ' '"); + } + + @Test + public void validateShouldNotReturnValidationErrorWhenBidderIsAlias() { + // given + given(bidderCatalog.isValidName(eq("bidder1Alias"))).willReturn(false); + given(bidderCatalog.isValidName(eq("bidder1"))).willReturn(true); + given(bidderCatalog.isActive(eq("bidder1"))).willReturn(true); + + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("bidder1Alias", "bidder1")) + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void validateShouldNotReturenValidationErrorWhenBidderIsAterisk() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList("*"))))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void validateShouldReturnValidationMessageWhenEidsPermissionsHasMissingSource() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of(null, singletonList("bidder1"))))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Missing required value request.ext.prebid.data.eidPermissions[].source"); + } + + @Test + public void validateShouldReturnValidationMessageWhenEidsPermissionsContainsDuplicatedSources() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + asList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList("*")), + ExtRequestPrebidDataEidPermissions.of("source", singletonList("*"))))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Duplicate source source in request.ext.prebid.data.eidpermissions[]"); + } + @Test public void validateShouldReturnValidationMessageWhenCantParseTargetingPriceGranularity() { // given @@ -1318,6 +1659,26 @@ public void validateShouldReturnValidationMessageWhenIncrementIsZero() { .containsOnly("Price granularity error: increment must be a nonzero positive number"); } + @Test + public void validateShouldReturnValidationMessageWhenIncrementIsMissed() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of( + 2, + singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), null))))) + .build()) + .build())) + .build(); + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Price granularity error: increment must be a nonzero positive number"); + } + @Test public void validateShouldReturnValidationMessageWhenIncrementIsNegative() { // given @@ -1433,6 +1794,27 @@ public void validateShouldReturnValidationMessageForInvalidTargeting() { + " must be enabled to enable targeting support"); } + @Test + public void validateShouldReturnValidationMessageWhenRangesContainsMissedMaxValue() { + final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, + asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), + ExtGranularityRange.of(null, BigDecimal.valueOf(0.05)))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranuality)) + .build()) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Price granularity error: max value should not be missed"); + } + @Test public void validateShouldReturnValidationMessageWhenRangesAreNotOrderedByMaxValue() { final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, @@ -1506,7 +1888,6 @@ public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsUnkno .user(User.builder() .ext(ExtUser.builder() .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) - .digitrust(ExtUserDigiTrust.of(null, null, 0)) .build()) .build()) .build(); @@ -1530,7 +1911,6 @@ public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIs .user(User.builder() .ext(ExtUser.builder() .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) - .digitrust(ExtUserDigiTrust.of(null, null, 0)) .build()) .build()) .build(); @@ -1549,7 +1929,6 @@ public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIs .user(User.builder() .ext(ExtUser.builder() .prebid(ExtUserPrebid.of(singletonMap("rubicon", "42"))) - .digitrust(ExtUserDigiTrust.of(null, null, 0)) .build()) .build()) .build(); @@ -1561,25 +1940,6 @@ public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIs assertThat(result.getErrors()).isEmpty(); } - @Test - public void validateShouldReturnValidationMessageWhenDigiTrustPrefNotEqualZero() { - // given; - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .ext(ExtUser.builder() - .digitrust(ExtUserDigiTrust.of(null, null, 1)) - .build()) - .build()) - .build(); - - // when - final ValidationResult result = requestValidator.validate(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user contains a digitrust object that is not valid"); - } - @Test public void validateShouldReturnValidationMessageWhenEidsIsEmpty() { // given @@ -1677,7 +2037,7 @@ public void validateShouldReturnValidationMessageWhenEidUidIdIsMissing() { } @Test - public void validateShouldReturnValidationMessageWhenEidSourceIsNotUnique() { + public void validateShouldReturnErrorWhenEidSourceIsNotUnique() { // given final BidRequest bidRequest = validBidRequestBuilder() .user(User.builder() @@ -1695,8 +2055,7 @@ public void validateShouldReturnValidationMessageWhenEidSourceIsNotUnique() { final ValidationResult result = requestValidator.validate(bidRequest); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.ext.eids must contain unique sources"); + assertThat(result.getErrors()).containsExactly("request.user.ext.eids must contain unique sources"); } @Test @@ -1732,6 +2091,24 @@ public void validateShouldReturnValidationMessageWhenAliasPointOnNotValidBidderN .containsOnly("request.ext.prebid.aliases.alias refers to unknown bidder: fake"); } + @Test + public void validateShouldReturnValidationMessageWhenAliasPointOnDisabledBidder() { + // given + final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("alias", "appnexus")) + .build()); + final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); + given(bidderCatalog.isValidName("appnexus")).willReturn(true); + given(bidderCatalog.isActive("appnexus")).willReturn(false); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.ext.prebid.aliases.alias refers to disabled bidder: appnexus"); + } + @Test public void validateShouldReturnEmptyValidationMessagesWhenAliasesWasUsed() { // given @@ -1772,7 +2149,7 @@ public void validateShouldThrowExceptionWhenNativeRequestEmpty() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp.[0].ext.native contains empty request value"); + .containsOnly("request.imp[0].native contains empty request value"); } @Test @@ -1785,7 +2162,21 @@ public void validateShouldThrowExceptionWhenNativeRequestMalformed() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Error while parsing request.imp.[0].ext.native.request"); + .containsOnly("Error while parsing request.imp[0].native.request"); + } + + @Test + public void validateShouldReturnValidationResultWithoutErrorsForNativeSpecificContextTypes() + throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> + nativeReqCustomizer.context(500).assets(singletonList(Asset.builder().build()))); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); } @Test @@ -1793,7 +2184,7 @@ public void validateShouldReturnValidationResultWithErrorWhenContextTypeOutOfPos throws JsonProcessingException { // given final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(100)); + nativeReqCustomizer.context(323)); // when final ValidationResult result = requestValidator.validate(bidRequest); @@ -1921,7 +2312,7 @@ public void validateShouldReturnValidationResultWithErrorWhenEventTrackersOutOfP // given final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(5).build())).assets(singletonList(Asset.builder().build()))); + .event(323).build())).assets(singletonList(Asset.builder().build()))); // when final ValidationResult result = requestValidator.validate(bidRequest); @@ -1984,12 +2375,41 @@ public void validateShouldReturnValidationResultWithEmptyErrorWhenValidEventTrac assertThat(result.getErrors()).isEmpty(); } + @Test + public void validateShouldReturnValidationResultWithEmptyErrorWhenEventTrackerHasSpecificType() + throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(500).methods(singletonList(2)).build())).assets(singletonList(Asset.builder().build()))); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void validateShouldReturnValidationResultWithoutErrorsForNativeSpecificPlacementTypes() + throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> + nativeReqCustomizer.plcmttype(500).assets(singletonList(Asset.builder().build()))); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + } + @Test public void validateShouldReturnValidationResultWithErrorWhenPlacementTypeOutOfPossibleValuesRange() throws JsonProcessingException { // given final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.plcmttype(100)); + nativeReqCustomizer.plcmttype(323)); // when final ValidationResult result = requestValidator.validate(bidRequest); @@ -2175,7 +2595,24 @@ public void validateShouldReturnValidationResultWithErrorWhenDataTypeOutOfPossib // then assertThat(result.getErrors()).hasSize(1) .containsOnly( - "request.imp[0].native.request.assets[0].data.type must in the range [1, 12]. Got 100"); + "request.imp[0].native.request.assets[0].data.type is invalid. See section 7.4: " + + "https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40"); + } + + @Test + public void validateShouldReturnValidationResultWithoutErrorsWhenDataHasSpecicNativeTypes() + throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .data(DataObject.builder().type(500).build()).build()))); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); } @Test @@ -2399,12 +2836,14 @@ public void validateShouldReturnValidationMessageWhenMetricValueIsNotValid() { @Test public void validateShouldReturnValidationMessageWhenAdjustmentFactorNegative() { // given + final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder().build(); + givenAdjustments.addFactor("rubicon", BigDecimal.valueOf(-1.1)); final BidRequest bidRequest = validBidRequestBuilder() .ext(ExtRequest.of( ExtRequestPrebid.builder() - .bidadjustmentfactors(singletonMap("rubicon", BigDecimal.valueOf(-1.1))).build())) + .bidadjustmentfactors(givenAdjustments) + .build())) .build(); - // when final ValidationResult result = requestValidator.validate(bidRequest); @@ -2414,13 +2853,38 @@ public void validateShouldReturnValidationMessageWhenAdjustmentFactorNegative() "request.ext.prebid.bidadjustmentfactors.rubicon must be a positive number. Got -1.100000"); } + @Test + public void validateShouldReturnValidationMessageWhenAdjustmentMediaFactorNegative() { + // given + final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder() + .mediatypes(new EnumMap<>(Collections.singletonMap(BidAdjustmentMediaType.banner, + Collections.singletonMap("rubicon", BigDecimal.valueOf(-1.1))))) + .build(); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of( + ExtRequestPrebid.builder() + .bidadjustmentfactors(givenAdjustments) + .build())) + .build(); + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly( + "request.ext.prebid.bidadjustmentfactors.banner.rubicon " + + "must be a positive number. Got -1.100000"); + } + @Test public void validateShouldReturnValidationMessageWhenBidderUnknown() { // given + final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder().build(); + givenAdjustments.addFactor("unknownBidder", BigDecimal.valueOf(1.1F)); final BidRequest bidRequest = validBidRequestBuilder() .ext(ExtRequest.of( ExtRequestPrebid.builder() - .bidadjustmentfactors(singletonMap("unknownBidder", BigDecimal.valueOf(1.1F))) + .bidadjustmentfactors(givenAdjustments) .build())) .build(); @@ -2433,12 +2897,40 @@ public void validateShouldReturnValidationMessageWhenBidderUnknown() { } @Test - public void validateShouldEmptyValidationMessagesWhenBidderIsKnownAndAdjustmentIsValid() { + public void validateShouldReturnValidationMessageWhenMediaBidderUnknown() { + // given + final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder() + .mediatypes(new EnumMap<>(Collections.singletonMap(BidAdjustmentMediaType.xNative, + Collections.singletonMap("unknownBidder", BigDecimal.valueOf(1.1))))) + .build(); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of( + ExtRequestPrebid.builder() + .bidadjustmentfactors(givenAdjustments) + .build())) + .build(); + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly( + "request.ext.prebid.bidadjustmentfactors.native.unknownBidder is not a known bidder or alias"); + } + + @Test + public void validateShouldReturnEmptyValidationMessagesWhenBidderIsKnownAndAdjustmentIsValid() { // given + final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder() + .mediatypes(new EnumMap<>(Collections.singletonMap(BidAdjustmentMediaType.xNative, + Collections.singletonMap("rubicon", BigDecimal.valueOf(2.1))))) + .build(); + givenAdjustments.addFactor("rubicon", BigDecimal.valueOf(1.1)); final BidRequest bidRequest = validBidRequestBuilder() .ext(ExtRequest.of( ExtRequestPrebid.builder() - .bidadjustmentfactors(singletonMap("rubicon", BigDecimal.valueOf(1.1))).build())) + .bidadjustmentfactors(givenAdjustments) + .build())) .build(); // when @@ -2449,13 +2941,19 @@ public void validateShouldEmptyValidationMessagesWhenBidderIsKnownAndAdjustmentI } @Test - public void validateShouldEmptyValidationMessagesWhenBidderIsKnownAliasForCoreBidderAndAdjustmentIsValid() { + public void validateShouldReturnEmptyValidationMessagesWhenBidderIsKnownAliasForCoreBidderAndAdjustmentIsValid() { // given + final String rubiconAlias = "rubicon_alias"; + final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder() + .mediatypes(new EnumMap<>(Collections.singletonMap(BidAdjustmentMediaType.xNative, + Collections.singletonMap("rubicon_alias", BigDecimal.valueOf(2.1))))) + .build(); + givenAdjustments.addFactor(rubiconAlias, BigDecimal.valueOf(1.1)); final BidRequest bidRequest = validBidRequestBuilder() .ext(ExtRequest.of( ExtRequestPrebid.builder() - .aliases(singletonMap("rubicon_alias", "rubicon")) - .bidadjustmentfactors(singletonMap("rubicon_alias", BigDecimal.valueOf(1.1))) + .aliases(singletonMap(rubiconAlias, "rubicon")) + .bidadjustmentfactors(givenAdjustments) .build())) .build(); @@ -2466,6 +2964,50 @@ public void validateShouldEmptyValidationMessagesWhenBidderIsKnownAliasForCoreBi assertThat(result.getErrors()).isEmpty(); } + @Test + public void validateShouldReturnEmptyValidationMessagesWhenBidderIsKnownBidderConfigAliasAndAdjustmentIsValid() { + // given + final String rubiconAlias = "rubicon_alias"; + final ExtRequestBidadjustmentfactors givenAdjustments = ExtRequestBidadjustmentfactors.builder().build(); + givenAdjustments.addFactor(rubiconAlias, BigDecimal.valueOf(1.1)); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of( + ExtRequestPrebid.builder() + .aliases(singletonMap(rubiconAlias, "rubicon")) + .bidadjustmentfactors(givenAdjustments) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + verify(bidderCatalog).isValidName(rubiconAlias); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void validateShouldReturnValidationMessageWhenMultipleSchainsForSameBidder() { + // given + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of( + ExtRequestPrebid.builder() + .schains(asList( + ExtRequestPrebidSchain.of(asList("bidder1", "bidder2"), null), + ExtRequestPrebidSchain.of(asList("bidder2", "bidder3"), null))) + .build())) + .build(); + + // when + final ValidationResult result = requestValidator.validate(bidRequest); + + // then + assertThat(result.getErrors()) + .containsOnly("request.ext.prebid.schains contains multiple schains for bidder bidder2; " + + "it must contain no more than one per bidder."); + } + @Test public void validateShouldReturnValidationMessageWhenRequestHaveDuplicatedImpIds() { // given @@ -2521,7 +3063,7 @@ private static Imp.ImpBuilder validImpBuilder() { .format(singletonList(Format.builder().wmin(1).wratio(5).hratio(1).build())) .build()) .pmp(Pmp.builder().deals(singletonList(Deal.builder().id("1").build())).build()) - .ext(mapper.valueToTree(singletonMap("rubicon", 0))); + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))); } private static BidRequest overwriteBannerFormatInFirstImp( diff --git a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java index 65cc0086f75..ad827ff0fcd 100644 --- a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java @@ -1,104 +1,758 @@ package org.prebid.server.validation; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Pmp; import com.iab.openrtb.response.Bid; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.proto.openrtb.ext.request.ExtDeal; +import org.prebid.server.proto.openrtb.ext.request.ExtDealLine; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountBidValidationConfig; import org.prebid.server.validation.model.ValidationResult; import java.math.BigDecimal; -import java.util.function.Function; +import java.util.List; +import java.util.function.UnaryOperator; -import static java.util.function.Function.identity; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.prebid.server.settings.model.BidValidationEnforcement.enforce; +import static org.prebid.server.settings.model.BidValidationEnforcement.skip; +import static org.prebid.server.settings.model.BidValidationEnforcement.warn; -public class ResponseBidValidatorTest { +public class ResponseBidValidatorTest extends VertxTest { + + private static final String BIDDER_NAME = "bidder"; + private static final String ACCOUNT_ID = "account"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private Metrics metrics; private ResponseBidValidator responseBidValidator; + @Mock + private BidderAliases bidderAliases; + @Before public void setUp() { - responseBidValidator = new ResponseBidValidator(); + responseBidValidator = new ResponseBidValidator(enforce, enforce, metrics, jacksonMapper, true); + + given(bidderAliases.resolveBidder(anyString())).willReturn(BIDDER_NAME); } @Test - public void validateShouldFailedIfMissingBid() { - final ValidationResult result = responseBidValidator.validate(null); + public void validateShouldFailedIfBidderBidCurrencyIsIncorrect() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(BidType.banner, "invalid", identity()), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Empty bid object submitted."); + // then + assertThat(result.getErrors()).containsOnly("BidResponse currency \"invalid\" is not valid"); } @Test - public void validateShouldFailedIfBidHasNoId() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.id(null))); + public void validateShouldFailIfMissingBid() { + // when + final ValidationResult result = responseBidValidator.validate( + BidderBid.of(null, null, "USD"), BIDDER_NAME, givenAuctionContext(), bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Bid missing required field 'id'"); + // then + assertThat(result.getErrors()).containsOnly("Empty bid object submitted"); } @Test - public void validateShouldFailedIfBidHasNoImpId() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.impid(null))); + public void validateShouldFailIfBidHasNoId() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.id(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Bid \"bidId1\" missing required field 'impid'"); + // then + assertThat(result.getErrors()).containsOnly("Bid missing required field 'id'"); } @Test - public void validateShouldFailedIfBidHasNoPrice() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.price(null))); + public void validateShouldFailIfBidHasNoImpId() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.impid(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Bid \"bidId1\" does not contain a 'price'"); + // then + assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing required field 'impid'"); } @Test - public void validateShouldFailedIfBidHasNegativePrice() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.price( - BigDecimal.valueOf(-1)))); + public void validateShouldFailIfBidHasNoPrice() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.price(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Bid \"bidId1\" `price `has negative value"); + // then + assertThat(result.getErrors()).hasSize(1).containsOnly("Bid \"bidId1\" does not contain a 'price'"); + } + + @Test + public void validateShouldFailIfBidHasNegativePrice() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.price(BigDecimal.valueOf(-1))), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.getErrors()).hasSize(1).containsOnly("Bid \"bidId1\" `price `has negative value"); } @Test public void validateShouldFailedIfNonDealBidHasZeroPrice() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.price( - BigDecimal.valueOf(0)))); + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.price(BigDecimal.valueOf(0))), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Non deal bid \"bidId1\" has 0 price"); + // then + assertThat(result.getErrors()).hasSize(1).containsOnly("Non deal bid \"bidId1\" has 0 price"); } @Test public void validateShouldSuccessForDealZeroPriceBid() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.price( - BigDecimal.valueOf(0)).dealid("dealId"))); + // when + final ValidationResult result = responseBidValidator.validate( + givenVideoBid(builder -> builder.price(BigDecimal.valueOf(0)).dealid("dealId")), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + // then assertThat(result.hasErrors()).isFalse(); } @Test - public void validateShouldFailedIfBidHasNoCrid() { - final ValidationResult result = responseBidValidator.validate(givenBid(builder -> builder.crid(null))); + public void validateShouldFailIfBidHasNoCrid() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.crid(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Bid \"bidId1\" missing creative ID"); + // then + assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing creative ID"); + } + + @Test + public void validateShouldFailIfBannerBidHasNoWidthAndHeight() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.w(null).h(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("BidResponse validation `enforce`: bidder `bidder` response triggers creative size " + + "validation for bid bidId1, account=account, referrer=unknown, max imp size='100x200', bid " + + "response size='nullxnull'"); + } + + @Test + public void validateShouldFailIfBannerBidWidthIsGreaterThanImposedByImp() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.w(150).h(150)), BIDDER_NAME, givenAuctionContext(), bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("BidResponse validation `enforce`: bidder `bidder` response triggers creative size" + + " validation for bid bidId1, account=account, referrer=unknown, max imp size='100x200'," + + " bid response size='150x150'"); + } + + @Test + public void validateShouldFailIfBannerBidHeightIsGreaterThanImposedByImp() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.w(50).h(250)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("BidResponse validation `enforce`: bidder `bidder` response triggers creative size" + + " validation for bid bidId1, account=account, referrer=unknown, max imp size='100x200'," + + " bid response size='50x250'"); + } + + @Test + public void validateShouldReturnSuccessIfNonBannerBidHasAnySize() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(BidType.video, builder -> builder.w(3).h(3)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldReturnSuccessIfBannerBidHasInvalidSizeButAccountDoesNotEnforceValidation() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.w(150).h(150)), + BIDDER_NAME, + givenAuctionContext( + givenAccount(builder -> builder.auction(AccountAuctionConfig.builder() + .bidValidations(AccountBidValidationConfig.of(skip)) + .build()))), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldFailIfBidHasNoCorrespondingImp() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.impid("nonExistentsImpid")), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("Bid \"bidId1\" has no corresponding imp in request"); + } + + @Test + public void validateShouldFailIfBidHasInsecureMarkerInCreativeInSecureContext() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("BidResponse validation `enforce`: bidder `bidder` response triggers secure creative " + + "validation for bid bidId1, account=account, referrer=unknown," + + " adm=http://site.com/creative.jpg"); + } + + @Test + public void validateShouldFailIfBidHasInsecureEncodedMarkerInCreativeInSecureContext() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("http%3A//site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("BidResponse validation `enforce`: bidder `bidder` response triggers secure creative" + + " validation for bid bidId1, account=account, referrer=unknown, " + + "adm=http%3A//site.com/creative.jpg"); + } + + @Test + public void validateShouldFailIfBidHasNoSecureMarkersInCreativeInSecureContext() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("//site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("BidResponse validation `enforce`: bidder `bidder` response triggers secure creative" + + " validation for bid bidId1, account=account, referrer=unknown, " + + "adm=//site.com/creative.jpg"); + } + + @Test + public void validateShouldReturnSuccessIfBidHasInsecureCreativeInInsecureContext() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldFailedIfVideoBidHasNoNurlAndAdm() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(BidType.video, builder -> builder.adm(null).nurl(null)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.getErrors()) + .containsOnly("Bid \"bidId1\" with video type missing adm and nurl"); + verify(metrics).updateAdapterRequestErrorMetric(BIDDER_NAME, MetricName.badserverresponse); + } + + @Test + public void validateShouldReturnSuccessfulResultForValidVideoBidWithNurl() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(BidType.video, builder -> builder.adm(null)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldReturnSuccessfulResultForValidVideoBidWithAdm() { + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(BidType.video, builder -> builder.nurl(null)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); } @Test public void validateShouldReturnSuccessfulResultForValidBid() { - final ValidationResult result = responseBidValidator.validate(givenBid(identity())); + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(identity()), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldReturnSuccessIfBannerSizeValidationNotEnabled() { + // given + responseBidValidator = new ResponseBidValidator(skip, enforce, metrics, jacksonMapper, true); + + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(identity()), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldReturnSuccessWithWarningIfBannerSizeEnforcementIsWarn() { + // given + responseBidValidator = new ResponseBidValidator(warn, enforce, metrics, jacksonMapper, true); + + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.w(null).h(null)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + assertThat(result.getWarnings()) + .containsOnly("BidResponse validation `warn`: bidder `bidder` response triggers creative size " + + "validation for bid bidId1, account=account, referrer=unknown, max imp size='100x200'," + + " bid response size='nullxnull'"); + } + + @Test + public void validateShouldReturnSuccessIfSecureMarkupValidationNotEnabled() { + // given + responseBidValidator = new ResponseBidValidator(enforce, skip, metrics, jacksonMapper, true); + + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldReturnSuccessWithWarningIfSecureMarkupEnforcementIsWarn() { + // given + responseBidValidator = new ResponseBidValidator(enforce, warn, metrics, jacksonMapper, true); + + // when + final ValidationResult result = responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + // then assertThat(result.hasErrors()).isFalse(); + assertThat(result.getWarnings()) + .containsOnly("BidResponse validation `warn`: bidder `bidder` response triggers secure creative " + + "validation for bid bidId1, account=account, referrer=unknown, " + + "adm=http://site.com/creative.jpg"); } - private static Bid givenBid(Function bidCustomizer) { + @Test + public void validateShouldIncrementSizeValidationErrMetrics() { + // when + responseBidValidator.validate( + givenBid(builder -> builder.w(150).h(200)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err); + } + + @Test + public void validateShouldIncrementSizeValidationWarnMetrics() { + // given + responseBidValidator = new ResponseBidValidator(warn, warn, metrics, jacksonMapper, true); + + // when + responseBidValidator.validate( + givenBid(builder -> builder.w(150).h(200)), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + // then + verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn); + } + + @Test + public void validateShouldIncrementSecureValidationErrMetrics() { + // when + responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err); + } + + @Test + public void validateShouldIncrementSecureValidationWarnMetrics() { + // given + responseBidValidator = new ResponseBidValidator(warn, warn, metrics, jacksonMapper, true); + + // when + responseBidValidator.validate( + givenBid(builder -> builder.adm("http://site.com/creative.jpg")), + BIDDER_NAME, + givenAuctionContext(givenBidRequest(builder -> builder.secure(1))), + bidderAliases); + + // then + verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn); + } + + @Test + public void validateShouldReturnSuccessfulResultForValidNonDealBid() { + final ValidationResult result = responseBidValidator.validate( + givenVideoBid(identity()), + BIDDER_NAME, + givenAuctionContext(), + bidderAliases); + + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldFailIfBidHasNoDealid() { + final ValidationResult result = responseBidValidator.validate( + givenVideoBid(identity()), + BIDDER_NAME, + givenAuctionContext(givenRequest(identity())), + bidderAliases); + + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Bid \"bidId1\" missing required field 'dealid'"); + } + + @Test + public void validateShouldSuccessIfBidHasDealidAndImpHasNoDeals() { + final ValidationResult result = responseBidValidator.validate( + givenVideoBid(bid -> bid.dealid("dealId1")), + BIDDER_NAME, + givenAuctionContext(givenRequest(identity())), + bidderAliases); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getWarnings()).isEmpty(); + } + + @Test + public void validateShouldWarnIfBidHasDealidMissingInImp() { + given(bidderAliases.resolveBidder(eq("anotherBidder"))).willReturn("anotherBidder"); + + final ValidationResult result = responseBidValidator.validate( + givenVideoBid(bid -> bid.dealid("dealId1")), + BIDDER_NAME, + givenAuctionContext(givenRequest(imp -> imp.pmp(pmp(asList( + deal(builder -> builder + .id("dealId2") + .ext(mapper.valueToTree(ExtDeal.of( + ExtDealLine.of(null, null, null, BIDDER_NAME))))), + deal(builder -> builder + .id("dealId3") + .ext(mapper.valueToTree(ExtDeal.of( + ExtDealLine.of(null, null, null, BIDDER_NAME))))), + deal(builder -> builder + .id("dealId4") + .ext(mapper.valueToTree(ExtDeal.of( + ExtDealLine.of(null, null, null, "anotherBidder")))))))))), + bidderAliases); + + assertThat(result.getWarnings()).hasSize(1) + .containsOnly("WARNING: Bid \"bidId1\" has 'dealid' not present in corresponding imp in request." + + " 'dealid' in bid: 'dealId1', deal Ids in imp: 'dealId2,dealId3'"); + } + + @Test + public void validateShouldFailIfBidIsBannerAndImpHasNoBanner() { + responseBidValidator = new ResponseBidValidator(skip, enforce, metrics, jacksonMapper, true); + + final ValidationResult result = responseBidValidator.validate( + givenBid(bid -> bid.dealid("dealId1")), + BIDDER_NAME, + givenAuctionContext(givenRequest(imp -> imp + .pmp(pmp(singletonList(deal(builder -> builder.id("dealId1"))))))), + bidderAliases); + + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Bid \"bidId1\" has banner media type but corresponding imp in request is missing " + + "'banner' object"); + } + + @Test + public void validateShouldFailIfBidIsBannerAndSizeHasNoMatchInBannerFormats() { + final ValidationResult result = responseBidValidator.validate( + givenBid(bid -> bid.dealid("dealId1").w(300).h(400)), + BIDDER_NAME, + givenAuctionContext(givenRequest(imp -> imp + .pmp(pmp(singletonList(deal(builder -> builder.id("dealId1"))))) + .banner(Banner.builder() + .format(singletonList(Format.builder().w(400).h(500).build())) + .build()))), + bidderAliases); + + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Bid \"bidId1\" has 'w' and 'h' not supported by corresponding imp in request. Bid " + + "dimensions: '300x400', formats in imp: '400x500'"); + } + + @Test + public void validateShouldFailIfBidIsBannerAndSizeHasNoMatchInLineItem() { + final ValidationResult result = responseBidValidator.validate( + givenBid(bid -> bid.dealid("dealId1").w(300).h(400)), + BIDDER_NAME, + givenAuctionContext(givenRequest(imp -> imp + .pmp(pmp(singletonList(deal(builder -> builder + .id("dealId1") + .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of("lineItemId", null, + singletonList(Format.builder().w(500).h(600).build()), null)))))))) + .banner(Banner.builder() + .format(singletonList(Format.builder().w(300).h(400).build())) + .build()))), + bidderAliases); + + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Bid \"bidId1\" has 'w' and 'h' not matched to Line Item. Bid dimensions: '300x400', " + + "Line Item sizes: '500x600'"); + } + + @Test + public void validateShouldSuccessIfBidIsBannerAndSizeHasNoMatchInLineItemForNonPgDeal() { + final ValidationResult result = responseBidValidator.validate( + givenBid(bid -> bid.dealid("dealId1").w(300).h(400)), + BIDDER_NAME, + givenAuctionContext(givenRequest(imp -> imp + .pmp(pmp(singletonList(deal(builder -> builder + .id("dealId1") + .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of(null, null, + singletonList(Format.builder().w(500).h(600).build()), null)))))))) + .banner(Banner.builder() + .format(singletonList(Format.builder().w(300).h(400).build())) + .build()))), + bidderAliases); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getWarnings()).isEmpty(); + } + + @Test + public void validateShouldReturnSuccessfulResultForValidDealNonBannerBid() { + final ValidationResult result = responseBidValidator.validate( + givenVideoBid(bid -> bid.dealid("dealId1")), + BIDDER_NAME, + givenAuctionContext(givenRequest(imp -> imp.pmp(pmp(singletonList( + deal(builder -> builder.id("dealId1"))))))), + bidderAliases); + + assertThat(result.hasErrors()).isFalse(); + } + + @Test + public void validateShouldReturnSuccessfulResultForValidDealBannerBid() { + final ValidationResult result = responseBidValidator.validate( + givenBid(bid -> bid.dealid("dealId1").w(300).h(400)), + BIDDER_NAME, + givenAuctionContext(givenRequest(imp -> imp + .pmp(pmp(singletonList(deal(builder -> builder + .id("dealId1") + .ext(mapper.valueToTree(ExtDeal.of(ExtDealLine.of(null, null, + singletonList(Format.builder().w(300).h(400).build()), null)))))))) + .banner(Banner.builder() + .format(singletonList(Format.builder().w(300).h(400).build())) + .build()))), + bidderAliases); + + assertThat(result.hasErrors()).isFalse(); + } + + private BidRequest givenRequest(UnaryOperator impCustomizer) { + final ObjectNode ext = mapper.createObjectNode().set( + "prebid", mapper.createObjectNode().set( + "bidder", mapper.createObjectNode().set( + BIDDER_NAME, mapper.createObjectNode().put( + "dealsonly", true)))); + + final Imp.ImpBuilder impBuilder = Imp.builder() + .id("impId1") + .ext(ext); + final Imp imp = impCustomizer.apply(impBuilder).build(); + + return BidRequest.builder().imp(singletonList(imp)).build(); + } + + private static Pmp pmp(List deals) { + return Pmp.builder().deals(deals).build(); + } + + private static Deal deal(UnaryOperator dealCustomizer) { + return dealCustomizer.apply(Deal.builder()).build(); + } + + private static BidderBid givenVideoBid(UnaryOperator bidCustomizer) { + return givenBid(BidType.video, bidCustomizer); + } + + private static BidderBid givenBid(UnaryOperator bidCustomizer) { + return givenBid(BidType.banner, bidCustomizer); + } + + private static BidderBid givenBid(BidType type, UnaryOperator bidCustomizer) { + return givenBid(type, "USD", bidCustomizer); + } + + private static BidderBid givenBid(BidType type, String bidCurrency, UnaryOperator bidCustomizer) { final Bid.BidBuilder bidBuilder = Bid.builder() .id("bidId1") + .adm("adm1") + .nurl("nurl") .impid("impId1") .crid("crid1") + .w(1) + .h(1) + .adm("https://site.com/creative.jpg") .price(BigDecimal.ONE); - return bidCustomizer.apply(bidBuilder).build(); + + return BidderBid.of(bidCustomizer.apply(bidBuilder).build(), type, bidCurrency); + } + + private static AuctionContext givenAuctionContext(BidRequest bidRequest, Account account) { + return AuctionContext.builder() + .account(account) + .bidRequest(bidRequest) + .build(); + } + + private static AuctionContext givenAuctionContext(BidRequest bidRequest) { + return givenAuctionContext(bidRequest, givenAccount()); + } + + private static AuctionContext givenAuctionContext(Account account) { + return givenAuctionContext(givenBidRequest(identity()), account); + } + + private static AuctionContext givenAuctionContext() { + return givenAuctionContext(givenBidRequest(identity()), givenAccount()); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + final Imp.ImpBuilder impBuilder = Imp.builder() + .id("impId1") + .banner(Banner.builder() + .format(asList(Format.builder().w(100).h(200).build(), Format.builder().w(50).h(50).build())) + .build()) + .ext(mapper.createObjectNode().set( + "prebid", mapper.createObjectNode().set( + "bidder", mapper.createObjectNode() + .putNull(BIDDER_NAME)))); + + return BidRequest.builder() + .imp(singletonList(impCustomizer.apply(impBuilder).build())) + .build(); + } + + private static Account givenAccount() { + return givenAccount(identity()); + } + + private static Account givenAccount(UnaryOperator accountCustomizer) { + return accountCustomizer.apply(Account.builder().id(ACCOUNT_ID)).build(); } } diff --git a/src/test/java/org/prebid/server/vast/VastModifierTest.java b/src/test/java/org/prebid/server/vast/VastModifierTest.java new file mode 100644 index 00000000000..936a2122351 --- /dev/null +++ b/src/test/java/org/prebid/server/vast/VastModifierTest.java @@ -0,0 +1,371 @@ +package org.prebid.server.vast; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cache.proto.request.PutObject; +import org.prebid.server.events.EventsContext; +import org.prebid.server.events.EventsService; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +public class VastModifierTest { + + private static final String AUCTION_ID = "auctionId"; + private static final String ACCOUNT_ID = "accountId"; + private static final String BIDDER = "bidder"; + private static final String INTEGRATION = "integration"; + private static final String VAST_URL_TRACKING = "http://external-url/event"; + private static final String BID_ID = "bidId"; + private static final String BID_NURL = "nurl1"; + private static final String LINEITEM_ID = "lineItemId"; + private static final long AUCTION_TIMESTAMP = 1000L; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private BidderCatalog bidderCatalog; + @Mock + private EventsService eventsService; + @Mock + private Metrics metrics; + + private VastModifier target; + + @Before + public void setUp() { + given(eventsService.vastUrlTracking(any(), any(), any(), any(), any())) + .willReturn(VAST_URL_TRACKING); + + given(bidderCatalog.isModifyingVastXmlAllowed(any())).willReturn(true); + + target = new VastModifier(bidderCatalog, eventsService, metrics); + } + + @Test + public void modifyVastXmlShouldReturnReceivedValueWhenEventsAreNotAllowed() { + // when + final JsonNode result = target.modifyVastXml(false, singleton(BIDDER), putObject(), ACCOUNT_ID, INTEGRATION); + + // then + assertThat(result).isEqualTo(nodeAdm()); + } + + @Test + public void modifyVastXmlShouldReturnReceivedValueWhenBidderIsNotAllowed() { + // when + final JsonNode result = target.modifyVastXml(true, emptySet(), putObject(), ACCOUNT_ID, INTEGRATION); + + // then + assertThat(result).isEqualTo(nodeAdm()); + } + + @Test + public void modifyVastXmlShouldReturnReceivedValueWhenValueIsNull() { + // when + final JsonNode result = target.modifyVastXml(true, singleton(BIDDER), givenPutObject(null), ACCOUNT_ID, + INTEGRATION); + + // then + assertThat(result).isNull(); + } + + @Test + public void modifyVastXmlShouldNotModifyVastAndAppendUrlWhenValueWithoutImpression() { + // given + final TextNode vastWithoutImpression = new TextNode("vast"); + + // when + final JsonNode result = target.modifyVastXml(true, singleton(BIDDER), givenPutObject(vastWithoutImpression), + ACCOUNT_ID, INTEGRATION); + + verify(eventsService).vastUrlTracking(any(), any(), any(), any(), any()); + + assertThat(result).isEqualTo(vastWithoutImpression); + } + + @Test + public void modifyVastXmlShouldModifyVastAndAppendUrl() { + // given + final JsonNode result = target.modifyVastXml(true, singleton(BIDDER), putObject(), ACCOUNT_ID, + INTEGRATION); + + // then + final String modifiedVast = "prebid.org wrapper" + + "" + + "" + + ""; + + assertThat(result).isEqualTo(new TextNode(modifiedVast)); + } + + @Test + public void createBidVastXmlShouldNotModifyWhenBidderNotAllowed() { + // given + given(bidderCatalog.isModifyingVastXmlAllowed(any())).willReturn(false); + + // when + final String result = target + .createBidVastXml(BIDDER, adm(), BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(), + LINEITEM_ID); + + // then + assertThat(result).isEqualTo(adm()); + } + + @Test + public void createBidVastXmlShouldInjectBidNurlWhenBidAdmIsNullAndEventsDisabledByAccount() { + // when + final String result = target.createBidVastXml(BIDDER, null, BID_NURL, BID_ID, ACCOUNT_ID, + givenEventsContext(false), emptyList(), LINEITEM_ID); + + // then + assertThat(result).isEqualTo(modifiedAdm(BID_NURL)); + } + + @Test + public void createBidVastXmlShouldInjectBidNurlWhenBidAdmIsEmptyAndEventsDisabledByAccount() { + // when + final String result = target.createBidVastXml(BIDDER, "", BID_NURL, BID_ID, ACCOUNT_ID, + givenEventsContext(false), emptyList(), LINEITEM_ID); + + // then + assertThat(result).isEqualTo(modifiedAdm(BID_NURL)); + } + + @Test + public void createBidVastXmlShouldReturnAdmWhenBidAdmIsPresentAndEventsDisabledByAccount() { + // when + final String result = target.createBidVastXml(BIDDER, adm(), BID_NURL, BID_ID, ACCOUNT_ID, + givenEventsContext(false), emptyList(), LINEITEM_ID); + + // then + assertThat(result).isEqualTo(adm()); + } + + @Test + public void createBidVastXmlShouldBeModifiedWithNewImpressionVastUrlWhenEventsEnabledAndNoEmptyTag() { + // when + final String bidAdm = "http:/test.com"; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(), + LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + + assertThat(result).isEqualTo("http:/test.com" + + ""); + } + + @Test + public void createBidVastXmlShouldBeModifiedWithNewImpressionAfterExistingImpressionTags() { + // when + final String bidAdm = "http:/test.com" + + "http:/test2.com"; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(), + LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + + assertThat(result).isEqualTo("http:/test.com" + + "http:/test2.com" + + ""); + } + + @Test + public void createBidVastXmlShouldInsertImpressionTagForEmptyInLine() { + // when + final String bidAdm = ""; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(), + LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + + assertThat(result).isEqualTo(""); + } + + @Test + public void createBidVastXmlShouldNotInsertImpressionTagForNoInLineCloseTag() { + // when + final String bidAdm = ""; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(), + LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + + assertThat(result).isEqualTo(bidAdm); + } + + @Test + public void createBidVastXmlShouldModifyWrapperTagInCaseInsensitiveMode() { + // when + final String bidAdm = "http:/test.com"; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(), + LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + + assertThat(result).isEqualTo("http:/test.com" + + ""); + } + + @Test + public void createBidVastXmlShouldInsertImpressionTagForEmptyWrapper() { + // when + final String result = target + .createBidVastXml(BIDDER, "", BID_NURL, + BID_ID, ACCOUNT_ID, eventsContext(), emptyList(), + LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + + assertThat(result) + .isEqualTo(""); + } + + @Test + public void createBidVastXmlShouldNotInsertImpressionTagForNoWrapperCloseTag() { + // when + final String bidAdm = ""; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(), + LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + + assertThat(result).isEqualTo(bidAdm); + } + + @Test + public void createBidVastXmlShouldModifyInlineTagInCaseInsensitiveMode() { + // when + final String bidAdm = "http:/test.com"; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(), + LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + + assertThat(result).isEqualTo("http:/test.com" + + ""); + } + + @Test + public void createBidVastXmlShouldBeModifiedIfInLineHasNoImpressionTags() { + // when + final String bidAdm = ""; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList(), + LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + + assertThat(result).isEqualTo(""); + } + + @Test + public void createBidVastXmlShouldNotBeModifiedIfNoParentTagsPresent() { + // when + final String adm = "http:/test.com"; + final List warnings = new ArrayList<>(); + final String result = target + .createBidVastXml(BIDDER, adm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), warnings, LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + assertThat(result).isEqualTo(adm); + assertThat(warnings).containsExactly("VastXml does not contain neither InLine nor Wrapper for bidder response"); + verify(metrics).updateAdapterRequestErrorMetric(BIDDER, MetricName.badserverresponse); + } + + @Test + public void createBidVastXmlShouldNotModifyWhenEventsEnabledAndAdmHaveNoImpression() { + // when + final String admWithNoImpression = "no impression"; + final String result = target.createBidVastXml(BIDDER, admWithNoImpression, BID_NURL, BID_ID, + ACCOUNT_ID, eventsContext(), new ArrayList<>(), LINEITEM_ID); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, LINEITEM_ID, eventsContext()); + + assertThat(result).isEqualTo(admWithNoImpression); + } + + private static PutObject givenPutObject(TextNode adm) { + return PutObject.builder() + .type("xml") + .bidid("bidId2") + .bidder(BIDDER) + .timestamp(1L) + .value(adm) + .build(); + } + + private static PutObject putObject() { + return givenPutObject(nodeAdm()); + } + + private static TextNode nodeAdm() { + return new TextNode(adm()); + } + + public static String adm() { + return "" + + "prebid.org wrapper" + + "" + + "" + + ""; + } + + private static String modifiedAdm(String bidNurl) { + return "" + + "prebid.org wrapper" + + "" + + "" + + ""; + } + + private static EventsContext givenEventsContext(boolean accountEnabled) { + return EventsContext.builder() + .enabledForAccount(accountEnabled) + .auctionId(AUCTION_ID) + .auctionTimestamp(AUCTION_TIMESTAMP) + .integration(INTEGRATION) + .build(); + } + + private static EventsContext eventsContext() { + return givenEventsContext(true); + } +} diff --git a/src/test/java/org/prebid/server/vertx/http/BasicHttpClientTest.java b/src/test/java/org/prebid/server/vertx/http/BasicHttpClientTest.java index b1ffe7f926e..b62d5b21b45 100644 --- a/src/test/java/org/prebid/server/vertx/http/BasicHttpClientTest.java +++ b/src/test/java/org/prebid/server/vertx/http/BasicHttpClientTest.java @@ -95,7 +95,7 @@ public void requestShouldSucceedIfHttpRequestSucceeds() { .willAnswer(withSelfAndPassObjectToHandler(Buffer.buffer("response"))); // when - final Future future = httpClient.request(HttpMethod.GET, null, null, null, 1L); + final Future future = httpClient.request(HttpMethod.GET, null, null, (String) null, 1L); // then assertThat(future.succeeded()).isTrue(); @@ -104,7 +104,7 @@ public void requestShouldSucceedIfHttpRequestSucceeds() { @Test public void requestShouldAllowFollowingRedirections() { // when - httpClient.request(HttpMethod.GET, null, null, null, 1L); + httpClient.request(HttpMethod.GET, null, null, (String) null, 1L); // then verify(httpClientRequest).setFollowRedirects(true); @@ -117,7 +117,7 @@ public void requestShouldFailIfHttpRequestFails() { .willAnswer(withSelfAndPassObjectToHandler(new RuntimeException("Request exception"))); // when - final Future future = httpClient.request(HttpMethod.GET, null, null, null, 1L); + final Future future = httpClient.request(HttpMethod.GET, null, null, (String) null, 1L); // then assertThat(future.failed()).isTrue(); @@ -134,7 +134,7 @@ public void requestShouldFailIfHttpResponseFails() { .willAnswer(withSelfAndPassObjectToHandler(new RuntimeException("Response exception"))); // when - final Future future = httpClient.request(HttpMethod.GET, null, null, null, 1L); + final Future future = httpClient.request(HttpMethod.GET, null, null, (String) null, 1L); // then assertThat(future.failed()).isTrue(); diff --git a/src/test/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClientTest.java b/src/test/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClientTest.java index e9647fdfc32..e996a6c77fe 100644 --- a/src/test/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClientTest.java +++ b/src/test/java/org/prebid/server/vertx/http/CircuitBreakerSecuredHttpClientTest.java @@ -11,6 +11,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.BDDMockito; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; @@ -22,14 +23,16 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.util.function.BooleanSupplier; +import java.util.function.LongSupplier; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -62,15 +65,15 @@ public void tearDown(TestContext context) { } @Test - public void requestShouldFailsOnInvalidUrl() { + public void requestShouldFailOnInvalidUrl() { // when and then - assertThatThrownBy(() -> httpClient.request(HttpMethod.GET, "invalid_url", null, null, 0L)) + assertThatThrownBy(() -> httpClient.request(HttpMethod.GET, "invalid_url", null, (String) null, 0L)) .isInstanceOf(PreBidException.class) .hasMessage("Invalid url: invalid_url"); } @Test - public void requestShouldSucceedsIfCircuitIsClosedAndWrappedHttpClientSucceeds(TestContext context) { + public void requestShouldSucceedIfCircuitIsClosedAndWrappedHttpClientSucceeds(TestContext context) { // given givenHttpClientReturning(HttpClientResponse.of(200, null, null)); @@ -78,13 +81,13 @@ public void requestShouldSucceedsIfCircuitIsClosedAndWrappedHttpClientSucceeds(T final Future future = doRequest(context); // then - verify(wrappedHttpClient).request(any(), anyString(), any(), any(), anyLong()); + verify(wrappedHttpClient).request(any(), anyString(), any(), (String) any(), anyLong()); assertThat(future.succeeded()).isTrue(); } @Test - public void requestShouldFailsIfCircuitIsClosedButWrappedHttpClientFails(TestContext context) { + public void requestShouldFailIfCircuitIsClosedButWrappedHttpClientFails(TestContext context) { // given givenHttpClientReturning(new RuntimeException("exception")); @@ -92,14 +95,14 @@ public void requestShouldFailsIfCircuitIsClosedButWrappedHttpClientFails(TestCon final Future future = doRequest(context); // then - verify(wrappedHttpClient).request(any(), anyString(), any(), any(), anyLong()); + verify(wrappedHttpClient).request(any(), anyString(), any(), (String) any(), anyLong()); assertThat(future.failed()).isTrue(); assertThat(future.cause()).isInstanceOf(RuntimeException.class).hasMessage("exception"); } @Test - public void requestShouldFailsIfCircuitIsHalfOpenedButWrappedHttpClientFailsAndClosingTimeIsNotPassedBy( + public void requestShouldFailIfCircuitIsHalfOpenedButWrappedHttpClientFailsAndClosingTimeIsNotPassedBy( TestContext context) { // given givenHttpClientReturning(new RuntimeException("exception")); @@ -109,7 +112,8 @@ public void requestShouldFailsIfCircuitIsHalfOpenedButWrappedHttpClientFailsAndC final Future future2 = doRequest(context); // 2 call // then - verify(wrappedHttpClient).request(any(), anyString(), any(), any(), anyLong()); // invoked only on 1 call + // invoked only on 1 call + verify(wrappedHttpClient).request(any(), anyString(), any(), (String) any(), anyLong()); assertThat(future1.failed()).isTrue(); assertThat(future1.cause()).isInstanceOf(RuntimeException.class).hasMessage("exception"); @@ -119,7 +123,7 @@ public void requestShouldFailsIfCircuitIsHalfOpenedButWrappedHttpClientFailsAndC } @Test - public void requestShouldFailsIfCircuitIsHalfOpenedButWrappedHttpClientFails(TestContext context) { + public void requestShouldFailIfCircuitIsHalfOpenedButWrappedHttpClientFails(TestContext context) { // given givenHttpClientReturning(new RuntimeException("exception")); @@ -131,7 +135,7 @@ public void requestShouldFailsIfCircuitIsHalfOpenedButWrappedHttpClientFails(Tes // then verify(wrappedHttpClient, times(2)) - .request(any(), anyString(), any(), any(), anyLong()); // invoked only on 1 & 3 calls + .request(any(), anyString(), any(), (String) any(), anyLong()); // invoked only on 1 & 3 calls assertThat(future1.failed()).isTrue(); assertThat(future1.cause()).isInstanceOf(RuntimeException.class).hasMessage("exception"); @@ -144,7 +148,7 @@ public void requestShouldFailsIfCircuitIsHalfOpenedButWrappedHttpClientFails(Tes } @Test - public void requestShouldSucceedsIfCircuitIsHalfOpenedAndWrappedHttpClientSucceeds(TestContext context) { + public void requestShouldSucceedIfCircuitIsHalfOpenedAndWrappedHttpClientSucceeds(TestContext context) { // given givenHttpClientReturning(new RuntimeException("exception"), HttpClientResponse.of(200, null, null)); @@ -156,7 +160,7 @@ public void requestShouldSucceedsIfCircuitIsHalfOpenedAndWrappedHttpClientSuccee // then verify(wrappedHttpClient, times(2)) - .request(any(), anyString(), any(), any(), anyLong()); // invoked only on 1 & 3 calls + .request(any(), anyString(), any(), (String) any(), anyLong()); // invoked only on 1 & 3 calls assertThat(future1.failed()).isTrue(); assertThat(future1.cause()).isInstanceOf(RuntimeException.class).hasMessage("exception"); @@ -168,7 +172,7 @@ public void requestShouldSucceedsIfCircuitIsHalfOpenedAndWrappedHttpClientSuccee } @Test - public void requestShouldFailsWithOriginalExceptionIfOpeningIntervalExceeds(TestContext context) { + public void requestShouldFailWithOriginalExceptionIfOpeningIntervalExceeds(TestContext context) { // given httpClient = new CircuitBreakerSecuredHttpClient(vertx, wrappedHttpClient, metrics, 2, 100L, 200L, clock); @@ -181,7 +185,7 @@ public void requestShouldFailsWithOriginalExceptionIfOpeningIntervalExceeds(Test // then verify(wrappedHttpClient, times(2)) - .request(any(), anyString(), any(), any(), anyLong()); // invoked on 1 & 2 calls + .request(any(), anyString(), any(), (String) any(), anyLong()); // invoked on 1 & 2 calls assertThat(future1.failed()).isTrue(); assertThat(future1.cause()).isInstanceOf(RuntimeException.class).hasMessage("exception1"); @@ -191,36 +195,61 @@ public void requestShouldFailsWithOriginalExceptionIfOpeningIntervalExceeds(Test } @Test - public void requestShouldReportMetricsOnCircuitOpened(TestContext context) { + public void circuitBreakerNumberGaugeShouldReportActualNumber(TestContext context) { + // when + doRequest(context); + + // then + final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(LongSupplier.class); + verify(metrics).createHttpClientCircuitBreakerNumberGauge(gaugeValueProviderCaptor.capture()); + final LongSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue(); + + assertThat(gaugeValueProvider.getAsLong()).isEqualTo(1); + } + + @Test + public void circuitBreakerGaugeShouldReportOpenedWhenCircuitOpen(TestContext context) { // given givenHttpClientReturning(new RuntimeException("exception")); // when - doRequest("http://www.some-host-1.com/path", context); + doRequest(context); // then - verify(metrics).updateHttpClientCircuitBreakerMetric(eq("www_some_host_1_com"), eq(true)); + final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class); + verify(metrics).createHttpClientCircuitBreakerGauge( + eq("http_url"), + gaugeValueProviderCaptor.capture()); + final BooleanSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue(); + + assertThat(gaugeValueProvider.getAsBoolean()).isTrue(); } @Test - public void requestShouldReportMetricsOnCircuitClosed(TestContext context) { + public void circuitBreakerGaugeShouldReportClosedWhenCircuitClosed(TestContext context) { // given givenHttpClientReturning(new RuntimeException("exception"), HttpClientResponse.of(200, null, null)); // when - doRequest("http://www.some-host-1.com/path", context); // 1 call - doRequest("http://www.some-host-1.com/path", context); // 2 call + doRequest(context); // 1 call + doRequest(context); // 2 call doWaitForClosingInterval(context); - doRequest("http://www.some-host-1.com/path", context); // 3 call + doRequest(context); // 3 call // then - verify(metrics).updateHttpClientCircuitBreakerMetric(eq("www_some_host_1_com"), eq(false)); + final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class); + verify(metrics).createHttpClientCircuitBreakerGauge( + eq("http_url"), + gaugeValueProviderCaptor.capture()); + final BooleanSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue(); + + assertThat(gaugeValueProvider.getAsBoolean()).isFalse(); } @SuppressWarnings("unchecked") private void givenHttpClientReturning(T... results) { BDDMockito.BDDMyOngoingStubbing> stubbing = - given(wrappedHttpClient.request(any(), anyString(), any(), any(), anyLong())); + given(wrappedHttpClient.request(any(), anyString(), any(), (String) any(), anyLong())); for (T result : results) { if (result instanceof Exception) { stubbing = stubbing.willReturn(Future.failedFuture((Throwable) result)); @@ -230,8 +259,9 @@ private void givenHttpClientReturning(T... results) { } } - private Future doRequest(String url, TestContext context) { - final Future future = httpClient.request(HttpMethod.GET, url, null, null, 0L); + private Future doRequest(TestContext context) { + final Future future = httpClient.request(HttpMethod.GET, "http://url", null, (String) null, + 0L); final Async async = context.async(); future.setHandler(ar -> async.complete()); @@ -240,10 +270,6 @@ private Future doRequest(String url, TestContext context) { return future; } - private Future doRequest(TestContext context) { - return doRequest("http://url", context); - } - private void doWaitForOpeningInterval(TestContext context) { doWait(context, 150L); } diff --git a/src/test/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClientTest.java b/src/test/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClientTest.java index 06facb59272..898ad59aff4 100644 --- a/src/test/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClientTest.java +++ b/src/test/java/org/prebid/server/vertx/jdbc/CircuitBreakerSecuredJdbcClientTest.java @@ -10,6 +10,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.BDDMockito; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; @@ -22,6 +23,7 @@ import java.time.Instant; import java.time.ZoneId; import java.util.List; +import java.util.function.BooleanSupplier; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; @@ -30,7 +32,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -193,7 +194,7 @@ public void executeQueryShouldFailsWithOriginalExceptionIfOpeningIntervalExceeds } @Test - public void executeQueryShouldReportMetricsOnCircuitOpened(TestContext context) { + public void circuitBreakerGaugeShouldReportOpenedWhenCircuitOpen(TestContext context) { // given givenExecuteQueryReturning(singletonList( Future.failedFuture(new RuntimeException("exception1")))); @@ -202,12 +203,16 @@ public void executeQueryShouldReportMetricsOnCircuitOpened(TestContext context) final Future future = jdbcClient.executeQuery("query", emptyList(), identity(), timeout); // then + final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class); + verify(metrics).createDatabaseCircuitBreakerGauge(gaugeValueProviderCaptor.capture()); + final BooleanSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue(); + future.setHandler(context.asyncAssertFailure(throwable -> - verify(metrics).updateDatabaseCircuitBreakerMetric(eq(true)))); + assertThat(gaugeValueProvider.getAsBoolean()).isTrue())); } @Test - public void executeQueryShouldReportMetricsOnCircuitClosed(TestContext context) { + public void circuitBreakerGaugeShouldReportClosedWhenCircuitClosed(TestContext context) { // given givenExecuteQueryReturning(asList( Future.failedFuture(new RuntimeException("exception1")), @@ -224,8 +229,12 @@ public void executeQueryShouldReportMetricsOnCircuitClosed(TestContext context) resultSet -> resultSet.getResults().get(0).getString(0), timeout); // 3 call // then - future.setHandler(context.asyncAssertSuccess(result -> - verify(metrics).updateDatabaseCircuitBreakerMetric(eq(false)))); + final ArgumentCaptor gaugeValueProviderCaptor = ArgumentCaptor.forClass(BooleanSupplier.class); + verify(metrics).createDatabaseCircuitBreakerGauge(gaugeValueProviderCaptor.capture()); + final BooleanSupplier gaugeValueProvider = gaugeValueProviderCaptor.getValue(); + + future.setHandler(context.asyncAssertSuccess(throwable -> + assertThat(gaugeValueProvider.getAsBoolean()).isFalse())); } @SuppressWarnings("unchecked") diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 1e5164187c0..a6a4bba85d5 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -7,6 +7,6 @@ - + \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-device-targeting.json b/src/test/resources/org/prebid/server/deals/targeting/test-device-targeting.json new file mode 100644 index 00000000000..b341d639d54 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-device-targeting.json @@ -0,0 +1,20 @@ +{ + "$and": [ + { + "device.ext.deviceatlas.browser": { + "$in": [ + "Chrome", + "Firefox" + ] + } + }, + { + "device.geo.ext.netacuity.country": { + "$in": [ + "us", + "jp" + ] + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-and-with-non-array.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-and-with-non-array.json new file mode 100644 index 00000000000..4459a762640 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-and-with-non-array.json @@ -0,0 +1,3 @@ +{ + "$and": {} +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-function.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-function.json new file mode 100644 index 00000000000..8315dec8997 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-function.json @@ -0,0 +1,5 @@ +{ + "adunit.size": { + "$in": [] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-geo-function.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-geo-function.json new file mode 100644 index 00000000000..07ac026bd2b --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-geo-function.json @@ -0,0 +1,5 @@ +{ + "geo.distance": { + "$intersects": [] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-segment-function.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-segment-function.json new file mode 100644 index 00000000000..ea2a189602f --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-segment-function.json @@ -0,0 +1,5 @@ +{ + "segment.bluekai": { + "$in": [] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-string-function.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-string-function.json new file mode 100644 index 00000000000..b451647996b --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-string-function.json @@ -0,0 +1,5 @@ +{ + "adunit.adslot": { + "$intersects": [] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-typed-function.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-typed-function.json new file mode 100644 index 00000000000..eddac02192f --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-incompatible-typed-function.json @@ -0,0 +1,5 @@ +{ + "bidp.rubicon.siteId": { + "$within": [] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-non-object.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-non-object.json new file mode 100644 index 00000000000..f67d1e4a258 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-category-non-object.json @@ -0,0 +1,3 @@ +{ + "adunit.size": 123 +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-in-integers-non-integer.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-in-integers-non-integer.json new file mode 100644 index 00000000000..19fd40b0906 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-in-integers-non-integer.json @@ -0,0 +1,7 @@ +{ + "user.ext.time.userhour": { + "$in": [ + "abc" + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-non-array.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-non-array.json new file mode 100644 index 00000000000..85710c883e9 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-non-array.json @@ -0,0 +1,5 @@ +{ + "adunit.size": { + "$intersects": {} + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-sizes-empty-size.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-sizes-empty-size.json new file mode 100644 index 00000000000..e3c0b26ba08 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-sizes-empty-size.json @@ -0,0 +1,8 @@ +{ + "adunit.size": { + "$intersects": [ + { + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-sizes-non-objects.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-sizes-non-objects.json new file mode 100644 index 00000000000..91b6c932bde --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-sizes-non-objects.json @@ -0,0 +1,8 @@ +{ + "adunit.size": { + "$intersects": [ + 123, + 234 + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-sizes-non-readable-size.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-sizes-non-readable-size.json new file mode 100644 index 00000000000..57ef2fce627 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-sizes-non-readable-size.json @@ -0,0 +1,9 @@ +{ + "adunit.size": { + "$intersects": [ + { + "w": {} + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-strings-empty.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-strings-empty.json new file mode 100644 index 00000000000..5a1cd32db73 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-strings-empty.json @@ -0,0 +1,7 @@ +{ + "adunit.mediatype": { + "$intersects": [ + "" + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-strings-non-string.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-strings-non-string.json new file mode 100644 index 00000000000..974de4e4c1a --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-intersects-strings-non-string.json @@ -0,0 +1,7 @@ +{ + "adunit.mediatype": { + "$intersects": [ + 123 + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-matches-empty.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-matches-empty.json new file mode 100644 index 00000000000..413e4c7b71f --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-matches-empty.json @@ -0,0 +1,5 @@ +{ + "adunit.adslot": { + "$matches": "" + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-matches-non-string.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-matches-non-string.json new file mode 100644 index 00000000000..3f2439279d9 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-matches-non-string.json @@ -0,0 +1,5 @@ +{ + "adunit.adslot": { + "$matches": 123 + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-multiple-fields-boolean-args.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-multiple-fields-boolean-args.json new file mode 100644 index 00000000000..d544404457e --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-multiple-fields-boolean-args.json @@ -0,0 +1,8 @@ +{ + "$and": [ + { + "aaa": {}, + "bbb": {} + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-multiple-fields-function.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-multiple-fields-function.json new file mode 100644 index 00000000000..f4d51caf96b --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-multiple-fields-function.json @@ -0,0 +1,6 @@ +{ + "adunit.size": { + "aaa": {}, + "bbb": {} + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-multiple-fields.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-multiple-fields.json new file mode 100644 index 00000000000..186dd09685f --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-multiple-fields.json @@ -0,0 +1,4 @@ +{ + "aaa": {}, + "bbb": {} +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-non-object.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-non-object.json new file mode 100644 index 00000000000..5862557ee7f --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-non-object.json @@ -0,0 +1,3 @@ +{ + "$and": 123 +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-not-with-non-object.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-not-with-non-object.json new file mode 100644 index 00000000000..6741ef4d8ef --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-not-with-non-object.json @@ -0,0 +1,3 @@ +{ + "$not": [] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-typed-function-incompatible-type.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-typed-function-incompatible-type.json new file mode 100644 index 00000000000..edafbc6a05b --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-typed-function-incompatible-type.json @@ -0,0 +1,7 @@ +{ + "bidp.rubicon.siteId": { + "$in": [ + false + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-typed-function-mixed-types.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-typed-function-mixed-types.json new file mode 100644 index 00000000000..40b94d6ec36 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-typed-function-mixed-types.json @@ -0,0 +1,8 @@ +{ + "bidp.rubicon.siteId": { + "$in": [ + 123, + "321" + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-field.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-field.json new file mode 100644 index 00000000000..81cc228d55f --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-field.json @@ -0,0 +1,3 @@ +{ + "aaa": {} +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-function.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-function.json new file mode 100644 index 00000000000..ccea774510a --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-function.json @@ -0,0 +1,5 @@ +{ + "adunit.size": { + "$abc": [] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-string-function.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-string-function.json new file mode 100644 index 00000000000..d83962f0989 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-string-function.json @@ -0,0 +1,5 @@ +{ + "adunit.adslot": { + "$abc": [] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-typed-function.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-typed-function.json new file mode 100644 index 00000000000..0cc0c3a781b --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-unknown-typed-function.json @@ -0,0 +1,5 @@ +{ + "bidp.rubicon.siteId": { + "$abc": [] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-within-empty-georegion.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-within-empty-georegion.json new file mode 100644 index 00000000000..22a638b0b53 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-within-empty-georegion.json @@ -0,0 +1,6 @@ +{ + "geo.distance": { + "$within": { + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-within-non-object.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-within-non-object.json new file mode 100644 index 00000000000..d10af646564 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-within-non-object.json @@ -0,0 +1,5 @@ +{ + "geo.distance": { + "$within": [] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-within-non-readable-georegion.json b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-within-non-readable-georegion.json new file mode 100644 index 00000000000..b21c6a2523d --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-invalid-targeting-definition-within-non-readable-georegion.json @@ -0,0 +1,7 @@ +{ + "geo.distance": { + "$within": { + "lat": {} + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-not-and-definition.json b/src/test/resources/org/prebid/server/deals/targeting/test-not-and-definition.json new file mode 100644 index 00000000000..ddb8501fd06 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-not-and-definition.json @@ -0,0 +1,28 @@ +{ + "$not": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 450, + "h": 250 + }, + { + "w": 400, + "h": 200 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner", + "video" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-not-in-integers-definition.json b/src/test/resources/org/prebid/server/deals/targeting/test-not-in-integers-definition.json new file mode 100644 index 00000000000..27284bdc6d3 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-not-in-integers-definition.json @@ -0,0 +1,10 @@ +{ + "$not": { + "bidp.rubicon.siteId": { + "$in": [ + 785, + 778 + ] + } + } +} diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-not-in-strings-definition.json b/src/test/resources/org/prebid/server/deals/targeting/test-not-in-strings-definition.json new file mode 100644 index 00000000000..011d00d81c1 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-not-in-strings-definition.json @@ -0,0 +1,10 @@ +{ + "$not": { + "site.domain": { + "$in": [ + "nba.com", + "cnn.com" + ] + } + } +} diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-not-intersects-integer-definition.json b/src/test/resources/org/prebid/server/deals/targeting/test-not-intersects-integer-definition.json new file mode 100644 index 00000000000..08c0b5c166c --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-not-intersects-integer-definition.json @@ -0,0 +1,10 @@ +{ + "$not": { + "ufpd.data.someId": { + "$intersects": [ + 123, + 321 + ] + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-not-intersects-sizes-definition.json b/src/test/resources/org/prebid/server/deals/targeting/test-not-intersects-sizes-definition.json new file mode 100644 index 00000000000..50f0f524a4b --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-not-intersects-sizes-definition.json @@ -0,0 +1,16 @@ +{ + "$not": { + "adunit.size": { + "$intersects": [ + { + "w": 450, + "h": 250 + }, + { + "w": 400, + "h": 200 + } + ] + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-not-matches-definition.json b/src/test/resources/org/prebid/server/deals/targeting/test-not-matches-definition.json new file mode 100644 index 00000000000..4f3d9a69c3f --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-not-matches-definition.json @@ -0,0 +1,7 @@ +{ + "$not": { + "site.domain": { + "$matches": "*nba.com*" + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-not-or-definition.json b/src/test/resources/org/prebid/server/deals/targeting/test-not-or-definition.json new file mode 100644 index 00000000000..b8da565c351 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-not-or-definition.json @@ -0,0 +1,28 @@ +{ + "$not": { + "$or": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 450, + "h": 250 + }, + { + "w": 400, + "h": 200 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner", + "video" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-not-within-definition.json b/src/test/resources/org/prebid/server/deals/targeting/test-not-within-definition.json new file mode 100644 index 00000000000..98a0670f5fa --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-not-within-definition.json @@ -0,0 +1,11 @@ +{ + "$not": { + "geo.distance": { + "$within": { + "lat": 2.424744, + "lon": 3.506435, + "radiusMiles": 10 + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/deals/targeting/test-valid-targeting-definition.json b/src/test/resources/org/prebid/server/deals/targeting/test-valid-targeting-definition.json new file mode 100644 index 00000000000..cd2acc99523 --- /dev/null +++ b/src/test/resources/org/prebid/server/deals/targeting/test-valid-targeting-definition.json @@ -0,0 +1,354 @@ +{ + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 250 + }, + { + "w": 400, + "h": 200 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner", + "video" + ] + } + }, + { + "$or": [ + { + "site.domain": { + "$matches": "*nba.com*" + } + }, + { + "site.domain": { + "$matches": "nba.com*" + } + }, + { + "site.domain": { + "$in": [ + "nba.com", + "cnn.com" + ] + } + } + ] + }, + { + "$or": [ + { + "site.referrer": { + "$matches": "*sports*" + } + }, + { + "site.referrer": { + "$matches": "http://nba.com/lalakers*" + } + }, + { + "site.referrer": { + "$in": [ + "http://cnn.com/culture", + "http://cnn.com/weather" + ] + } + } + ] + }, + { + "$or": [ + { + "app.bundle": { + "$matches": "*com.google.calendar*" + } + }, + { + "app.bundle": { + "$matches": "com.google.calendar*" + } + }, + { + "app.bundle": { + "$in": [ + "com.google.calendar", + "com.tmz" + ] + } + } + ] + }, + { + "$or": [ + { + "adunit.adslot": { + "$matches": "*/home/top*" + } + }, + { + "adunit.adslot": { + "$matches": "/home/top*" + } + }, + { + "adunit.adslot": { + "$in": [ + "/home/top", + "/home/bottom" + ] + } + } + ] + }, + { + "device.geo.ext.vendor.attribute": { + "$in": [ + "device_geo_ext_value1", + "device_geo_ext_value2" + ] + } + }, + { + "device.geo.ext.vendor.nested.attribute": { + "$in": [ + "device_geo_ext_nested_value1", + "device_geo_ext_nested_value2" + ] + } + }, + { + "device.ext.vendor.attribute": { + "$in": [ + "device_ext_value1", + "device_ext_value2" + ] + } + }, + { + "device.ext.vendor.nested.attribute": { + "$in": [ + "device_ext_nested_value1", + "device_ext_nested_value2" + ] + } + }, + { + "pos": { + "$in": [ + 1, + 3 + ] + } + }, + { + "geo.distance": { + "$within": { + "lat": 123.456, + "lon": 789.123, + "radiusMiles": 10 + } + } + }, + { + "$or": [ + { + "bidp.rubicon.siteId": { + "$in": [ + 123, + 321 + ] + } + }, + { + "bidp.rubicon.siteId": { + "$intersects": [ + 123, + 321 + ] + } + } + ] + }, + { + "$or": [ + { + "bidp.appnexus.placementName": { + "$matches": "*somePlacement*" + } + }, + { + "bidp.appnexus.placementName": { + "$matches": "somePlacement*" + } + }, + { + "bidp.appnexus.placementName": { + "$in": [ + "somePlacement1", + "somePlacement2" + ] + } + }, + { + "bidp.appnexus.placementName": { + "$intersects": [ + "somePlacement1", + "somePlacement2" + ] + } + } + ] + }, + { + "$or": [ + { + "segment.rubicon": { + "$intersects": [ + "123", + "234", + "345" + ] + } + }, + { + "segment.bluekai": { + "$intersects": [ + "123", + "234", + "345" + ] + } + } + ] + }, + { + "$or": [ + { + "ufpd.someId": { + "$in": [ + 123, + 321 + ] + } + }, + { + "ufpd.someId": { + "$intersects": [ + 123, + 321 + ] + } + } + ] + }, + { + "$or": [ + { + "ufpd.sport": { + "$matches": "*hockey*" + } + }, + { + "ufpd.sport": { + "$matches": "hockey*" + } + }, + { + "ufpd.sport": { + "$in": [ + "hockey", + "soccer" + ] + } + }, + { + "ufpd.sport": { + "$intersects": [ + "hockey", + "soccer" + ] + } + } + ] + }, + { + "$or": [ + { + "sfpd.someId": { + "$in": [ + 123, + 321 + ] + } + }, + { + "sfpd.someId": { + "$intersects": [ + 123, + 321 + ] + } + } + ] + }, + { + "$or": [ + { + "sfpd.sport": { + "$matches": "*hockey*" + } + }, + { + "sfpd.sport": { + "$matches": "hockey*" + } + }, + { + "sfpd.sport": { + "$in": [ + "hockey", + "soccer" + ] + } + }, + { + "sfpd.sport": { + "$intersects": [ + "hockey", + "soccer" + ] + } + } + ] + }, + { + "user.ext.time.userdow": { + "$in": [ + 5, + 6 + ] + } + }, + { + "user.ext.time.userhour": { + "$in": [ + 10, + 11, + 12, + 13, + 14 + ] + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/amp/test-amp-response.json b/src/test/resources/org/prebid/server/it/amp/test-amp-response.json index dad9a1cffc6..734e6c6e282 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-amp-response.json +++ b/src/test/resources/org/prebid/server/it/amp/test-amp-response.json @@ -13,12 +13,12 @@ "hb_bidder_rubicon": "rubicon", "hb_size_appnexus": "300x250", "rpfl_1001": "2_tier0100", - "hb_cache_host": "localhost:8090", - "hb_cache_path": "/cache", - "hb_cache_host_rubicon": "localhost:8090", - "hb_cache_path_rubicon": "/cache", - "hb_cache_host_appnexus": "localhost:8090", - "hb_cache_path_appnexus": "/cache", + "hb_cache_host": "{{ cache.host }}", + "hb_cache_path": "{{ cache.path }}", + "hb_cache_host_rubicon": "{{ cache.host }}", + "hb_cache_path_rubicon": "{{ cache.path }}", + "hb_cache_host_appnexus": "{{ cache.host }}", + "hb_cache_path_appnexus": "{{ cache.path }}", "static_keyword1": "static_value1" } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/amp/test-appnexus-bid-request.json b/src/test/resources/org/prebid/server/it/amp/test-appnexus-bid-request.json index c60367a8448..9ed6f515e51 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-appnexus-bid-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-appnexus-bid-request.json @@ -26,10 +26,11 @@ } ], "site": { - "domain": "example.com", + "domain": "google.com", "page": "https://google.com", "publisher": { - "id": "accountId" + "id": "accountId", + "domain": "google.com" }, "ext": { "amp": 1 @@ -39,6 +40,13 @@ "ua": "userAgent", "ip": "193.168.244.1" }, + "user": { + "ext": { + "ConsentedProvidersSettings": { + "consented_providers": "someConsent" + } + } + }, "at": 1, "tmax": 5000, "cur": [ @@ -83,17 +91,24 @@ "includewinners": true, "includebidderkeys": true }, + "storedrequest": { + "id": "test-amp-stored-request" + }, "cache": { "bids": {} }, - "auctiontimestamp": 1000, + "auctiontimestamp": 0, "amp": { "data": { "curl": "https%3A%2F%2Fgoogle.com", + "consent_type": "3", "consent_string": "1YNN", + "gdpr_applies": "false", + "attl_consent": "someConsent", "ow": "980", "oh": "120", "tag_id": "test-amp-stored-request", + "targeting": "%7B%22gam-key1%22%3A%22val1%22%2C%22gam-key2%22%3A%22val2%22%7D", "slot": "overwrite-tagId", "account": "accountId", "timeout": "10000000" @@ -108,7 +123,10 @@ ], "channel": { "name": "amp" + }, + "pbs": { + "endpoint": "/openrtb2/amp" } } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/amp/test-cache-request.json b/src/test/resources/org/prebid/server/it/amp/test-cache-request.json index 7c28d6d1435..a3e1f49885c 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-cache-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-cache-request.json @@ -11,6 +11,9 @@ "w": 300, "h": 600, "ext": { + "prebid": { + "type": "banner" + }, "rp": { "targeting": [ { @@ -20,9 +23,11 @@ ] } ] - } + }, + "origbidcpm": 12.09 } - } + }, + "aid":"tid" }, { "type": "json", @@ -41,15 +46,21 @@ "w": 300, "h": 250, "ext": { + "prebid": { + "type": "banner" + }, "appnexus": { "brand_id": 1, "auction_id": 8189378542222915032, "bidder_id": 2, "bid_ad_type": 0, "ranking_price": 0.0 - } + }, + "origbidcpm": 5.5, + "origbidcur": "USD" } - } + }, + "aid":"tid" } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/amp/test-rubicon-bid-request.json b/src/test/resources/org/prebid/server/it/amp/test-rubicon-bid-request.json index 980408f6332..d12c3db99ce 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-rubicon-bid-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-rubicon-bid-request.json @@ -27,18 +27,25 @@ "target": { "page": [ "https://google.com" + ], + "gam-key1": [ + "val1" + ], + "gam-key2": [ + "val2" ] }, "track": { "mint": "", "mint_version": "" } - } + }, + "maxbids": 1 } } ], "site": { - "domain": "example.com", + "domain": "google.com", "page": "https://google.com", "publisher": { "ext": { @@ -62,7 +69,12 @@ } }, "user": { - "buyeruid": "J5VLCWQP-26-CWFT" + "buyeruid": "J5VLCWQP-26-CWFT", + "ext": { + "ConsentedProvidersSettings": { + "consented_providers": "someConsent" + } + } }, "at": 1, "tmax": 5000, diff --git a/src/test/resources/org/prebid/server/it/auction/adform/test-adform-bid-response-1.json b/src/test/resources/org/prebid/server/it/auction/adform/test-adform-bid-response-1.json deleted file mode 100644 index 1f06270b2ab..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/adform/test-adform-bid-response-1.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "response": "banner", - "banner": "adm12", - "win_bid": "10.5", - "currency": "currency", - "width": 400, - "height": 300, - "dealId": "dealId", - "win_crid": "crid12" - } -] \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/adform/test-auction-adform-request.json b/src/test/resources/org/prebid/server/it/auction/adform/test-auction-adform-request.json deleted file mode 100644 index 0413c04ffe4..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/adform/test-auction-adform-request.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "account_id": "1001", - "tid": "tid", - "cache_markup": 1, - "sort_bids": 1, - "timeout_millis": 5000, - "ad_units": [ - { - "code": "adUnitCode12", - "sizes": [ - { - "w": 300, - "h": 250 - } - ], - "bids": [ - { - "bid_id": "bidId12", - "bidder": "adform", - "params": { - "mid": 15, - "priceType": "gross" - } - } - ], - "media_types": [ - "banner" - ] - } - ], - "device": { - "pxratio": 4.2, - "language": "en", - "dnt": 10, - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "sdk": { - "version": "version1", - "source": "source1", - "platform": "platform1" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/adform/test-auction-adform-response.json b/src/test/resources/org/prebid/server/it/auction/adform/test-auction-adform-response.json deleted file mode 100644 index 4af2f8c27dd..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/adform/test-auction-adform-response.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "tid": "tid", - "status": "no_cookie", - "bidder_status": [ - { - "bidder": "adform", - "response_time_ms": "{{ adform.response_time_ms }}", - "num_bids": 1, - "debug": [ - { - "request_uri": "{{ adform.exchange_uri }}?CC=1&adid=ifaId&fd=1&gdpr=1&gdpr_consent=consent1&ip=193.168.244.1&pt=gross&rp=4&stid=tid&bWlkPTE1JnJjdXI9VVNE", - "response_body": "[{\"response\":\"banner\",\"banner\":\"adm12\",\"win_bid\":\"10.5\",\"currency\":\"currency\",\"width\":400,\"height\":300,\"dealId\":\"dealId\",\"win_crid\":\"crid12\"}]", - "status_code": 200 - } - ] - } - ], - "bids": [ - { - "bid_id": "bidId12", - "code": "adUnitCode12", - "creative_id": "crid12", - "media_type": "banner", - "bidder": "adform", - "price": 10.5, - "width": 400, - "height": 300, - "cache_id": "6e07e8e2-fc58-4bb0-8d66-2277589bd27c", - "cache_url": "{{ cache.resource_url }}6e07e8e2-fc58-4bb0-8d66-2277589bd27c", - "response_time_ms": "{{ adform.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "10.50", - "hb_size_adform": "400x300", - "hb_size": "400x300", - "hb_bidder": "adform", - "hb_cache_id_adform": "6e07e8e2-fc58-4bb0-8d66-2277589bd27c", - "hb_cache_id": "6e07e8e2-fc58-4bb0-8d66-2277589bd27c", - "hb_bidder_adform": "adform", - "hb_pb_adform": "10.50" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/adform/test-cache-adform-request.json b/src/test/resources/org/prebid/server/it/auction/adform/test-cache-adform-request.json deleted file mode 100644 index 86fa2720966..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/adform/test-cache-adform-request.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "adm": "adm12", - "width": 400, - "height": 300 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/adform/test-cache-adform-response.json b/src/test/resources/org/prebid/server/it/auction/adform/test-cache-adform-response.json deleted file mode 100644 index aa8366074bf..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/adform/test-cache-adform-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "6e07e8e2-fc58-4bb0-8d66-2277589bd27c" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-request.json b/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-request.json deleted file mode 100644 index 2e66d02422d..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-request.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "account_id": "1001", - "tid": "tid", - "cache_markup": 1, - "sort_bids": 1, - "timeout_millis": 5000, - "ad_units": [ - { - "code": "adUnitCode10", - "sizes": [ - { - "w": 300, - "h": 250 - } - ], - "bids": [ - { - "bid_id": "bidId10", - "bidder": "conversant", - "params": { - "site_id": "siteId1", - "secure": 42, - "tag_id": "tagId1", - "position": 28, - "bidfloor": 7.32, - "mobile": 64, - "mimes": [ - "mime1" - ], - "api": [ - 1 - ], - "protocols": [ - 5 - ], - "maxduration": 30 - } - } - ], - "media_types": [ - "banner" - ] - } - ], - "device": { - "pxratio": 4.2, - "language": "en", - "dnt": 10, - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "sdk": { - "version": "version1", - "source": "source1", - "platform": "platform1" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-response.json b/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-response.json deleted file mode 100644 index fec15683456..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/conversant/test-auction-conversant-response.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "tid": "tid", - "status": "no_cookie", - "bidder_status": [ - { - "bidder": "conversant", - "response_time_ms": "{{ conversant.response_time_ms }}", - "num_bids": 1, - "debug": [ - { - "request_uri": "{{ conversant.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode10\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250,\"pos\":28},\"displaymanager\":\"prebid-s2s\",\"displaymanagerver\":\"1.0.1\",\"tagid\":\"tagId1\",\"bidfloor\":7.32,\"secure\":42}],\"site\":{\"id\":\"siteId1\",\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"mobile\":64},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"CV-UID\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId10\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode10\",\"price\":5.78,\"adm\":\"adm10\",\"crid\":\"crid10\",\"dealid\":\"dealId10\",\"w\":300,\"h\":250}],\"seat\":\"seatId10\",\"group\":0}]}", - "status_code": 200 - } - ] - } - ], - "bids": [ - { - "bid_id": "bidId10", - "code": "adUnitCode10", - "creative_id": "crid10", - "media_type": "banner", - "bidder": "conversant", - "price": 5.78, - "width": 300, - "height": 250, - "cache_id": "2cb178a4-881d-4dde-a5f6-370fb2e93017", - "cache_url": "{{ cache.resource_url }}2cb178a4-881d-4dde-a5f6-370fb2e93017", - "response_time_ms": "{{ conversant.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "5.70", - "hb_cache_id_conversant": "2cb178a4-881d-4dde-a5f6-370fb2e93017", - "hb_size": "300x250", - "hb_pb_conversant": "5.70", - "hb_bidder_conversant": "conversant", - "hb_bidder": "conversant", - "hb_cache_id": "2cb178a4-881d-4dde-a5f6-370fb2e93017", - "hb_size_conversant": "300x250" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/conversant/test-cache-conversant-request.json b/src/test/resources/org/prebid/server/it/auction/conversant/test-cache-conversant-request.json deleted file mode 100644 index 21122b286c9..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/conversant/test-cache-conversant-request.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "adm": "adm10", - "width": 300, - "height": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/conversant/test-cache-conversant-response.json b/src/test/resources/org/prebid/server/it/auction/conversant/test-cache-conversant-response.json deleted file mode 100644 index 8584f6fb4bb..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/conversant/test-cache-conversant-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "2cb178a4-881d-4dde-a5f6-370fb2e93017" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/conversant/test-conversant-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/conversant/test-conversant-bid-request-1.json deleted file mode 100644 index a7bedd264ce..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/conversant/test-conversant-bid-request-1.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode10", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 300, - "h": 250, - "pos": 28 - }, - "displaymanager": "prebid-s2s", - "displaymanagerver": "1.0.1", - "tagid": "tagId1", - "bidfloor": 7.32, - "secure": 42 - } - ], - "site": { - "id": "siteId1", - "domain": "example.com", - "page": "http://www.example.com", - "mobile": 64 - }, - "device": { - "ua": "userAgent", - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId", - "dnt": 10 - }, - "user": { - "buyeruid": "CV-UID", - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/conversant/test-conversant-bid-response-1.json b/src/test/resources/org/prebid/server/it/auction/conversant/test-conversant-bid-response-1.json deleted file mode 100644 index 7bcc9648d6b..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/conversant/test-conversant-bid-response-1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "bidResponseId10", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode10", - "price": 5.78, - "adm": "adm10", - "crid": "crid10", - "dealid": "dealId10", - "w": 300, - "h": 250 - } - ], - "seat": "seatId10", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-request.json b/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-request.json deleted file mode 100644 index 23849c265da..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-request.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "account_id": "1001", - "tid": "tid", - "cache_markup": 1, - "sort_bids": 1, - "timeout_millis": 5000, - "ad_units": [ - { - "code": "adUnitCode4", - "sizes": [ - { - "w": 300, - "h": 250 - } - ], - "bids": [ - { - "bid_id": "bidId4", - "bidder": "districtm", - "params": { - "placement_id": 9848285, - "inv_code": "invCode1", - "member": "member1", - "keywords": [ - { - "key": "k1", - "value": [ - "v1", - "v2" - ] - } - ], - "traffic_source_code": "trafficSourceCode1", - "reserve": 1.0, - "position": "above" - } - } - ], - "media_types": [ - "video" - ], - "video": { - "mimes": [ - "mimes" - ], - "minduration": 20, - "maxduration": 60, - "startdelay": 5, - "playback_method": 1, - "protocols": [ - 1 - ] - } - } - ], - "device": { - "pxratio": 4.2, - "language": "en", - "dnt": 10, - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "sdk": { - "version": "version1", - "source": "source1", - "platform": "platform1" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-response.json b/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-response.json deleted file mode 100644 index 33df9613410..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/districtm/test-auction-districtm-response.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "tid": "tid", - "status": "no_cookie", - "bidder_status": [ - { - "bidder": "districtm", - "response_time_ms": "{{ districtm.response_time_ms }}", - "num_bids": 1, - "debug": [ - { - "request_uri": "{{ appnexus.exchange_uri }}?member_id=member1", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode4\",\"video\":{\"mimes\":[\"mimes\"],\"minduration\":20,\"maxduration\":60,\"protocols\":[1],\"w\":300,\"h\":250,\"startdelay\":5,\"playbackmethod\":[1]},\"tagid\":\"invCode1\",\"bidfloor\":1.0,\"ext\":{\"appnexus\":{\"placement_id\":9848285,\"keywords\":\"k1=v1,k1=v2\",\"traffic_source_code\":\"trafficSourceCode1\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"id\":\"12345\",\"buyeruid\":\"12345\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId4\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode4\",\"price\":5.78,\"adm\":\"adm4\",\"crid\":\"crid4\",\"dealid\":\"dealId4\",\"w\":300,\"h\":250,\"ext\":{\"appnexus\":{\"bid_ad_type\":1}}}],\"seat\":\"seatId4\",\"group\":0}]}", - "status_code": 200 - } - ] - } - ], - "bids": [ - { - "bid_id": "bidId4", - "code": "adUnitCode4", - "creative_id": "crid4", - "media_type": "video", - "bidder": "districtm", - "price": 5.78, - "width": 300, - "height": 250, - "deal_id": "dealId4", - "cache_id": "aa5bcbed-344f-4c63-8ae2-a9017ebbe971", - "cache_url": "{{ cache.resource_url }}aa5bcbed-344f-4c63-8ae2-a9017ebbe971", - "response_time_ms": "{{ districtm.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "5.70", - "hb_size_districtm": "300x250", - "hb_deal_districtm": "dealId4", - "hb_bidder_districtm": "districtm", - "hb_size": "300x250", - "hb_bidder": "districtm", - "hb_cache_id": "aa5bcbed-344f-4c63-8ae2-a9017ebbe971", - "hb_pb_districtm": "5.70", - "hb_cache_id_districtm": "aa5bcbed-344f-4c63-8ae2-a9017ebbe971", - "hb_deal": "dealId4" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/districtm/test-cache-districtm-request.json b/src/test/resources/org/prebid/server/it/auction/districtm/test-cache-districtm-request.json deleted file mode 100644 index 5680b6d1a47..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/districtm/test-cache-districtm-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "puts": [ - { - "type": "xml", - "value": "adm4" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/districtm/test-cache-districtm-response.json b/src/test/resources/org/prebid/server/it/auction/districtm/test-cache-districtm-response.json deleted file mode 100644 index da049428cad..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/districtm/test-cache-districtm-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "aa5bcbed-344f-4c63-8ae2-a9017ebbe971" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/districtm/test-districtm-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/districtm/test-districtm-bid-request-1.json deleted file mode 100644 index 5acca6e19ea..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/districtm/test-districtm-bid-request-1.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode4", - "video": { - "mimes": [ - "mimes" - ], - "minduration": 20, - "maxduration": 60, - "protocols": [ - 1 - ], - "w": 300, - "h": 250, - "startdelay": 5, - "playbackmethod": [ - 1 - ] - }, - "tagid": "invCode1", - "bidfloor": 1.0, - "ext": { - "appnexus": { - "placement_id": 9848285, - "keywords": "k1=v1,k1=v2", - "traffic_source_code": "trafficSourceCode1" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com" - }, - "device": { - "ua": "userAgent", - "dnt": 10, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "id": "12345", - "buyeruid": "12345", - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 1 - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/districtm/test-districtm-bid-response-1.json b/src/test/resources/org/prebid/server/it/auction/districtm/test-districtm-bid-response-1.json deleted file mode 100644 index dd839aaeef0..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/districtm/test-districtm-bid-response-1.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "bidResponseId4", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode4", - "price": 5.78, - "adm": "adm4", - "crid": "crid4", - "dealid": "dealId4", - "w": 300, - "h": 250, - "ext": { - "appnexus": { - "bid_ad_type": 1 - } - } - } - ], - "seat": "seatId4", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-request.json b/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-request.json deleted file mode 100644 index 1df884d2720..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-request.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "account_id": "1001", - "tid": "tid", - "cache_markup": 1, - "sort_bids": 1, - "timeout_millis": 5000, - "ad_units": [ - { - "code": "adUnitCode7", - "sizes": [ - { - "w": 300, - "h": 250 - } - ], - "bids": [ - { - "bid_id": "bidId7", - "bidder": "ix", - "params": { - "siteId": 486 - } - } - ], - "media_types": [ - "banner" - ] - } - ], - "device": { - "pxratio": 4.2, - "language": "en", - "dnt": 10, - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "sdk": { - "version": "version1", - "source": "source1", - "platform": "platform1" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-response.json b/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-response.json deleted file mode 100644 index 398840b9c3e..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/ix/test-auction-ix-response.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "tid": "tid", - "status": "no_cookie", - "bidder_status": [ - { - "bidder": "ix", - "response_time_ms": "{{ ix.response_time_ms }}", - "num_bids": 1, - "debug": [ - { - "request_uri": "{{ ix.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode7\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250},\"tagid\":\"adUnitCode7\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"486\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"IE-UID\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId7\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode7\",\"price\":5.78,\"adm\":\"adm7\",\"crid\":\"crid7\",\"dealid\":\"dealId7\",\"w\":300,\"h\":250}],\"seat\":\"seatId7\",\"group\":0}]}", - "status_code": 200 - } - ] - } - ], - "bids": [ - { - "bid_id": "bidId7", - "code": "adUnitCode7", - "creative_id": "crid7", - "media_type":"banner", - "bidder": "ix", - "price": 5.78, - "width": 300, - "height": 250, - "deal_id": "dealId7", - "cache_id": "f5c0e27a-1b9c-4b9c-9eb3-7d2b94900bf8", - "cache_url": "{{ cache.resource_url }}f5c0e27a-1b9c-4b9c-9eb3-7d2b94900bf8", - "response_time_ms": "{{ ix.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "5.70", - "hb_size_ix": "300x250", - "hb_size": "300x250", - "hb_pb_ix": "5.70", - "hb_deal_ix": "dealId7", - "hb_bidder": "ix", - "hb_cache_id": "f5c0e27a-1b9c-4b9c-9eb3-7d2b94900bf8", - "hb_bidder_ix": "ix", - "hb_cache_id_ix": "f5c0e27a-1b9c-4b9c-9eb3-7d2b94900bf8", - "hb_deal": "dealId7" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/ix/test-cache-ix-request.json b/src/test/resources/org/prebid/server/it/auction/ix/test-cache-ix-request.json deleted file mode 100644 index 2bfccbe7ffd..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/ix/test-cache-ix-request.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "adm": "adm7", - "width": 300, - "height": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/ix/test-cache-ix-response.json b/src/test/resources/org/prebid/server/it/auction/ix/test-cache-ix-response.json deleted file mode 100644 index e804337dfa9..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/ix/test-cache-ix-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "f5c0e27a-1b9c-4b9c-9eb3-7d2b94900bf8" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/ix/test-ix-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/ix/test-ix-bid-request-1.json deleted file mode 100644 index 311b3bd37fd..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/ix/test-ix-bid-request-1.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode7", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 300, - "h": 250 - }, - "tagid": "adUnitCode7" - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "486" - } - }, - "device": { - "ua": "userAgent", - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId", - "dnt": 10 - }, - "user": { - "buyeruid": "IE-UID", - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/ix/test-ix-bid-response-1.json b/src/test/resources/org/prebid/server/it/auction/ix/test-ix-bid-response-1.json deleted file mode 100644 index be37cbacc59..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/ix/test-ix-bid-response-1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "bidResponseId7", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode7", - "price": 5.78, - "adm": "adm7", - "crid": "crid7", - "dealid": "dealId7", - "w": 300, - "h": 250 - } - ], - "seat": "seatId7", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-request.json b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-request.json deleted file mode 100644 index c05eadda14c..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-request.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "account_id": "1001", - "tid": "tid", - "cache_markup": 1, - "sort_bids": 1, - "timeout_millis": 5000, - "ad_units": [ - { - "code": "adUnitCode8", - "sizes": [ - { - "w": 300, - "h": 250 - } - ], - "bids": [ - { - "bid_id": "bidId8", - "bidder": "lifestreet", - "params": { - "slot_tag": "slot.tag1" - } - } - ], - "media_types": [ - "banner" - ] - } - ], - "device": { - "pxratio": 4.2, - "language": "en", - "dnt": 10, - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "sdk": { - "version": "version1", - "source": "source1", - "platform": "platform1" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-response.json b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-response.json deleted file mode 100644 index 2ba37a52718..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-auction-lifestreet-response.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "tid": "tid", - "status": "no_cookie", - "bidder_status": [ - { - "bidder": "lifestreet", - "response_time_ms": "{{ lifestreet.response_time_ms }}", - "num_bids": 1, - "debug": [ - { - "request_uri": "{{ lifestreet.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode8\",\"banner\":{\"w\":300,\"h\":250},\"tagid\":\"slot.tag1\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"LS-UID\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId8\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode8\",\"price\":5.78,\"adm\":\"adm8\",\"crid\":\"crid8\",\"dealid\":\"dealId8\",\"w\":300,\"h\":250}],\"seat\":\"seatId8\",\"group\":0}]}", - "status_code": 200 - } - ] - } - ], - "bids": [ - { - "bid_id": "bidId8", - "code": "adUnitCode8", - "creative_id": "crid8", - "bidder": "lifestreet", - "price": 5.78, - "width": 300, - "height": 250, - "deal_id": "dealId8", - "cache_id": "7a68398e-c00f-4ca9-88bb-9078a15e7e26", - "cache_url": "{{ cache.resource_url }}7a68398e-c00f-4ca9-88bb-9078a15e7e26", - "response_time_ms": "{{ lifestreet.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "5.70", - "hb_size_lifestreet": "300x250", - "hb_size": "300x250", - "hb_pb_lifestreet": "5.70", - "hb_bidder": "lifestreet", - "hb_cache_id": "7a68398e-c00f-4ca9-88bb-9078a15e7e26", - "hb_deal_lifestreet": "dealId8", - "hb_cache_id_lifestreet": "7a68398e-c00f-4ca9-88bb-9078a15e7e26", - "hb_deal": "dealId8", - "hb_bidder_lifestreet": "lifestreet" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-cache-lifestreet-request.json b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-cache-lifestreet-request.json deleted file mode 100644 index a93eb12c2da..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-cache-lifestreet-request.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "adm": "adm8", - "width": 300, - "height": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-cache-lifestreet-response.json b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-cache-lifestreet-response.json deleted file mode 100644 index 08d4189111f..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-cache-lifestreet-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "7a68398e-c00f-4ca9-88bb-9078a15e7e26" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-lifestreet-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-lifestreet-bid-request-1.json deleted file mode 100644 index b6351131bbe..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-lifestreet-bid-request-1.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode8", - "banner": { - "w": 300, - "h": 250 - }, - "tagid": "slot.tag1" - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com" - }, - "device": { - "ua": "userAgent", - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId", - "dnt": 10 - }, - "user": { - "buyeruid": "LS-UID", - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-lifestreet-bid-response-1.json b/src/test/resources/org/prebid/server/it/auction/lifestreet/test-lifestreet-bid-response-1.json deleted file mode 100644 index 22707ff7242..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/lifestreet/test-lifestreet-bid-response-1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "bidResponseId8", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode8", - "price": 5.78, - "adm": "adm8", - "crid": "crid8", - "dealid": "dealId8", - "w": 300, - "h": 250 - } - ], - "seat": "seatId8", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-request.json b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-request.json deleted file mode 100644 index 2befcd7a7e4..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-request.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "account_id": "1001", - "tid": "tid", - "cache_markup": 1, - "sort_bids": 1, - "timeout_millis": 5000, - "ad_units": [ - { - "code": "adUnitCode9", - "sizes": [ - { - "w": 200, - "h": 150 - } - ], - "bids": [ - { - "bid_id": "bidId9", - "bidder": "pubmatic", - "params": { - "publisherId": "publisherId9", - "adSlot": "slot9@300x250:zzz" - } - } - ], - "media_types": [ - "banner" - ] - } - ], - "device": { - "pxratio": 4.2, - "language": "en", - "dnt": 10, - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "sdk": { - "version": "version1", - "source": "source1", - "platform": "platform1" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-response.json b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-response.json deleted file mode 100644 index e8c3ab7e747..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-auction-pubmatic-response.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "tid": "tid", - "status": "no_cookie", - "bidder_status": [ - { - "bidder": "pubmatic", - "response_time_ms": "{{ pubmatic.response_time_ms }}", - "num_bids": 1, - "debug": [ - { - "request_uri": "{{ pubmatic.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode9\",\"banner\":{\"format\":[{\"w\":200,\"h\":150}],\"w\":300,\"h\":250},\"tagid\":\"slot9\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId9\",\"domain\":\"example.com\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"PM-UID\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId9\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode9\",\"price\":5.78,\"adm\":\"adm9\",\"crid\":\"crid9\",\"dealid\":\"dealId9\",\"w\":300,\"h\":250}],\"seat\":\"seatId9\",\"group\":0}]}", - "status_code": 200 - } - ] - } - ], - "bids": [ - { - "bid_id": "bidId9", - "code": "adUnitCode9", - "creative_id": "crid9", - "media_type": "banner", - "bidder": "pubmatic", - "price": 5.78, - "width": 300, - "height": 250, - "deal_id": "dealId9", - "cache_id": "a55a58c6-24d9-4a5b-a449-e95fb98d41dd", - "cache_url": "{{ cache.resource_url }}a55a58c6-24d9-4a5b-a449-e95fb98d41dd", - "response_time_ms": "{{ pubmatic.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "5.70", - "hb_pb_pubmatic": "5.70", - "hb_cache_id_pubmatic": "a55a58c6-24d9-4a5b-a449-e95fb98d41dd", - "hb_size": "300x250", - "hb_bidder": "pubmatic", - "hb_cache_id": "a55a58c6-24d9-4a5b-a449-e95fb98d41dd", - "hb_deal_pubmatic": "dealId9", - "hb_bidder_pubmatic": "pubmatic", - "hb_deal": "dealId9", - "hb_size_pubmatic": "300x250" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-cache-pubmatic-request.json b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-cache-pubmatic-request.json deleted file mode 100644 index b015ea2139d..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-cache-pubmatic-request.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "adm": "adm9", - "width": 300, - "height": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-cache-pubmatic-response.json b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-cache-pubmatic-response.json deleted file mode 100644 index 36383fd6e4b..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-cache-pubmatic-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "a55a58c6-24d9-4a5b-a449-e95fb98d41dd" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-pubmatic-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-pubmatic-bid-request-1.json deleted file mode 100644 index f42d82b05d8..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-pubmatic-bid-request-1.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode9", - "banner": { - "format": [ - { - "w": 200, - "h":150 - } - ], - "w": 300, - "h": 250 - }, - "tagid": "slot9" - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId9", - "domain": "example.com" - } - }, - "device": { - "ua": "userAgent", - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId", - "dnt": 10 - }, - "user": { - "buyeruid": "PM-UID", - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-pubmatic-bid-response-1.json b/src/test/resources/org/prebid/server/it/auction/pubmatic/test-pubmatic-bid-response-1.json deleted file mode 100644 index 53906d8862a..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pubmatic/test-pubmatic-bid-response-1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "bidResponseId9", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode9", - "price": 5.78, - "adm": "adm9", - "crid": "crid9", - "dealid": "dealId9", - "w": 300, - "h": 250 - } - ], - "seat": "seatId9", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-request.json b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-request.json deleted file mode 100644 index ef099a0f325..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-request.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "account_id": "1001", - "tid": "tid", - "cache_markup": 1, - "sort_bids": 1, - "timeout_millis": 5000, - "ad_units": [ - { - "code": "adUnitCode6", - "sizes": [ - { - "w": 300, - "h": 250 - } - ], - "bids": [ - { - "bid_id": "bidId6", - "bidder": "pulsepoint", - "params": { - "cp": 123, - "ct": 456, - "cf": "300x250" - } - } - ], - "media_types": [ - "banner" - ] - } - ], - "device": { - "pxratio": 4.2, - "language": "en", - "dnt": 10, - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "sdk": { - "version": "version1", - "source": "source1", - "platform": "platform1" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-response.json b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-response.json deleted file mode 100644 index 1cc5f52f719..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-auction-pulsepoint-response.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "tid": "tid", - "status": "no_cookie", - "bidder_status": [ - { - "bidder": "pulsepoint", - "response_time_ms": "{{ pulsepoint.response_time_ms }}", - "num_bids": 1, - "debug": [ - { - "request_uri": "{{ pulsepoint.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode6\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250},\"tagid\":\"456\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"123\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"PP-UID\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId6\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode6\",\"price\":5.78,\"adm\":\"adm6\",\"crid\":\"crid6\",\"dealid\":\"dealId6\",\"w\":300,\"h\":250}],\"seat\":\"seatId6\",\"group\":0}]}", - "status_code": 200 - } - ] - } - ], - "bids": [ - { - "bid_id": "bidId6", - "code": "adUnitCode6", - "creative_id": "crid6", - "media_type": "banner", - "bidder": "pulsepoint", - "price": 5.78, - "width": 300, - "height": 250, - "cache_id": "9380cf5b-d1d3-4112-ae80-d4420052af3f", - "cache_url": "{{ cache.resource_url }}9380cf5b-d1d3-4112-ae80-d4420052af3f", - "response_time_ms": "{{ pulsepoint.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "5.70", - "hb_cache_id_pulsepoint": "9380cf5b-d1d3-4112-ae80-d4420052af3f", - "hb_pb_pulsepoint": "5.70", - "hb_size_pulsepoint": "300x250", - "hb_size": "300x250", - "hb_bidder_pulsepoint": "pulsepoint", - "hb_bidder": "pulsepoint", - "hb_cache_id": "9380cf5b-d1d3-4112-ae80-d4420052af3f" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-cache-pulsepoint-request.json b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-cache-pulsepoint-request.json deleted file mode 100644 index fcaac2fe2e6..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-cache-pulsepoint-request.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "adm": "adm6", - "width": 300, - "height": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-cache-pulsepoint-response.json b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-cache-pulsepoint-response.json deleted file mode 100644 index 280007554de..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-cache-pulsepoint-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "9380cf5b-d1d3-4112-ae80-d4420052af3f" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-pulsepoint-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-pulsepoint-bid-request-1.json deleted file mode 100644 index 04973cac104..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-pulsepoint-bid-request-1.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode6", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 300, - "h": 250 - }, - "tagid": "456" - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "123" - } - }, - "device": { - "ua": "userAgent", - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId", - "dnt": 10 - }, - "user": { - "buyeruid": "PP-UID", - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-pulsepoint-bid-response-1.json b/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-pulsepoint-bid-response-1.json deleted file mode 100644 index 7fb4c849025..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/pulsepoint/test-pulsepoint-bid-response-1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "bidResponseId6", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode6", - "price": 5.78, - "adm": "adm6", - "crid": "crid6", - "dealid": "dealId6", - "w": 300, - "h": 250 - } - ], - "seat": "seatId6", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-appnexus-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-appnexus-bid-request-1.json deleted file mode 100644 index 5acca6e19ea..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-appnexus-bid-request-1.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode4", - "video": { - "mimes": [ - "mimes" - ], - "minduration": 20, - "maxduration": 60, - "protocols": [ - 1 - ], - "w": 300, - "h": 250, - "startdelay": 5, - "playbackmethod": [ - 1 - ] - }, - "tagid": "invCode1", - "bidfloor": 1.0, - "ext": { - "appnexus": { - "placement_id": 9848285, - "keywords": "k1=v1,k1=v2", - "traffic_source_code": "trafficSourceCode1" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com" - }, - "device": { - "ua": "userAgent", - "dnt": 10, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "id": "12345", - "buyeruid": "12345", - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 1 - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-appnexus-bid-response-1.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-appnexus-bid-response-1.json deleted file mode 100644 index dd839aaeef0..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-appnexus-bid-response-1.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "bidResponseId4", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode4", - "price": 5.78, - "adm": "adm4", - "crid": "crid4", - "dealid": "dealId4", - "w": 300, - "h": 250, - "ext": { - "appnexus": { - "bid_ad_type": 1 - } - } - } - ], - "seat": "seatId4", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-request.json deleted file mode 100644 index 0ec5c77e558..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-request.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "account_id": "1001", - "tid": "tid", - "cache_markup": 1, - "sort_bids": 1, - "timeout_millis": 5000, - "ad_units": [ - { - "code": "adUnitCode1", - "sizes": [ - { - "w": 300, - "h": 250 - } - ], - "bids": [ - { - "bid_id": "bidId1", - "bidder": "rubicon", - "params": { - "accountId": 2001, - "siteId": 3001, - "zoneId": 4001, - "inventory": { - "rating": [ - "5-star" - ], - "prodtype": [ - "tech" - ] - }, - "visitor": { - "ucat": [ - "new" - ], - "search": [ - "iphone" - ] - }, - "video": { - "size_id": 15, - "skip": 5, - "skipdelay": 1 - } - } - } - ], - "media_types": [ - "video" - ], - "video": { - "mimes": [ - "mimes" - ], - "minduration": 20, - "maxduration": 60, - "startdelay": 5, - "skippable": 5, - "playback_method": 1, - "protocols": [ - 1 - ] - } - }, - { - "code": "adUnitCode2", - "sizes": [ - { - "w": 300, - "h": 600 - } - ], - "config_id": "14062", - "media_types": [ - "banner" - ] - }, - { - "code": "adUnitCode3", - "sizes": [ - { - "w": 768, - "h": 1024 - }, - { - "w": 980, - "h": 400 - } - ], - "bids": [ - { - "bid_id": "bidId3", - "bidder": "rubicon", - "params": { - "accountId": 2001, - "siteId": 3001, - "zoneId": 4001 - } - } - ], - "media_types": [ - "banner" - ] - }, - { - "code": "adUnitCode4", - "sizes": [ - { - "w": 300, - "h": 250 - } - ], - "bids": [ - { - "bid_id": "bidId4", - "bidder": "appnexus", - "params": { - "placement_id": 9848285, - "inv_code": "invCode1", - "member": "member1", - "keywords": [ - { - "key": "k1", - "value": [ - "v1", - "v2" - ] - } - ], - "traffic_source_code": "trafficSourceCode1", - "reserve": 1.0, - "position": "above" - } - } - ], - "media_types": [ - "video" - ], - "video": { - "mimes": [ - "mimes" - ], - "minduration": 20, - "maxduration": 60, - "startdelay": 5, - "playback_method": 1, - "protocols": [ - 1 - ] - } - } - ], - "device": { - "pxratio": 4.2, - "language": "en", - "dnt": 10, - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "sdk": { - "version": "version1", - "source": "source1", - "platform": "platform1" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-response.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-response.json deleted file mode 100644 index 0d599bcb5e4..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-auction-rubicon-appnexus-response.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "tid": "tid", - "status": "no_cookie", - "bidder_status": [ - { - "bidder": "appnexus", - "response_time_ms": "{{ appnexus.response_time_ms }}", - "num_bids": 1, - "debug": [ - { - "request_uri": "{{ appnexus.exchange_uri }}?member_id=member1", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode4\",\"video\":{\"mimes\":[\"mimes\"],\"minduration\":20,\"maxduration\":60,\"protocols\":[1],\"w\":300,\"h\":250,\"startdelay\":5,\"playbackmethod\":[1]},\"tagid\":\"invCode1\",\"bidfloor\":1.0,\"ext\":{\"appnexus\":{\"placement_id\":9848285,\"keywords\":\"k1=v1,k1=v2\",\"traffic_source_code\":\"trafficSourceCode1\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"id\":\"12345\",\"buyeruid\":\"12345\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId4\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode4\",\"price\":5.78,\"adm\":\"adm4\",\"crid\":\"crid4\",\"dealid\":\"dealId4\",\"w\":300,\"h\":250,\"ext\":{\"appnexus\":{\"bid_ad_type\":1}}}],\"seat\":\"seatId4\",\"group\":0}]}", - "status_code": 200 - } - ] - }, - { - "bidder": "rubicon", - "response_time_ms": "{{ rubicon.response_time_ms }}", - "num_bids": 2, - "debug": [ - { - "request_uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode1\",\"video\":{\"mimes\":[\"mimes\"],\"minduration\":20,\"maxduration\":60,\"protocols\":[1],\"w\":300,\"h\":250,\"startdelay\":5,\"playbackmethod\":[1],\"ext\":{\"skip\":5,\"skipdelay\":1,\"rp\":{\"size_id\":15}}},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"rating\":[\"5-star\"],\"prodtype\":[\"tech\"]},\"track\":{\"mint\":\"prebid\",\"mint_version\":\"source1_platform1_version1\"}}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\",\"ext\":{\"rp\":{\"pixelratio\":4.2}}},\"user\":{\"id\":\"12345\",\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"ext\":{\"consent\":\"consent1\",\"rp\":{\"target\":{\"ucat\":[\"new\"],\"search\":[\"iphone\"]}}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode1\",\"price\":8.43,\"adm\":\"adm1\",\"crid\":\"crid1\",\"dealid\":\"dealId1\",\"w\":300,\"h\":250,\"ext\":{\"rp\":{\"targeting\":[{\"key\":\"rpfl_1001\",\"values\":[\"2_tier0100\"]}]}}}],\"seat\":\"seatId1\",\"group\":0}]}", - "status_code": 200 - }, - { - "request_uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode2\",\"banner\":{\"format\":[{\"w\":300,\"h\":600}],\"w\":300,\"h\":600,\"ext\":{\"rp\":{\"size_id\":10,\"mime\":\"text/html\"}}},\"ext\":{\"rp\":{\"zone_id\":7001,\"track\":{\"mint\":\"prebid\",\"mint_version\":\"source1_platform1_version1\"}}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":5001}}},\"ext\":{\"rp\":{\"site_id\":6001}}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\",\"ext\":{\"rp\":{\"pixelratio\":4.2}}},\"user\":{\"id\":\"12345\",\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"ext\":{\"consent\":\"consent1\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId2\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode2\",\"price\":4.26,\"adm\":\"adm2\",\"crid\":\"crid2\",\"dealid\":\"dealId2\",\"w\":300,\"h\":600}],\"seat\":\"seatId2\",\"group\":0}]}", - "status_code": 200 - }, - { - "request_uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode3\",\"banner\":{\"format\":[{\"w\":768,\"h\":1024},{\"w\":980,\"h\":400}],\"w\":768,\"h\":1024,\"ext\":{\"rp\":{\"size_id\":80,\"alt_size_ids\":[102],\"mime\":\"text/html\"}}},\"ext\":{\"rp\":{\"zone_id\":4001,\"track\":{\"mint\":\"prebid\",\"mint_version\":\"source1_platform1_version1\"}}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\",\"ext\":{\"rp\":{\"pixelratio\":4.2}}},\"user\":{\"id\":\"12345\",\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"ext\":{\"consent\":\"consent1\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId3\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode3\",\"price\":5.12,\"adm\":\"adm3\",\"crid\":\"crid3\",\"dealid\":\"dealId3\",\"w\":0,\"h\":0}],\"seat\":\"seatId3\",\"group\":0}]}", - "status_code": 200 - } - ] - } - ], - "bids": [ - { - "bid_id": "bidId4", - "code": "adUnitCode4", - "creative_id": "crid4", - "media_type": "video", - "bidder": "appnexus", - "price": 5.78, - "width": 300, - "height": 250, - "deal_id": "dealId4", - "cache_id": "724b4488-f344-4e73-b79a-07e302176875", - "cache_url": "{{ cache.resource_url }}724b4488-f344-4e73-b79a-07e302176875", - "response_time_ms": "{{ appnexus.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "5.70", - "hb_pb_appnexus": "5.70", - "hb_size": "300x250", - "hb_bidder_appnexus": "appnexus", - "hb_bidder": "appnexus", - "hb_deal_appnexus": "dealId4", - "hb_cache_id": "724b4488-f344-4e73-b79a-07e302176875", - "hb_size_appnexus": "300x250", - "hb_deal": "dealId4", - "hb_cache_id_appnexus": "724b4488-f344-4e73-b79a-07e302176875" - } - }, - { - "bid_id": "bidId1", - "code": "adUnitCode1", - "creative_id": "crid1", - "media_type": "video", - "bidder": "rubicon", - "price": 8.43, - "width": 300, - "height": 250, - "deal_id": "dealId1", - "cache_id": "7d57932e-0b73-4835-a9a8-155aa145f6be", - "cache_url": "{{ cache.resource_url }}7d57932e-0b73-4835-a9a8-155aa145f6be", - "response_time_ms": "{{ rubicon.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "8.40", - "hb_pb_rubicon": "8.40", - "hb_cache_id_rubicon": "7d57932e-0b73-4835-a9a8-155aa145f6be", - "hb_deal_rubicon": "dealId1", - "hb_size": "300x250", - "hb_bidder": "rubicon", - "hb_size_rubicon": "300x250", - "hb_bidder_rubicon": "rubicon", - "hb_cache_id": "7d57932e-0b73-4835-a9a8-155aa145f6be", - "hb_deal": "dealId1", - "rpfl_1001": "2_tier0100" - } - }, - { - "bid_id": "bidId2", - "code": "adUnitCode2", - "creative_id": "crid2", - "media_type": "banner", - "bidder": "rubicon", - "price": 4.26, - "width": 300, - "height": 600, - "deal_id": "dealId2", - "cache_id": "24eae0fc-1070-4a4e-a16d-401be6a3de2a", - "cache_url": "{{ cache.resource_url }}24eae0fc-1070-4a4e-a16d-401be6a3de2a", - "response_time_ms": "{{ rubicon.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "4.20", - "hb_pb_rubicon": "4.20", - "hb_cache_id_rubicon": "24eae0fc-1070-4a4e-a16d-401be6a3de2a", - "hb_deal_rubicon": "dealId2", - "hb_size": "300x600", - "hb_bidder": "rubicon", - "hb_size_rubicon": "300x600", - "hb_bidder_rubicon": "rubicon", - "hb_cache_id": "24eae0fc-1070-4a4e-a16d-401be6a3de2a", - "hb_deal": "dealId2" - } - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-cache-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-cache-rubicon-appnexus-request.json deleted file mode 100644 index 90e784b1ef6..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-cache-rubicon-appnexus-request.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "puts": [ - { - "type": "xml", - "value": "adm4" - }, - { - "type": "xml", - "value": "adm1" - }, - { - "type": "json", - "value": { - "adm": "adm2", - "width": 300, - "height": 600 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-cache-rubicon-appnexus-response.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-cache-rubicon-appnexus-response.json deleted file mode 100644 index d694bb7fc9b..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-cache-rubicon-appnexus-response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "responses": [ - { - "uuid": "724b4488-f344-4e73-b79a-07e302176875" - }, - { - "uuid": "7d57932e-0b73-4835-a9a8-155aa145f6be" - }, - { - "uuid": "24eae0fc-1070-4a4e-a16d-401be6a3de2a" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-request-1.json deleted file mode 100644 index fb6db46cdb6..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-request-1.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode1", - "video": { - "mimes": [ - "mimes" - ], - "minduration": 20, - "maxduration": 60, - "protocols": [ - 1 - ], - "w": 300, - "h": 250, - "startdelay": 5, - "playbackmethod": [ - 1 - ], - "ext": { - "skip": 5, - "skipdelay": 1, - "rp": { - "size_id": 15 - } - } - }, - "ext": { - "rp": { - "zone_id": 4001, - "target": { - "rating": [ - "5-star" - ], - "prodtype": [ - "tech" - ] - }, - "track": { - "mint": "prebid", - "mint_version": "source1_platform1_version1" - } - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "ext": { - "rp": { - "account_id": 2001 - } - } - }, - "ext": { - "rp": { - "site_id": 3001 - } - } - }, - "device": { - "ua": "userAgent", - "dnt": 10, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId", - "ext": { - "rp": { - "pixelratio": 4.2 - } - } - }, - "user": { - "id": "12345", - "buyeruid": "J5VLCWQP-26-CWFT", - "ext": { - "consent": "consent1", - "rp": { - "target": { - "ucat": [ - "new" - ], - "search": [ - "iphone" - ] - } - } - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 1 - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-request-2.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-request-2.json deleted file mode 100644 index 9325d46f3f8..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-request-2.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode2", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ], - "w": 300, - "h": 600, - "ext": { - "rp": { - "size_id": 10, - "mime": "text/html" - } - } - }, - "ext": { - "rp": { - "zone_id": 7001, - "track": { - "mint": "prebid", - "mint_version": "source1_platform1_version1" - } - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "ext": { - "rp": { - "account_id": 5001 - } - } - }, - "ext": { - "rp": { - "site_id": 6001 - } - } - }, - "device": { - "ua": "userAgent", - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId", - "dnt": 10, - "ext": { - "rp": { - "pixelratio": 4.2 - } - } - }, - "user": { - "id": "12345", - "buyeruid": "J5VLCWQP-26-CWFT", - "ext": { - "consent": "consent1" - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - } -} diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-request-3.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-request-3.json deleted file mode 100644 index 6d72214c356..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-request-3.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode3", - "banner": { - "format": [ - { - "w": 768, - "h": 1024 - }, - { - "w": 980, - "h": 400 - } - ], - "w": 768, - "h": 1024, - "ext": { - "rp": { - "size_id": 80, - "alt_size_ids": [ - 102 - ], - "mime": "text/html" - } - } - }, - "ext": { - "rp": { - "zone_id": 4001, - "track": { - "mint": "prebid", - "mint_version": "source1_platform1_version1" - } - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "ext": { - "rp": { - "account_id": 2001 - } - } - }, - "ext": { - "rp": { - "site_id": 3001 - } - } - }, - "device": { - "ua": "userAgent", - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId", - "dnt": 10, - "ext": { - "rp": { - "pixelratio": 4.2 - } - } - }, - "user": { - "id": "12345", - "buyeruid": "J5VLCWQP-26-CWFT", - "ext": { - "consent" : "consent1" - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - } -} diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-response-1.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-response-1.json deleted file mode 100644 index 7396703433a..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-response-1.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "id": "bidResponseId1", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode1", - "price": 8.43, - "adm": "adm1", - "crid": "crid1", - "dealid": "dealId1", - "w": 300, - "h": 250, - "ext": { - "rp": { - "targeting": [ - { - "key": "rpfl_1001", - "values": [ - "2_tier0100" - ] - } - ] - } - } - } - ], - "seat": "seatId1", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-response-2.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-response-2.json deleted file mode 100644 index 0d686bd4b94..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-response-2.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "bidResponseId2", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode2", - "price": 4.26, - "adm": "adm2", - "crid": "crid2", - "dealid": "dealId2", - "w": 300, - "h": 600 - } - ], - "seat": "seatId2", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-response-3.json b/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-response-3.json deleted file mode 100644 index d102513ec71..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/rubicon_appnexus/test-rubicon-bid-response-3.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "bidResponseId3", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode3", - "price": 5.12, - "adm": "adm3", - "crid": "crid3", - "dealid": "dealId3", - "w": 0, - "h": 0 - } - ], - "seat": "seatId3", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-request.json b/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-request.json deleted file mode 100644 index 930ae4cceb0..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-request.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "account_id": "1001", - "tid": "tid", - "cache_markup": 1, - "sort_bids": 1, - "timeout_millis": 5000, - "ad_units": [ - { - "code": "adUnitCode11", - "sizes": [ - { - "w": 300, - "h": 250 - } - ], - "bids": [ - { - "bid_id": "bidId11", - "bidder": "sovrn", - "params": { - "site_id": "siteId1", - "secure": 42, - "tag_id": "tagId1", - "position": 28, - "bidfloor": 7.32, - "mobile": 64, - "mimes": [ - "mime1" - ], - "api": [ - 1 - ], - "protocols": [ - 5 - ], - "maxduration": 30 - } - } - ], - "media_types": [ - "banner" - ] - } - ], - "device": { - "pxratio": 4.2, - "language": "en", - "dnt": 10, - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "sdk": { - "version": "version1", - "source": "source1", - "platform": "platform1" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-response.json b/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-response.json deleted file mode 100644 index b08299832c8..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/sovrn/test-auction-sovrn-response.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "tid": "tid", - "status": "no_cookie", - "bidder_status": [ - { - "bidder": "sovrn", - "response_time_ms": "{{ sovrn.response_time_ms }}", - "num_bids": 1, - "debug": [ - { - "request_uri": "{{ sovrn.exchange_uri }}", - "request_body": "{\"id\":\"tid\",\"imp\":[{\"id\":\"adUnitCode11\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":300,\"h\":250},\"tagid\":\"tagId1\"}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\"},\"device\":{\"ua\":\"userAgent\",\"dnt\":10,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"990011\",\"ext\":{\"consent\":\"consent1\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":1}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":1}}}", - "response_body": "{\"id\":\"bidResponseId11\",\"seatbid\":[{\"bid\":[{\"impid\":\"adUnitCode11\",\"price\":5.78,\"adm\":\"adm11\",\"crid\":\"crid11\",\"dealid\":\"dealId11\",\"w\":300,\"h\":250}],\"seat\":\"seatId11\",\"group\":0}]}", - "status_code": 200 - } - ] - } - ], - "bids": [ - { - "bid_id": "bidId11", - "code": "adUnitCode11", - "creative_id": "crid11", - "bidder": "sovrn", - "price": 5.78, - "width": 300, - "height": 250, - "deal_id": "dealId11", - "cache_id": "b5aedfd4-06cb-4a79-bbb8-bfdcb57a29ef", - "cache_url": "{{ cache.resource_url }}b5aedfd4-06cb-4a79-bbb8-bfdcb57a29ef", - "response_time_ms": "{{ sovrn.response_time_ms }}", - "ad_server_targeting": { - "hb_pb": "5.70", - "hb_size": "300x250", - "hb_bidder_sovrn": "sovrn", - "hb_pb_sovrn": "5.70", - "hb_bidder": "sovrn", - "hb_cache_id": "b5aedfd4-06cb-4a79-bbb8-bfdcb57a29ef", - "hb_deal_sovrn": "dealId11", - "hb_deal": "dealId11", - "hb_size_sovrn": "300x250", - "hb_cache_id_sovrn": "b5aedfd4-06cb-4a79-bbb8-bfdcb57a29ef" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/sovrn/test-cache-sovrn-request.json b/src/test/resources/org/prebid/server/it/auction/sovrn/test-cache-sovrn-request.json deleted file mode 100644 index d21195449fd..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/sovrn/test-cache-sovrn-request.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "adm": "adm11", - "width": 300, - "height": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/sovrn/test-cache-sovrn-response.json b/src/test/resources/org/prebid/server/it/auction/sovrn/test-cache-sovrn-response.json deleted file mode 100644 index b7d5875767d..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/sovrn/test-cache-sovrn-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "b5aedfd4-06cb-4a79-bbb8-bfdcb57a29ef" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/sovrn/test-sovrn-bid-request-1.json b/src/test/resources/org/prebid/server/it/auction/sovrn/test-sovrn-bid-request-1.json deleted file mode 100644 index 67edaac40d7..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/sovrn/test-sovrn-bid-request-1.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "adUnitCode11", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 300, - "h": 250 - }, - "tagid": "tagId1" - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com" - }, - "device": { - "ua": "userAgent", - "dnt": 10, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "990011", - "ext": { - "consent": "consent1", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 1 - } - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "at": 1, - "tmax": 5000, - "source": { - "fd": 1, - "tid": "tid" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/auction/sovrn/test-sovrn-bid-response-1.json b/src/test/resources/org/prebid/server/it/auction/sovrn/test-sovrn-bid-response-1.json deleted file mode 100644 index b062b7ff795..00000000000 --- a/src/test/resources/org/prebid/server/it/auction/sovrn/test-sovrn-bid-response-1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "bidResponseId11", - "seatbid": [ - { - "bid": [ - { - "impid": "adUnitCode11", - "price": 5.78, - "adm": "adm11", - "crid": "crid11", - "dealid": "dealId11", - "w": 300, - "h": 250 - } - ], - "seat": "seatId11", - "group": 0 - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/cache/update/test-auction-request.json b/src/test/resources/org/prebid/server/it/cache/update/test-auction-request.json index a22728bd972..c1e52744723 100644 --- a/src/test/resources/org/prebid/server/it/cache/update/test-auction-request.json +++ b/src/test/resources/org/prebid/server/it/cache/update/test-auction-request.json @@ -54,7 +54,7 @@ } } }, - "auctiontimestamp": 1000 + "auctiontimestamp": 0 } }, "regs": { diff --git a/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json b/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json index aed8243dab0..62915475d2a 100644 --- a/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json +++ b/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json @@ -22,7 +22,8 @@ "hb_size_rubicon": "120x600", "hb_bidder_rubicon": "rubicon" } - } + }, + "origbidcpm": 4.26 } }, { @@ -31,7 +32,6 @@ "price": 3, "adm": "adm1", "crid": "crid1", - "dealid": "dealId1", "w": 120, "h": 600, "ext": { @@ -40,12 +40,10 @@ "targeting": { "hb_pb": "3.00", "hb_pb_rubicon": "3.00", - "hb_deal_rubicon": "dealId1", "hb_size": "120x600", "hb_bidder": "rubicon", "hb_size_rubicon": "120x600", - "hb_bidder_rubicon": "rubicon", - "hb_deal": "dealId1" + "hb_bidder_rubicon": "rubicon" }, "storedrequestattributes": { "maxduration": 60, @@ -64,7 +62,8 @@ 1 ] } - } + }, + "origbidcpm": 3 } } ], @@ -78,7 +77,7 @@ "rubicon": "{{ rubicon.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-request1.json b/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-request1.json index 9b06e267cfe..a6e90ad23f1 100644 --- a/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-request1.json +++ b/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-request1.json @@ -31,12 +31,13 @@ "mint": "", "mint_version": "" } - } + }, + "maxbids": 1 } } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { "ext": { diff --git a/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-request2.json b/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-request2.json index 78d5bfb552c..2766113a429 100644 --- a/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-request2.json +++ b/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-request2.json @@ -32,12 +32,13 @@ "mint": "", "mint_version": "" } - } + }, + "maxbids": 1 } } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { "ext": { diff --git a/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-response2.json b/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-response2.json index 562ba1cf4bb..5a25990110b 100644 --- a/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-response2.json +++ b/src/test/resources/org/prebid/server/it/cache/update/test-rubicon-bid-response2.json @@ -9,7 +9,6 @@ "price": 3, "adm": "adm1", "crid": "crid1", - "dealid": "dealId1", "w": 120, "h": 600 } diff --git a/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-first-and-second-response.json b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-first-and-second-response.json new file mode 100644 index 00000000000..08f5756c291 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-first-and-second-response.json @@ -0,0 +1,370 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bidId1", + "impid": "impId1", + "price": 10.6, + "adm": "", + "crid": "crid1", + "dealid": "dealId1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb": "10.60", + "hb_pb_rubicon": "10.60", + "hb_deal_rubicon": "dealId1", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_deal": "dealId1" + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1", + "imp": "http://localhost:8080/event?t=imp&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1" + } + }, + "origbidcpm": 10.6 + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "rubicon": [ + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem1\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId1\",\"impid\":\"impId1\",\"dealid\":\"dealId1\",\"price\":10.6,\"adm\":\"\",\"crid\":\"crid1\",\"w\":300,\"h\":250}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem2\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId2\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId2\",\"impid\":\"impId1\",\"dealid\":\"dealId2\",\"price\":9.6,\"adm\":\"\",\"crid\":\"crid2\",\"w\":300,\"h\":250}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem3\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}" + } + ], + "userservice": [ + { + "uri": "{{ userservice_uri }}", + "requestbody": "{\"time\":\"{{ userservice_time }}\",\"ids\":[{\"type\":\"khaos\",\"id\":\"J5VLCWQP-26-CWFT\"}]}", + "responsebody": "{\"user\":{\"data\":[{\"id\":\"1111\",\"name\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"4444\",\"name\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"fcapIds\":[\"fcapId3\"]}}}", + "status": 200 + } + ] + }, + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com", + "id": "2001" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0" + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ], + "time": { + "userdow": "{{ userdow }}", + "userhour": "{{ userhour }}" + } + } + }, + "at": 1, + "tmax": 2000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "auctiontimestamp": 1000, + "debug": 1, + "trace": "verbose", + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } + }, + "pgmetrics": { + "sent_to_client": [ + "lineItem1" + ], + "sent_to_client_as_top_match": [ + "lineItem1" + ], + "matched_whole_targeting": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "ready_to_serve": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "sent_to_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + }, + "sent_to_bidder_as_top_match": { + "rubicon": [ + "lineItem1" + ] + }, + "received_from_bidder": { + "rubicon": [ + "lineItem1", + "lineItem2" + ] + } + }, + "trace": { + "lineitems": { + "lineItem3": [ + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.044Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.061Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem1": [ + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.048Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem2": [ + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.053Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem2 for bidder rubicon ready to serve. relPriority 3" + } + ] + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-first-bid-only-response.json b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-first-bid-only-response.json new file mode 100644 index 00000000000..bcf04ea8cc1 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-first-bid-only-response.json @@ -0,0 +1,367 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bidId1", + "impid": "impId1", + "price": 10.6, + "adm": "", + "crid": "crid1", + "dealid": "dealId1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb": "10.60", + "hb_pb_rubicon": "10.60", + "hb_deal_rubicon": "dealId1", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_deal": "dealId1" + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1", + "imp": "http://localhost:8080/event?t=imp&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1" + } + }, + "origbidcpm": 10.6 + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "rubicon": [ + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem1\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId1\",\"impid\":\"impId1\",\"dealid\":\"dealId1\",\"price\":10.6,\"adm\":\"\",\"crid\":\"crid1\",\"w\":300,\"h\":250}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem2\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}" + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem3\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}" + } + ], + "userservice": [ + { + "uri": "{{ userservice_uri }}", + "requestbody": "{\"time\":\"{{ userservice_time }}\",\"ids\":[{\"type\":\"khaos\",\"id\":\"J5VLCWQP-26-CWFT\"}]}", + "responsebody": "{\"user\":{\"data\":[{\"id\":\"1111\",\"name\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"4444\",\"name\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"fcapIds\":[\"fcapId3\"]}}}", + "status": 200 + } + ] + }, + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "2001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0" + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ], + "time": { + "userdow": "{{ userdow }}", + "userhour": "{{ userhour }}" + } + } + }, + "at": 1, + "tmax": 2000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "auctiontimestamp": 1000, + "debug": 1, + "trace": "verbose", + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } + }, + "pgmetrics": { + "sent_to_client": [ + "lineItem1" + ], + "sent_to_client_as_top_match": [ + "lineItem1" + ], + "matched_whole_targeting": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "ready_to_serve": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "sent_to_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + }, + "sent_to_bidder_as_top_match": { + "rubicon": [ + "lineItem1" + ] + }, + "received_from_bidder": { + "rubicon": [ + "lineItem1" + ] + } + }, + "trace": { + "lineitems": { + "lineItem3": [ + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.044Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.061Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem1": [ + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.048Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem2": [ + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.053Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem2 for bidder rubicon ready to serve. relPriority 3" + } + ] + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-in-order-response.json b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-in-order-response.json new file mode 100644 index 00000000000..d8a8cb5a423 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-in-order-response.json @@ -0,0 +1,367 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bidId1", + "impid": "impId1", + "price": 10.6, + "adm": "", + "crid": "crid1", + "dealid": "dealId1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb": "10.60", + "hb_pb_rubicon": "10.60", + "hb_deal_rubicon": "dealId1", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_deal": "dealId1" + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1", + "imp": "http://localhost:8080/event?t=imp&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1" + } + }, + "origbidcpm": 10.6 + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "rubicon": [ + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem1\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId1\",\"impid\":\"impId1\",\"dealid\":\"dealId1\",\"price\":10.6,\"adm\":\"\",\"crid\":\"crid1\",\"w\":300,\"h\":250}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem2\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}" + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem3\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}" + } + ], + "userservice": [ + { + "uri": "{{ userservice_uri }}", + "requestbody": "{\"time\":\"{{ userservice_time }}\",\"ids\":[{\"type\":\"khaos\",\"id\":\"J5VLCWQP-26-CWFT\"}]}", + "responsebody": "{\"user\":{\"data\":[{\"id\":\"1111\",\"name\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"4444\",\"name\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"fcapIds\":[\"fcapId3\"]}}}", + "status": 200 + } + ] + }, + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "2001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0" + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ], + "time": { + "userdow": "{{ userdow }}", + "userhour": "{{ userhour }}" + } + } + }, + "at": 1, + "tmax": 2000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "auctiontimestamp": 1000, + "debug": 1, + "trace": "verbose", + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } + }, + "pgmetrics": { + "sent_to_client": [ + "lineItem1" + ], + "sent_to_client_as_top_match": [ + "lineItem1" + ], + "matched_whole_targeting": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "ready_to_serve": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "sent_to_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + }, + "sent_to_bidder_as_top_match": { + "rubicon": [ + "lineItem1" + ] + }, + "received_from_bidder": { + "rubicon": [ + "lineItem1" + ] + } + }, + "trace": { + "lineitems": { + "lineItem3": [ + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.044Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.061Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem1": [ + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.048Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem2": [ + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.053Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem2 for bidder rubicon ready to serve. relPriority 3" + } + ] + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-in-reverse-order-response.json b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-in-reverse-order-response.json new file mode 100644 index 00000000000..8ddad27189e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-in-reverse-order-response.json @@ -0,0 +1,373 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bidId1", + "impid": "impId1", + "price": 10.6, + "adm": "", + "crid": "crid1", + "dealid": "dealId1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb": "10.60", + "hb_pb_rubicon": "10.60", + "hb_deal_rubicon": "dealId1", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_deal": "dealId1" + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1", + "imp": "http://localhost:8080/event?t=imp&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1" + } + }, + "origbidcpm": 10.6 + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "rubicon": [ + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem2\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId2\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId2\",\"impid\":\"impId1\",\"dealid\":\"dealId2\",\"price\":9.6,\"adm\":\"\",\"crid\":\"crid2\",\"w\":300,\"h\":250}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem3\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId3\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId3\",\"impid\":\"impId1\",\"dealid\":\"dealId3\",\"price\":8.6,\"adm\":\"\",\"crid\":\"crid3\",\"w\":300,\"h\":250}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem1\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId1\",\"impid\":\"impId1\",\"dealid\":\"dealId1\",\"price\":10.6,\"adm\":\"\",\"crid\":\"crid1\",\"w\":300,\"h\":250}]}]}", + "status": 200 + } + ], + "userservice": [ + { + "uri": "{{ userservice_uri }}", + "requestbody": "{\"time\":\"{{ userservice_time }}\",\"ids\":[{\"type\":\"khaos\",\"id\":\"J5VLCWQP-26-CWFT\"}]}", + "responsebody": "{\"user\":{\"data\":[{\"id\":\"1111\",\"name\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"4444\",\"name\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"fcapIds\":[\"fcapId3\"]}}}", + "status": 200 + } + ] + }, + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "2001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0" + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ], + "time": { + "userdow": "{{ userdow }}", + "userhour": "{{ userhour }}" + } + } + }, + "at": 1, + "tmax": 2000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "auctiontimestamp": 1000, + "debug": 1, + "trace": "verbose", + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } + }, + "pgmetrics": { + "sent_to_client": [ + "lineItem1" + ], + "sent_to_client_as_top_match": [ + "lineItem1" + ], + "matched_whole_targeting": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "ready_to_serve": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "sent_to_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + }, + "sent_to_bidder_as_top_match": { + "rubicon": [ + "lineItem1" + ] + }, + "received_from_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + } + }, + "trace": { + "lineitems": { + "lineItem3": [ + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:07:36.045Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:07:36.064Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem1": [ + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:07:36.049Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:07:36.064Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem2": [ + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:07:36.056Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:07:36.064Z", + "category": "pacing", + "message": "Matched Line Item lineItem2 for bidder rubicon ready to serve. relPriority 3" + } + ] + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-second-bid-only-response.json b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-second-bid-only-response.json new file mode 100644 index 00000000000..5fa30e8832c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-second-bid-only-response.json @@ -0,0 +1,371 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bidId2", + "impid": "impId1", + "price": 9.6, + "adm": "", + "crid": "crid2", + "dealid": "dealId2", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb": "9.60", + "hb_pb_rubicon": "9.60", + "hb_deal_rubicon": "dealId2", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_deal": "dealId2" + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2", + "imp": "http://localhost:8080/event?t=imp&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2" + } + }, + "origbidcpm": 9.6 + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "rubicon": [ + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem1\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem2\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId2\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId2\",\"impid\":\"impId1\",\"dealid\":\"dealId2\",\"price\":9.6,\"adm\":\"\",\"crid\":\"crid2\",\"w\":300,\"h\":250}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem3\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId3\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[]}]}", + "status": 200 + } + ], + "userservice": [ + { + "uri": "{{ userservice_uri }}", + "requestbody": "{\"time\":\"{{ userservice_time }}\",\"ids\":[{\"type\":\"khaos\",\"id\":\"J5VLCWQP-26-CWFT\"}]}", + "responsebody": "{\"user\":{\"data\":[{\"id\":\"1111\",\"name\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"4444\",\"name\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"fcapIds\":[\"fcapId3\"]}}}", + "status": 200 + } + ] + }, + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "2001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0" + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ], + "time": { + "userdow": "{{ userdow }}", + "userhour": "{{ userhour }}" + } + } + }, + "at": 1, + "tmax": 2000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "auctiontimestamp": 1000, + "debug": 1, + "trace": "verbose", + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } + }, + "pgmetrics": { + "sent_to_client": [ + "lineItem2" + ], + "sent_to_client_as_top_match": [ + "lineItem2" + ], + "matched_whole_targeting": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "ready_to_serve": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "sent_to_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + }, + "sent_to_bidder_as_top_match": { + "rubicon": [ + "lineItem1" + ] + }, + "received_from_bidder": { + "rubicon": [ + "lineItem2" + ] + } + }, + "trace": { + "lineitems": { + "lineItem3": [ + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.044Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.061Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem1": [ + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.048Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem2": [ + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.053Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem2 for bidder rubicon ready to serve. relPriority 3" + } + ] + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-third-and-second-response.json b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-third-and-second-response.json new file mode 100644 index 00000000000..97269f054bb --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-third-and-second-response.json @@ -0,0 +1,372 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bidId2", + "impid": "impId1", + "price": 9.6, + "adm": "", + "crid": "crid2", + "dealid": "dealId2", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb": "9.60", + "hb_pb_rubicon": "9.60", + "hb_deal_rubicon": "dealId2", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_deal": "dealId2" + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2", + "imp": "http://localhost:8080/event?t=imp&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2" + } + }, + "origbidcpm": 9.6 + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "rubicon": [ + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem2\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId2\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId2\",\"impid\":\"impId1\",\"dealid\":\"dealId2\",\"price\":9.6,\"adm\":\"\",\"crid\":\"crid2\",\"w\":300,\"h\":250}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem1\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem3\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId3\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId3\",\"impid\":\"impId1\",\"dealid\":\"dealId3\",\"price\":8.6,\"adm\":\"\",\"crid\":\"crid3\",\"w\":300,\"h\":250}]}]}", + "status": 200 + } + ], + "userservice": [ + { + "uri": "{{ userservice_uri }}", + "requestbody": "{\"time\":\"{{ userservice_time }}\",\"ids\":[{\"type\":\"khaos\",\"id\":\"J5VLCWQP-26-CWFT\"}]}", + "responsebody": "{\"user\":{\"data\":[{\"id\":\"1111\",\"name\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"4444\",\"name\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"fcapIds\":[\"fcapId3\"]}}}", + "status": 200 + } + ] + }, + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "2001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0" + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ], + "time": { + "userdow": "{{ userdow }}", + "userhour": "{{ userhour }}" + } + } + }, + "at": 1, + "tmax": 2000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "auctiontimestamp": 1000, + "debug": 1, + "trace": "verbose", + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } + }, + "pgmetrics": { + "sent_to_client": [ + "lineItem2" + ], + "sent_to_client_as_top_match": [ + "lineItem2" + ], + "matched_whole_targeting": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "ready_to_serve": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "sent_to_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + }, + "sent_to_bidder_as_top_match": { + "rubicon": [ + "lineItem1" + ] + }, + "received_from_bidder": { + "rubicon": [ + "lineItem3", + "lineItem2" + ] + } + }, + "trace": { + "lineitems": { + "lineItem3": [ + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.044Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.061Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem1": [ + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.048Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem2": [ + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.053Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem2 for bidder rubicon ready to serve. relPriority 3" + } + ] + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-third-bid-only-response.json b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-third-bid-only-response.json new file mode 100644 index 00000000000..6454861db30 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/responses/test-auction-third-bid-only-response.json @@ -0,0 +1,371 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bidId3", + "impid": "impId1", + "price": 8.6, + "adm": "", + "crid": "crid3", + "dealid": "dealId3", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb": "8.60", + "hb_pb_rubicon": "8.60", + "hb_deal_rubicon": "dealId3", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_deal": "dealId3" + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId3&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem3", + "imp": "http://localhost:8080/event?t=imp&b=bidId3&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem3" + } + }, + "origbidcpm": 8.6 + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "rubicon": [ + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem1\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem2\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId2\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem3\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId3\",\"seatbid\":[{\"seat\":\"seatId\",\"bid\":[{\"id\":\"bidId3\",\"impid\":\"impId1\",\"dealid\":\"dealId3\",\"price\":8.6,\"adm\":\"\",\"crid\":\"crid3\",\"w\":300,\"h\":250}]}]}", + "status": 200 + } + ], + "userservice": [ + { + "uri": "{{ userservice_uri }}", + "requestbody": "{\"time\":\"{{ userservice_time }}\",\"ids\":[{\"type\":\"khaos\",\"id\":\"J5VLCWQP-26-CWFT\"}]}", + "responsebody": "{\"user\":{\"data\":[{\"id\":\"1111\",\"name\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"4444\",\"name\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"fcapIds\":[\"fcapId3\"]}}}", + "status": 200 + } + ] + }, + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "2001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0" + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ], + "time": { + "userdow": "{{ userdow }}", + "userhour": "{{ userhour }}" + } + } + }, + "at": 1, + "tmax": 2000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "auctiontimestamp": 1000, + "debug": 1, + "trace": "verbose", + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } + }, + "pgmetrics": { + "sent_to_client": [ + "lineItem3" + ], + "sent_to_client_as_top_match": [ + "lineItem3" + ], + "matched_whole_targeting": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "ready_to_serve": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "sent_to_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + }, + "sent_to_bidder_as_top_match": { + "rubicon": [ + "lineItem1" + ] + }, + "received_from_bidder": { + "rubicon": [ + "lineItem3" + ] + } + }, + "trace": { + "lineitems": { + "lineItem3": [ + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.044Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:08:59.061Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem1": [ + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.048Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem2": [ + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.053Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:08:59.062Z", + "category": "pacing", + "message": "Matched Line Item lineItem2 for bidder rubicon ready to serve. relPriority 3" + } + ] + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/premature/test-auction-request.json b/src/test/resources/org/prebid/server/it/deals/premature/test-auction-request.json new file mode 100644 index 00000000000..05add188544 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/test-auction-request.json @@ -0,0 +1,49 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "cur": [ + "USD" + ], + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + } + }, + "site": { + "publisher": { + "id": "2001" + } + }, + "ext": { + "prebid": { + "debug": 1, + "trace": "verbose", + "targeting": { + }, + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/premature/test-planner-plan-response.json b/src/test/resources/org/prebid/server/it/deals/premature/test-planner-plan-response.json new file mode 100644 index 00000000000..d80ece7ad26 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/test-planner-plan-response.json @@ -0,0 +1,209 @@ +[ + { + "lineItemId": "lineItem1", + "extLineItemId": "extLineItem1", + "dealId": "dealId1", + "status": "active", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "accountId": "2001", + "source": "rubicon", + "price": { + "cpm": 10.60, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "{{ lineItem.startTime }}", + "endTimeStamp": "{{ lineItem.endTime }}", + "updatedTimeStamp": "{{ now }}", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId1", + "count": 3, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "{{ now }}", + "startTimeStamp": "{{ plan.startTime }}", + "endTimeStamp": "{{ plan.endTime }}", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + }, + { + "lineItemId": "lineItem2", + "dealId": "dealId2", + "extLineItemId": "extLineItem2", + "status": "active", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "accountId": "2001", + "source": "rubicon", + "price": { + "cpm": 9.60, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "{{ lineItem.startTime }}", + "endTimeStamp": "{{ lineItem.endTime }}", + "updatedTimeStamp": "{{ now }}", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId2", + "count": 3, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "{{ now }}", + "startTimeStamp": "{{ plan.startTime }}", + "endTimeStamp": "{{ plan.endTime }}", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + }, + { + "lineItemId": "lineItem3", + "extLineItemId": "extLineItem3", + "dealId": "dealId3", + "status": "active", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "accountId": "2001", + "source": "rubicon", + "price": { + "cpm": 8.60, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "{{ lineItem.startTime }}", + "endTimeStamp": "{{ lineItem.endTime }}", + "updatedTimeStamp": "{{ now }}", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId1", + "count": 3, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "{{ now }}", + "startTimeStamp": "{{ plan.startTime }}", + "endTimeStamp": "{{ plan.endTime }}", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + } +] diff --git a/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-bid-response-1.json b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-bid-response-1.json new file mode 100644 index 00000000000..9b3e45e74e4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-bid-response-1.json @@ -0,0 +1,20 @@ +{ + "id": "bidResponseId1", + "seatbid": [ + { + "seat": "seatId", + "bid": [ + { + "id": "bidId1", + "impid": "impId1", + "dealid": "dealId1", + "price": 10.60, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-bid-response-2.json b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-bid-response-2.json new file mode 100644 index 00000000000..ba3e19d5887 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-bid-response-2.json @@ -0,0 +1,20 @@ +{ + "id": "bidResponseId2", + "seatbid": [ + { + "seat": "seatId", + "bid": [ + { + "id": "bidId2", + "impid": "impId1", + "dealid": "dealId2", + "price": 9.60, + "adm": "", + "crid": "crid2", + "w": 300, + "h": 250 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-bid-response-3.json b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-bid-response-3.json new file mode 100644 index 00000000000..3583fa59b7f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-bid-response-3.json @@ -0,0 +1,20 @@ +{ + "id": "bidResponseId3", + "seatbid": [ + { + "seat": "seatId", + "bid": [ + { + "id": "bidId3", + "impid": "impId1", + "dealid": "dealId3", + "price": 8.60, + "adm": "", + "crid": "crid3", + "w": 300, + "h": 250 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-no-bid-response-1.json b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-no-bid-response-1.json new file mode 100644 index 00000000000..533d6537f8f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-no-bid-response-1.json @@ -0,0 +1,11 @@ +{ + "id": "bidResponseId1", + "seatbid": [ + { + "seat": "seatId", + "bid": [ + + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-no-bid-response-2.json b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-no-bid-response-2.json new file mode 100644 index 00000000000..b3c9444dd24 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-no-bid-response-2.json @@ -0,0 +1,11 @@ +{ + "id": "bidResponseId2", + "seatbid": [ + { + "seat": "seatId", + "bid": [ + + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-no-bid-response-3.json b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-no-bid-response-3.json new file mode 100644 index 00000000000..5363e36c8f9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/premature/test-rubicon-no-bid-response-3.json @@ -0,0 +1,10 @@ +{ + "id": "bidResponseId3", + "seatbid": [ + { + "seat": "seatId", + "bid": [ + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/simulation/test-auction-request.json b/src/test/resources/org/prebid/server/it/deals/simulation/test-auction-request.json new file mode 100644 index 00000000000..3f0d5365313 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/simulation/test-auction-request.json @@ -0,0 +1,49 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "cur": [ + "USD" + ], + "site": { + "publisher": { + "id": "2001" + } + }, + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + } + }, + "ext": { + "prebid": { + "debug": 1, + "trace": "verbose", + "targeting": { + }, + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/simulation/test-auction-response-1.json b/src/test/resources/org/prebid/server/it/deals/simulation/test-auction-response-1.json new file mode 100644 index 00000000000..4d5dba4ab7f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/simulation/test-auction-response-1.json @@ -0,0 +1,272 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "impId1-lineItem1", + "impid": "impId1", + "price": 10.0, + "adm": "", + "crid": "crid", + "dealid": "dealId1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb": "10.00", + "hb_pb_rubicon": "10.00", + "hb_deal_rubicon": "dealId1", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_deal": "dealId1" + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=impId1-lineItem1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1", + "imp": "http://localhost:8080/event?t=imp&b=impId1-lineItem1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1" + } + }, + "origbidcpm": 10.0, + "origbidcur": "USD" + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "rubicon": [] + }, + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "2001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0" + }, + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [], + "time": { + "userdow": "{{ userdow }}", + "userhour": "{{ userhour }}" + } + } + }, + "at": 1, + "tmax": 2000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "debug": 1, + "trace": "verbose", + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "auctiontimestamp": 1000, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } + }, + "pgmetrics": { + "sent_to_client": [ + "lineItem1" + ], + "sent_to_client_as_top_match": [ + "lineItem1" + ], + "matched_whole_targeting": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "ready_to_serve": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "sent_to_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + }, + "sent_to_bidder_as_top_match": { + "rubicon": [ + "lineItem3" + ] + }, + "received_from_bidder": { + "rubicon": [ + "lineItem1", + "lineItem2" + ] + } + }, + "trace": { + "lineitems": { + "lineItem3": [ + { + "lineitemid": "lineItem3", + "time": "2020-04-24T13:54:17.715Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-24T13:54:17.733Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem1": [ + { + "lineitemid": "lineItem1", + "time": "2020-04-24T13:54:17.72Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-24T13:54:17.734Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem2": [ + { + "lineitemid": "lineItem2", + "time": "2020-04-24T13:54:17.725Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-24T13:54:17.734Z", + "category": "pacing", + "message": "Matched Line Item lineItem2 for bidder rubicon ready to serve. relPriority 3" + } + ] + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/simulation/test-auction-response-2.json b/src/test/resources/org/prebid/server/it/deals/simulation/test-auction-response-2.json new file mode 100644 index 00000000000..ceffca54759 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/simulation/test-auction-response-2.json @@ -0,0 +1,255 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "impId1-lineItem2", + "impid": "impId1", + "price": 7.0, + "adm": "", + "crid": "crid", + "dealid": "dealId2", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_pb": "7.00", + "hb_pb_rubicon": "7.00", + "hb_deal_rubicon": "dealId2", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_deal": "dealId2" + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=impId1-lineItem2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2", + "imp": "http://localhost:8080/event?t=imp&b=impId1-lineItem2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2" + } + }, + "origbidcpm": 7.0, + "origbidcur": "USD" + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "rubicon": [] + }, + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "2001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0" + }, + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [], + "time": { + "userdow": "{{ userdow }}", + "userhour": "{{ userhour }}" + } + } + }, + "at": 1, + "tmax": 2000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "debug": 1, + "trace": "verbose", + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "auctiontimestamp": 1000, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } + }, + "pgmetrics": { + "sent_to_client": [ + "lineItem2" + ], + "sent_to_client_as_top_match": [ + "lineItem2" + ], + "matched_whole_targeting": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "ready_to_serve": [ + "lineItem3", + "lineItem2" + ], + "pacing_deferred": [ + "lineItem1" + ], + "sent_to_bidder": { + "rubicon": [ + "lineItem3", + "lineItem2" + ] + }, + "sent_to_bidder_as_top_match": { + "rubicon": [ + "lineItem3" + ] + }, + "received_from_bidder": { + "rubicon": [ + "lineItem2" + ] + } + }, + "trace": { + "lineitems": { + "lineItem3": [ + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:03:17.986Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-24T14:03:18.002Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem1": [ + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:03:17.993Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-24T14:03:18.002Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon not ready to serve. Will be ready at never, current time is 2019-10-10T00:16:00.000Z" + } + ], + "lineItem2": [ + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:03:18.001Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-24T14:03:18.003Z", + "category": "pacing", + "message": "Matched Line Item lineItem2 for bidder rubicon ready to serve. relPriority 3" + } + ] + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/simulation/test-bid-rates.json b/src/test/resources/org/prebid/server/it/deals/simulation/test-bid-rates.json new file mode 100644 index 00000000000..fe32f4a040e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/simulation/test-bid-rates.json @@ -0,0 +1,5 @@ +{ + "lineItem1": 1.00, + "lineItem2": 1.00, + "lineItem3": 0.00 +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/simulation/test-delivery-stats-progress-request-1.json b/src/test/resources/org/prebid/server/it/deals/simulation/test-delivery-stats-progress-request-1.json new file mode 100644 index 00000000000..07187887734 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/simulation/test-delivery-stats-progress-request-1.json @@ -0,0 +1,149 @@ +{ + "reportTimeStamp": "2019-10-10T00:00:03.000Z", + "dataWindowStartTimeStamp": "2019-10-10T00:00:02.000Z", + "dataWindowEndTimeStamp": "2019-10-10T00:00:03.000Z", + "instanceId": "localhost", + "vendor": "local", + "region": "local", + "clientAuctions": 1, + "lineItemStatus": [ + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem2", + "dealId": "dealId2", + "extLineItemId": "extLineItem2", + "accountAuctions": 1, + "domainMatched": 0, + "targetMatched": 1, + "targetMatchedButFcapped": 0, + "targetMatchedButFcapLookupFailed": 0, + "pacingDeferred": 0, + "sentToBidder": 1, + "sentToBidderAsTopMatch": 0, + "receivedFromBidder": 1, + "receivedFromBidderInvalidated": 0, + "sentToClient": 0, + "sentToClientAsTopMatch": 0, + "lostToLineItems": [ + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem3", + "count": 1 + }, + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem1", + "count": 1 + } + ], + "events": [], + "deliverySchedule": [ + { + "planId": "1", + "planStartTimeStamp": "2019-10-10T00:00:00.000Z", + "planExpirationTimeStamp": "2019-10-10T00:10:00.000Z", + "planUpdatedTimeStamp": "2019-10-10T00:00:00.000Z", + "tokens": [ + { + "class": 1, + "total": 5000, + "spent": 0 + }, + { + "class": 2, + "total": 125, + "spent": 0 + } + ] + } + ] + }, + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem3", + "dealId": "dealId3", + "extLineItemId": "extLineItem3", + "accountAuctions": 1, + "domainMatched": 0, + "targetMatched": 1, + "targetMatchedButFcapped": 0, + "targetMatchedButFcapLookupFailed": 0, + "pacingDeferred": 0, + "sentToBidder": 1, + "sentToBidderAsTopMatch": 1, + "receivedFromBidder": 0, + "receivedFromBidderInvalidated": 0, + "sentToClient": 0, + "sentToClientAsTopMatch": 0, + "events": [], + "deliverySchedule": [ + { + "planId": "1", + "planStartTimeStamp": "2019-10-10T00:00:00.000Z", + "planExpirationTimeStamp": "2019-10-10T00:10:00.000Z", + "planUpdatedTimeStamp": "2019-10-10T00:00:00.000Z", + "tokens": [ + { + "class": 1, + "total": 5000, + "spent": 0 + }, + { + "class": 2, + "total": 125, + "spent": 0 + } + ] + } + ] + }, + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem1", + "dealId": "dealId1", + "extLineItemId": "extLineItem1", + "accountAuctions": 1, + "domainMatched": 0, + "targetMatched": 1, + "targetMatchedButFcapped": 0, + "targetMatchedButFcapLookupFailed": 0, + "pacingDeferred": 0, + "sentToBidder": 1, + "sentToBidderAsTopMatch": 0, + "receivedFromBidder": 1, + "receivedFromBidderInvalidated": 0, + "sentToClient": 1, + "sentToClientAsTopMatch": 1, + "lostToLineItems": [ + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem3", + "count": 1 + } + ], + "events": [], + "deliverySchedule": [ + { + "planId": "1", + "planStartTimeStamp": "2019-10-10T00:00:00.000Z", + "planExpirationTimeStamp": "2019-10-10T00:10:00.000Z", + "planUpdatedTimeStamp": "2019-10-10T00:00:00.000Z", + "tokens": [ + { + "class": 2, + "total": 125, + "spent": 0, + "totalSpent": 0 + }, + { + "class": 1, + "total": 5000, + "spent": 1, + "totalSpent": 1 + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/simulation/test-delivery-stats-progress-request-2.json b/src/test/resources/org/prebid/server/it/deals/simulation/test-delivery-stats-progress-request-2.json new file mode 100644 index 00000000000..ab3a570202f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/simulation/test-delivery-stats-progress-request-2.json @@ -0,0 +1,98 @@ +{ + "reportTimeStamp": "2019-10-10T00:17:00.000Z", + "dataWindowStartTimeStamp": "2019-10-10T00:00:03.000Z", + "dataWindowEndTimeStamp": "2019-10-10T00:17:00.000Z", + "instanceId": "localhost", + "vendor": "local", + "region": "local", + "clientAuctions": 1, + "lineItemStatus": [ + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem2", + "dealId": "dealId2", + "extLineItemId": "extLineItem2", + "accountAuctions": 1, + "domainMatched": 0, + "targetMatched": 1, + "targetMatchedButFcapped": 0, + "targetMatchedButFcapLookupFailed": 0, + "pacingDeferred": 0, + "sentToBidder": 1, + "sentToBidderAsTopMatch": 0, + "receivedFromBidder": 1, + "receivedFromBidderInvalidated": 0, + "sentToClient": 1, + "sentToClientAsTopMatch": 1, + "lostToLineItems": [ + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem3", + "count": 1 + } + ], + "events": [], + "deliverySchedule": [ + { + "planId": "2", + "planStartTimeStamp": "2019-10-10T00:10:00.000Z", + "planExpirationTimeStamp": "2019-10-10T00:20:00.000Z", + "planUpdatedTimeStamp": "2019-10-10T00:10:00.000Z", + "tokens": [ + { + "class": 2, + "total": 125, + "spent": 0, + "totalSpent": 0 + }, + { + "class": 1, + "total": 5000, + "spent": 1, + "totalSpent": 1 + } + ] + } + ] + }, + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem3", + "dealId": "dealId3", + "extLineItemId": "extLineItem3", + "accountAuctions": 1, + "domainMatched": 0, + "targetMatched": 1, + "targetMatchedButFcapped": 0, + "targetMatchedButFcapLookupFailed": 0, + "pacingDeferred": 0, + "sentToBidder": 1, + "sentToBidderAsTopMatch": 1, + "receivedFromBidder": 0, + "receivedFromBidderInvalidated": 0, + "sentToClient": 0, + "sentToClientAsTopMatch": 0, + "events": [], + "deliverySchedule": [ + { + "planId": "2", + "planStartTimeStamp": "2019-10-10T00:10:00.000Z", + "planExpirationTimeStamp": "2019-10-10T00:20:00.000Z", + "planUpdatedTimeStamp": "2019-10-10T00:10:00.000Z", + "tokens": [ + { + "class": 2, + "total": 125, + "spent": 0 + }, + { + "class": 1, + "total": 5000, + "spent": 0 + } + ] + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/deals/simulation/test-planner-plan-response-1.json b/src/test/resources/org/prebid/server/it/deals/simulation/test-planner-plan-response-1.json new file mode 100644 index 00000000000..a88998a9ee1 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/simulation/test-planner-plan-response-1.json @@ -0,0 +1,257 @@ +[ + { + "lineItemId": "lineItem1", + "extLineItemId": "extLineItem1", + "dealId": "dealId1", + "status": "active", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "accountId": "2001", + "source": "rubicon", + "price": { + "cpm": 10.00, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "2019-10-10T00:00:00Z", + "endTimeStamp": "2019-10-10T00:20:00Z", + "updatedTimeStamp": "2019-10-10T00:00:00Z", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId1", + "count": 3, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "2019-10-10T00:00:00Z", + "startTimeStamp": "2019-10-10T00:00:00Z", + "endTimeStamp": "2019-10-10T00:10:00Z", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + }, + { + "planId": "2", + "updatedTimeStamp": "2019-10-10T00:10:00Z", + "startTimeStamp": "2019-10-10T00:20:00Z", + "endTimeStamp": "2019-10-10T00:30:00Z", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + }, + { + "lineItemId": "lineItem2", + "dealId": "dealId2", + "extLineItemId": "extLineItem2", + "status": "active", + "sizes": [ + { + "w": 320, + "h": 320 + } + ], + "accountId": "2001", + "source": "rubicon", + "price": { + "cpm": 7.00, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "2019-10-10T00:00:00Z", + "endTimeStamp": "2019-10-10T00:20:00Z", + "updatedTimeStamp": "2019-10-10T00:00:00Z", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId2", + "count": 3, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "2019-10-10T00:00:00Z", + "startTimeStamp": "2019-10-10T00:00:00Z", + "endTimeStamp": "2019-10-10T00:10:00Z", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + }, + { + "planId": "2", + "updatedTimeStamp": "2019-10-10T00:10:00Z", + "startTimeStamp": "2019-10-10T00:10:00Z", + "endTimeStamp": "2019-10-10T00:20:00Z", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + }, + { + "lineItemId": "lineItem3", + "extLineItemId": "extLineItem3", + "dealId": "dealId3", + "status": "active", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "accountId": "2001", + "source": "rubicon", + "price": { + "cpm": 15.00, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "2019-10-10T00:00:00Z", + "endTimeStamp": "2019-10-10T00:20:00Z", + "updatedTimeStamp": "2019-10-10T00:00:00Z", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId1", + "count": 3, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "2019-10-10T00:00:00Z", + "startTimeStamp": "2019-10-10T00:00:00Z", + "endTimeStamp": "2019-10-10T00:10:00Z", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + }, + { + "planId": "2", + "updatedTimeStamp": "2019-10-10T00:10:00Z", + "startTimeStamp": "2019-10-10T00:10:00Z", + "endTimeStamp": "2019-10-10T00:20:00Z", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + } +] diff --git a/src/test/resources/org/prebid/server/it/deals/simulation/test-planner-register-request-1.json b/src/test/resources/org/prebid/server/it/deals/simulation/test-planner-register-request-1.json new file mode 100644 index 00000000000..c8aa1a6e300 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/simulation/test-planner-register-request-1.json @@ -0,0 +1,6 @@ +{ + "healthIndex": 0.43, + "hostInstanceId": "localhost", + "region": "local", + "vendor": "local" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/test-auction-request.json b/src/test/resources/org/prebid/server/it/deals/test-auction-request.json new file mode 100644 index 00000000000..0807172afc8 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-auction-request.json @@ -0,0 +1,122 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + }, + { + "id": "impId2", + "banner": { + "format": [ + { + "w": 320, + "h": 320 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + }, + { + "id": "impId3", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001, + "dealsonly": true + } + } + } + } + }, + { + "id": "impId4", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "cur": [ + "USD" + ], + "site": { + "publisher": { + "id": "2001" + } + }, + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + } + }, + "ext": { + "prebid": { + "debug": 1, + "trace": "verbose", + "targeting": { + }, + "cache": { + "bids": {}, + "vastxml": { + "ttlseconds": 120 + } + }, + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-auction-response.json b/src/test/resources/org/prebid/server/it/deals/test-auction-response.json new file mode 100644 index 00000000000..bd5469e8fc1 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-auction-response.json @@ -0,0 +1,727 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bidId1", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "dealid": "dealId1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_deal_rubicon": "dealId1", + "hb_cache_id": "3f37ff80-b395-449f-a80e-c21a38bb6eec", + "hb_cache_path_rubicon": "/cache", + "hb_cache_host_rubicon": "{{ cache.host }}", + "hb_pb": "8.40", + "hb_pb_rubicon": "8.40", + "hb_cache_id_rubicon": "3f37ff80-b395-449f-a80e-c21a38bb6eec", + "hb_cache_path": "/cache", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_cache_host": "{{ cache.host }}", + "hb_deal": "dealId1" + }, + "cache": { + "bids": { + "url": "{{ cache.resource_url }}3f37ff80-b395-449f-a80e-c21a38bb6eec", + "cacheId": "3f37ff80-b395-449f-a80e-c21a38bb6eec" + } + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1", + "imp": "http://localhost:8080/event?t=imp&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1" + } + }, + "origbidcpm": 8.43 + } + }, + { + "id": "bidId2", + "impid": "impId2", + "price": 8.43, + "adm": "", + "crid": "crid2", + "dealid": "dealId2", + "w": 320, + "h": 320, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_deal_rubicon": "dealId2", + "hb_size_rubicon": "320x320", + "hb_cache_id": "13e78248-7339-4e35-b297-874d0031bc1a", + "hb_cache_path_rubicon": "/cache", + "hb_cache_host_rubicon": "{{ cache.host }}", + "hb_pb": "8.40", + "hb_pb_rubicon": "8.40", + "hb_cache_id_rubicon": "13e78248-7339-4e35-b297-874d0031bc1a", + "hb_cache_path": "/cache", + "hb_size": "320x320", + "hb_bidder": "rubicon", + "hb_bidder_rubicon": "rubicon", + "hb_cache_host": "{{ cache.host }}", + "hb_deal": "dealId2" + }, + "cache": { + "bids": { + "url": "{{ cache.resource_url }}13e78248-7339-4e35-b297-874d0031bc1a", + "cacheId": "13e78248-7339-4e35-b297-874d0031bc1a" + } + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2", + "imp": "http://localhost:8080/event?t=imp&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2" + } + }, + "origbidcpm": 8.43 + } + }, + { + "id": "bidId4", + "impid": "impId4", + "price": 8.43, + "adm": "", + "crid": "crid1", + "dealid": "dealId3", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_deal_rubicon": "dealId3", + "hb_size_rubicon": "300x250", + "hb_cache_id": "1cdcf339-e59b-4e2c-8382-1fb845d4961c", + "hb_cache_path_rubicon": "/cache", + "hb_cache_host_rubicon": "{{ cache.host }}", + "hb_pb": "8.40", + "hb_pb_rubicon": "8.40", + "hb_cache_id_rubicon": "1cdcf339-e59b-4e2c-8382-1fb845d4961c", + "hb_cache_path": "/cache", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_bidder_rubicon": "rubicon", + "hb_cache_host": "{{ cache.host }}", + "hb_deal": "dealId3" + }, + "cache": { + "bids": { + "url": "{{ cache.resource_url }}1cdcf339-e59b-4e2c-8382-1fb845d4961c", + "cacheId": "1cdcf339-e59b-4e2c-8382-1fb845d4961c" + } + }, + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId4&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem3", + "imp": "http://localhost:8080/event?t=imp&b=bidId4&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem3" + } + }, + "origbidcpm": 8.43 + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "httpcalls": { + "cache": [ + { + "uri": "{{ cache.endpoint }}", + "requestheaders": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"bidId1\",\"impid\":\"impId1\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid1\",\"dealid\":\"dealId1\",\"w\":300,\"h\":250,\"ext\":{\"origbidcpm\":8.43,\"prebid\":{\"type\":\"banner\",\"events\":{\"win\":\"http://localhost:8080/event?t=win&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1\",\"imp\":\"http://localhost:8080/event?t=imp&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1\"}}},\"wurl\":\"http://localhost:8080/event?t=win&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1\"},\"aid\":\"tid\"},{\"type\":\"json\",\"value\":{\"id\":\"bidId4\",\"impid\":\"impId4\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid1\",\"dealid\":\"dealId3\",\"w\":300,\"h\":250,\"ext\":{\"origbidcpm\":8.43,\"prebid\":{\"type\":\"banner\",\"events\":{\"win\":\"http://localhost:8080/event?t=win&b=bidId4&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem3\",\"imp\":\"http://localhost:8080/event?t=imp&b=bidId4&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem3\"}}},\"wurl\":\"http://localhost:8080/event?t=win&b=bidId4&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem3\"},\"aid\":\"tid\"},{\"type\":\"json\",\"value\":{\"id\":\"bidId2\",\"impid\":\"impId2\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid2\",\"dealid\":\"dealId2\",\"w\":320,\"h\":320,\"ext\":{\"origbidcpm\":8.43,\"prebid\":{\"type\":\"banner\",\"events\":{\"win\":\"http://localhost:8080/event?t=win&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2\",\"imp\":\"http://localhost:8080/event?t=imp&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2\"}}},\"wurl\":\"http://localhost:8080/event?t=win&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2\"},\"aid\":\"tid\"}]}", + "responsebody": "{\"responses\":[{\"uuid\":\"13e78248-7339-4e35-b297-874d0031bc1a\"},{\"uuid\":\"3f37ff80-b395-449f-a80e-c21a38bb6eec\"},{\"uuid\":\"1cdcf339-e59b-4e2c-8382-1fb845d4961c\"}]}", + "status": 200 + } + ], + "rubicon": [ + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId4\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem3\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId4\",\"seatbid\":[{\"seat\":\"seatId4\",\"bid\":[{\"id\":\"bidId4\",\"impid\":\"impId4\",\"dealid\":\"dealId3\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid1\",\"w\":300,\"h\":250}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem1\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"seat\":\"seatId1\",\"bid\":[{\"id\":\"bidId1\",\"impid\":\"impId1\",\"dealid\":\"dealId1\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid1\",\"w\":300,\"h\":250}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId2\",\"banner\":{\"format\":[{\"w\":320,\"h\":320}],\"ext\":{\"rp\":{\"size_id\":72,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId2\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem2\",\"extlineitemid\":\"extLineItem2\",\"sizes\":[{\"w\":320,\"h\":320}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem2\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId2\",\"seatbid\":[{\"seat\":\"seatId2\",\"bid\":[{\"id\":\"bidId2\",\"impid\":\"impId2\",\"dealid\":\"dealId2\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid2\",\"w\":320,\"h\":320}]}]}", + "status": 200 + }, + { + "uri": "{{ rubicon.exchange_uri }}?tk_xint=rp-pbs", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"ext\":{\"rp\":{\"size_id\":15,\"mime\":\"text/html\"}}},\"pmp\":{\"private_auction\":0,\"deals\":[{\"id\":\"dealId1\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem1\",\"extlineitemid\":\"extLineItem1\",\"sizes\":[{\"w\":300,\"h\":250}]}}},{\"id\":\"dealId3\",\"bidfloor\":0.0,\"ext\":{\"line\":{\"lineitemid\":\"lineItem3\",\"extlineitemid\":\"extLineItem3\",\"sizes\":[{\"w\":300,\"h\":250}]}}}]},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"page\":[\"http://www.example.com\"],\"line_item\":\"extLineItem3\"},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"ip\":\"185.199.110.0\",\"ext\":{\"rp\":{}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"id\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"fcapids\":[\"fcapId3\"],\"time\":{\"userdow\":{{ userdow }},\"userhour\":{{ userhour }}}}},\"at\":1,\"tmax\":2000}", + "responsebody": "{\"id\":\"bidResponseId3\",\"seatbid\":[{\"seat\":\"seatId3\",\"bid\":[{\"id\":\"bidId3\",\"impid\":\"impId1\",\"dealid\":\"dealId3\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid3\",\"w\":300,\"h\":250}]}]}", + "status": 200 + } + ], + "userservice": [ + { + "uri": "{{ userservice_uri }}", + "requestbody": "{\"time\":\"{{ userservice_time }}\",\"ids\":[{\"type\":\"khaos\",\"id\":\"J5VLCWQP-26-CWFT\"}]}", + "responsebody": "{\"user\":{\"data\":[{\"id\":\"1111\",\"name\":\"rubicon\",\"segment\":[{\"id\":\"2222\"},{\"id\":\"3333\"}]},{\"id\":\"4444\",\"name\":\"bluekai\",\"segment\":[{\"id\":\"5555\"},{\"id\":\"6666\"}]}],\"ext\":{\"fcapIds\":[\"fcapId3\"]}}}", + "status": 200 + } + ] + }, + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + }, + { + "id": "impId2", + "banner": { + "format": [ + { + "w": 320, + "h": 320 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "sizes": [ + { + "w": 320, + "h": 320 + } + ], + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + }, + { + "id": "impId3", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001, + "dealsonly": true + } + } + } + } + }, + { + "id": "impId4", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "bidder": "rubicon" + } + } + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "2001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0" + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ], + "time": { + "userdow": "{{ userdow }}", + "userhour": "{{ userhour }}" + } + } + }, + "at": 1, + "tmax": 2000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "debug": 1, + "trace": "verbose", + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "cache": { + "bids": {}, + "vastxml": { + "ttlseconds": 120 + } + }, + "auctiontimestamp": 1000, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } + }, + "pgmetrics": { + "sent_to_client": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "sent_to_client_as_top_match": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "matched_whole_targeting": [ + "lineItem3", + "lineItem4", + "lineItem1", + "lineItem2" + ], + "matched_targeting_fcapped": [ + "lineItem4" + ], + "ready_to_serve": [ + "lineItem3", + "lineItem1", + "lineItem2" + ], + "sent_to_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + }, + "sent_to_bidder_as_top_match": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + }, + "received_from_bidder": { + "rubicon": [ + "lineItem3", + "lineItem1", + "lineItem2" + ] + } + }, + "trace": { + "deals": [ + { + "time": "2020-04-27T13:08:04.049Z", + "category": "cleanup", + "message": "No Line Items from bidders rubicon matching imp with id impId3 and ready to serve. Removing imp from requests to all bidders because dealsonly flag is on for these bidders and no other valid bidders in imp." + } + ], + "lineitems": { + "lineItem3": [ + { + "lineitemid": "lineItem3", + "time": "2020-04-27T13:08:03.963Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-27T13:08:03.981Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-27T13:08:03.992Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting did not match imp with id impId2" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-27T13:08:04.009Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting did not match imp with id impId3" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-27T13:08:04.029Z", + "category": "targeting", + "message": "Line Item lineItem3 targeting matched imp with id impId4" + }, + { + "lineitemid": "lineItem3", + "time": "2020-04-27T13:08:04.043Z", + "category": "pacing", + "message": "Matched Line Item lineItem3 for bidder rubicon ready to serve. relPriority 3" + } + ], + "lineItem4": [ + { + "lineitemid": "lineItem4", + "time": "2020-04-27T13:08:03.967Z", + "category": "targeting", + "message": "Line Item lineItem4 targeting did not match imp with id impId1" + }, + { + "lineitemid": "lineItem4", + "time": "2020-04-27T13:08:03.995Z", + "category": "targeting", + "message": "Line Item lineItem4 targeting did not match imp with id impId2" + }, + { + "lineitemid": "lineItem4", + "time": "2020-04-27T13:08:04.014Z", + "category": "targeting", + "message": "Line Item lineItem4 targeting matched imp with id impId3" + }, + { + "lineitemid": "lineItem4", + "time": "2020-04-27T13:08:04.024Z", + "category": "pacing", + "message": "Matched Line Item lineItem4 for bidder rubicon is frequency capped by fcap id fcapId3." + }, + { + "lineitemid": "lineItem4", + "time": "2020-04-27T13:08:04.033Z", + "category": "targeting", + "message": "Line Item lineItem4 targeting did not match imp with id impId4" + } + ], + "lineItem1": [ + { + "lineitemid": "lineItem1", + "time": "2020-04-27T13:08:03.971Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId1" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-27T13:08:03.981Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon ready to serve. relPriority 3" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-27T13:08:03.999Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting did not match imp with id impId2" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-27T13:08:04.019Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting did not match imp with id impId3" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-27T13:08:04.038Z", + "category": "targeting", + "message": "Line Item lineItem1 targeting matched imp with id impId4" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-27T13:08:04.043Z", + "category": "pacing", + "message": "Matched Line Item lineItem1 for bidder rubicon ready to serve. relPriority 3" + }, + { + "lineitemid": "lineItem1", + "time": "2020-04-27T13:08:04.044Z", + "category": "cleanup", + "message": "LineItem lineItem1 was dropped from imp with id impId4 because it was top match in another imp" + } + ], + "lineItem2": [ + { + "lineitemid": "lineItem2", + "time": "2020-04-27T13:08:03.975Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting did not match imp with id impId1" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-27T13:08:04.004Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting matched imp with id impId2" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-27T13:08:04.004Z", + "category": "pacing", + "message": "Matched Line Item lineItem2 for bidder rubicon ready to serve. relPriority 3" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-27T13:08:04.024Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting did not match imp with id impId3" + }, + { + "lineitemid": "lineItem2", + "time": "2020-04-27T13:08:04.043Z", + "category": "targeting", + "message": "Line Item lineItem2 targeting did not match imp with id impId4" + } + ] + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}", + "cache": "{{ cache.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-cache-deals-request.json b/src/test/resources/org/prebid/server/it/deals/test-cache-deals-request.json new file mode 100644 index 00000000000..fc6432dc38f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-cache-deals-request.json @@ -0,0 +1,79 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "bidId4", + "impid": "impId4", + "price": 8.43, + "adm": "", + "crid": "crid1", + "dealid": "dealId3", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 8.43, + "prebid": { + "type": "banner", + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId4&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem3", + "imp": "http://localhost:8080/event?t=imp&b=bidId4&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem3" + } + } + }, + "wurl": "http://localhost:8080/event?t=win&b=bidId4&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem3" + }, + "aid": "tid" + }, + { + "type": "json", + "value": { + "id": "bidId2", + "impid": "impId2", + "price": 8.43, + "adm": "", + "crid": "crid2", + "dealid": "dealId2", + "w": 320, + "h": 320, + "ext": { + "origbidcpm": 8.43, + "prebid": { + "type": "banner", + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2", + "imp": "http://localhost:8080/event?t=imp&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2" + } + } + }, + "wurl": "http://localhost:8080/event?t=win&b=bidId2&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem2" + }, + "aid": "tid" + }, + { + "type": "json", + "value": { + "id": "bidId1", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "dealid": "dealId1", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 8.43, + "prebid": { + "type": "banner", + "events": { + "win": "http://localhost:8080/event?t=win&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1", + "imp": "http://localhost:8080/event?t=imp&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1" + } + } + }, + "wurl": "http://localhost:8080/event?t=win&b=bidId1&a=2001&aid=tid&ts=1000&bidder=rubicon&f=i&int=&x=0&l=lineItem1" + }, + "aid": "tid" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/test-cache-matcher.json b/src/test/resources/org/prebid/server/it/deals/test-cache-matcher.json new file mode 100644 index 00000000000..6ce206a9f25 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-cache-matcher.json @@ -0,0 +1,5 @@ +{ + "bidId2@8.43": "13e78248-7339-4e35-b297-874d0031bc1a", + "bidId1@8.43": "3f37ff80-b395-449f-a80e-c21a38bb6eec", + "bidId4@8.43": "1cdcf339-e59b-4e2c-8382-1fb845d4961c" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/test-deals-application.properties b/src/test/resources/org/prebid/server/it/deals/test-deals-application.properties new file mode 100644 index 00000000000..d03471de59e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-deals-application.properties @@ -0,0 +1,46 @@ +http.port=8070 +admin.port=9060 +hostname=localhost +auction.ad-server-currency=USD +admin-endpoints.currency-rates.enabled=true +admin-endpoints.currency-rates.on-application-port=true +admin-endpoints.currency-rates.protected=false +admin-endpoints.e2eadmin.enabled=true +admin-endpoints.e2eadmin.on-application-port=true +admin-endpoints.e2eadmin.protected=false +admin-endpoints.lineitem-status.enabled=true +admin-endpoints.lineitem-status.on-application-port=true +admin-endpoints.lineitem-status.protected=false +admin-endpoints.deals-status.enabled=true +admin-endpoints.deals-status.on-application-port=true +admin-endpoints.deals-status.protected=false +deals.enabled=true +deals.planner.plan-endpoint=http://localhost:8090/planner-plan +deals.planner.register-endpoint=http://localhost:8090/planner-register +deals.planner.username=username +deals.planner.password=password +deals.planner.update-period="*/1 * * * * *" +deals.delivery-stats.endpoint=http://localhost:8090/delivery-stats-progress +deals.delivery-stats.delivery-period="*/5 * * * * *" +deals.delivery-stats.timeout-ms=1000 +deals.delivery-stats.username=username +deals.delivery-stats.password=password +deals.delivery-stats.reports-interval-ms=0 +deals.delivery-progress.report-reset-period="*/5 * * * * *" +deals.user-data.user-details-endpoint=http://localhost:8090/user-data-details +deals.user-data.win-event-endpoint=http://localhost:8090/user-data-win-event +deals.user-data.timeout=1000 +deals.user-data.user-ids[0].type=khaos +deals.user-data.user-ids[0].source=uid +deals.user-data.user-ids[0].location=rubicon +deals.max-deals-per-bidder=3 +deals.alert-proxy.enabled=false +deals.alert-proxy.url=http://localhost +deals.alert-proxy.timeout-sec=5 +deals.alert-proxy.username=username +deals.alert-proxy.password=password +profile=test +infra=vm +data-center=aws +system=PBS +sub-system=Prebid Server diff --git a/src/test/resources/org/prebid/server/it/deals/test-deals-simulation-application.properties b/src/test/resources/org/prebid/server/it/deals/test-deals-simulation-application.properties new file mode 100644 index 00000000000..7f7ad27e492 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-deals-simulation-application.properties @@ -0,0 +1,3 @@ +http.port=10080 +admin.port=10060 +deals.simulation.enabled=true diff --git a/src/test/resources/org/prebid/server/it/deals/test-delivery-stats-progress-request-1.json b/src/test/resources/org/prebid/server/it/deals/test-delivery-stats-progress-request-1.json new file mode 100644 index 00000000000..d7ef6aa340d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-delivery-stats-progress-request-1.json @@ -0,0 +1,203 @@ +{ + "instanceId": "localhost", + "vendor": "local", + "region": "local", + "clientAuctions": 1, + "lineItemStatus": [ + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem4", + "dealId": "dealId4", + "extLineItemId": "extLineItem4", + "accountAuctions": 1, + "domainMatched": 0, + "targetMatched": 1, + "targetMatchedButFcapped": 1, + "targetMatchedButFcapLookupFailed": 0, + "pacingDeferred": 0, + "sentToBidder": 0, + "sentToBidderAsTopMatch": 0, + "receivedFromBidder": 0, + "receivedFromBidderInvalidated": 0, + "sentToClient": 0, + "sentToClientAsTopMatch": 0, + "events": [], + "deliverySchedule": [ + { + "planId": "1", + "tokens": [ + { + "class": 1, + "total": 5000, + "spent": 0 + }, + { + "class": 2, + "total": 125, + "spent": 0 + } + ] + } + ] + }, + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem1", + "dealId": "dealId1", + "extLineItemId": "extLineItem1", + "accountAuctions": 1, + "domainMatched": 0, + "targetMatched": 1, + "targetMatchedButFcapped": 0, + "targetMatchedButFcapLookupFailed": 0, + "pacingDeferred": 0, + "sentToBidder": 1, + "sentToBidderAsTopMatch": 1, + "receivedFromBidder": 1, + "receivedFromBidderInvalidated": 0, + "sentToClient": 1, + "sentToClientAsTopMatch": 1, + "events": [ + { + "type": "win", + "count": 1 + } + ], + "deliverySchedule": [ + { + "planId": "1", + "tokens": [ + { + "class": 2, + "total": 125, + "spent": 0, + "totalSpent": 0 + }, + { + "class": 1, + "total": 5000, + "spent": 1, + "totalSpent": 1 + } + ] + } + ] + }, + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem2", + "dealId": "dealId2", + "extLineItemId": "extLineItem2", + "accountAuctions": 1, + "domainMatched": 0, + "targetMatched": 1, + "targetMatchedButFcapped": 0, + "targetMatchedButFcapLookupFailed": 0, + "pacingDeferred": 0, + "sentToBidder": 1, + "sentToBidderAsTopMatch": 1, + "receivedFromBidder": 1, + "receivedFromBidderInvalidated": 0, + "sentToClient": 1, + "sentToClientAsTopMatch": 1, + "events": [], + "deliverySchedule": [ + { + "planId": "1", + "tokens": [ + { + "class": 2, + "total": 125, + "spent": 0, + "totalSpent": 0 + }, + { + "class": 1, + "total": 5000, + "spent": 1, + "totalSpent": 1 + } + ] + } + ] + }, + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem5", + "dealId": "dealId5", + "extLineItemId": "extLineItem5", + "domainMatched": 0, + "targetMatched": 0, + "targetMatchedButFcapped": 0, + "targetMatchedButFcapLookupFailed": 0, + "pacingDeferred": 0, + "sentToBidder": 0, + "sentToBidderAsTopMatch": 0, + "receivedFromBidder": 0, + "receivedFromBidderInvalidated": 0, + "sentToClient": 0, + "sentToClientAsTopMatch": 0, + "events": [], + "deliverySchedule": [ + { + "planId": "1", + "tokens": [ + { + "class": 2, + "total": 125, + "spent": 0 + }, + { + "class": 1, + "total": 5000, + "spent": 0 + } + ] + } + ] + }, + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem3", + "dealId": "dealId3", + "extLineItemId": "extLineItem3", + "accountAuctions": 1, + "domainMatched": 0, + "targetMatched": 1, + "targetMatchedButFcapped": 0, + "targetMatchedButFcapLookupFailed": 0, + "pacingDeferred": 0, + "sentToBidder": 1, + "sentToBidderAsTopMatch": 1, + "receivedFromBidder": 1, + "receivedFromBidderInvalidated": 0, + "sentToClient": 1, + "sentToClientAsTopMatch": 1, + "lostToLineItems": [ + { + "lineItemSource": "rubicon", + "lineItemId": "lineItem1", + "count": 1 + } + ], + "events": [], + "deliverySchedule": [ + { + "planId": "1", + "tokens": [ + { + "class": 1, + "total": 5000, + "spent": 1 + }, + { + "class": 2, + "total": 125, + "spent": 0 + } + ] + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-planner-plan-response-1.json b/src/test/resources/org/prebid/server/it/deals/test-planner-plan-response-1.json new file mode 100644 index 00000000000..d71a4d25cb6 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-planner-plan-response-1.json @@ -0,0 +1,347 @@ +[ + { + "lineItemId": "lineItem1", + "extLineItemId": "extLineItem1", + "dealId": "dealId1", + "status": "active", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "accountId": "2001", + "source": "rubicon", + "price": { + "cpm": 5.60, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "{{ lineItem.startTime }}", + "endTimeStamp": "{{ lineItem.endTime }}", + "updatedTimeStamp": "{{ now }}", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId1", + "count": 3, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "{{ now }}", + "startTimeStamp": "{{ plan.startTime }}", + "endTimeStamp": "{{ plan.endTime }}", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + }, + { + "lineItemId": "lineItem2", + "dealId": "dealId2", + "extLineItemId": "extLineItem2", + "status": "active", + "sizes": [ + { + "w": 320, + "h": 320 + } + ], + "accountId": "2001", + "source": "rubicon", + "price": { + "cpm": 5.60, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "{{ lineItem.startTime }}", + "endTimeStamp": "{{ lineItem.endTime }}", + "updatedTimeStamp": "{{ now }}", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 320, + "h": 320 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId2", + "count": 3, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "{{ now }}", + "startTimeStamp": "{{ plan.startTime }}", + "endTimeStamp": "{{ plan.endTime }}", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + }, + { + "lineItemId": "lineItem3", + "extLineItemId": "extLineItem3", + "dealId": "dealId3", + "status": "active", + "sizes": [ + { + "w": 300, + "h": 250 + } + ], + "accountId": "2001", + "source": "rubicon", + "price": { + "cpm": 4.60, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "{{ lineItem.startTime }}", + "endTimeStamp": "{{ lineItem.endTime }}", + "updatedTimeStamp": "{{ now }}", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId1", + "count": 3, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "{{ now }}", + "startTimeStamp": "{{ plan.startTime }}", + "endTimeStamp": "{{ plan.endTime }}", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + }, + { + "lineItemId": "lineItem4", + "extLineItemId": "extLineItem4", + "dealId": "dealId4", + "status": "active", + "sizes": [ + { + "w": 300, + "h": 600 + } + ], + "accountId": "2001", + "source": "rubicon", + "price": { + "cpm": 5.60, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "{{ lineItem.startTime }}", + "endTimeStamp": "{{ lineItem.endTime }}", + "updatedTimeStamp": "{{ now }}", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 600 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId3", + "count": 10, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "{{ now }}", + "startTimeStamp": "{{ plan.startTime }}", + "endTimeStamp": "{{ plan.endTime }}", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + }, + { + "lineItemId": "lineItem5", + "extLineItemId": "extLineItem5", + "dealId": "dealId5", + "status": "active", + "sizes": [ + { + "w": 300, + "h": 600 + } + ], + "accountId": "2002", + "source": "rubicon", + "price": { + "cpm": 5.60, + "currency": "USD" + }, + "relativePriority": 3, + "startTimeStamp": "{{ lineItem.startTime }}", + "endTimeStamp": "{{ lineItem.endTime }}", + "updatedTimeStamp": "{{ now }}", + "targeting": { + "$and": [ + { + "adunit.size": { + "$intersects": [ + { + "w": 300, + "h": 600 + } + ] + } + }, + { + "adunit.mediatype": { + "$intersects": [ + "banner" + ] + } + } + ] + }, + "frequencyCaps": [ + { + "fcapId": "fcapId5", + "count": 10, + "periods": 7, + "periodType": "day" + } + ], + "deliverySchedules": [ + { + "planId": "1", + "updatedTimeStamp": "{{ now }}", + "startTimeStamp": "{{ plan.startTime }}", + "endTimeStamp": "{{ plan.endTime }}", + "tokens": [ + { + "class": 1, + "total": 5000 + }, + { + "class": 2, + "total": 125 + } + ] + } + ] + } +] diff --git a/src/test/resources/org/prebid/server/it/deals/test-planner-register-request-1.json b/src/test/resources/org/prebid/server/it/deals/test-planner-register-request-1.json new file mode 100644 index 00000000000..ca88dd51e89 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-planner-register-request-1.json @@ -0,0 +1,15 @@ +{ + "healthIndex": 1, + "status": { + "dealsStatus": { + "instanceId": "localhost", + "vendor": "local", + "region": "local", + "clientAuctions": 0, + "lineItemStatus": [] + } + }, + "hostInstanceId": "localhost", + "region": "local", + "vendor": "local" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/test-planner-register-response.json b/src/test/resources/org/prebid/server/it/deals/test-planner-register-response.json new file mode 100644 index 00000000000..456c5fa32f7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-planner-register-response.json @@ -0,0 +1,12 @@ +{ + "tracer": { + "cmd": "start", + "raw": "true", + "durationInSeconds": "300", + "filters": { + "accountId": "1001", + "bidderCode": "pgRubicon", + "lineItemId": "642534" + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-1.json new file mode 100644 index 00000000000..593f9e16697 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-1.json @@ -0,0 +1,130 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "ext": { + "rp": { + "size_id": 15, + "mime": "text/html" + } + } + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ] + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ] + } + } + } + ] + }, + "ext": { + "rp": { + "zone_id": 4001, + "target": { + "line_item": "extLineItem1" + }, + "track": { + "mint": "", + "mint_version": "" + } + }, + "maxbids": 1 + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "ext": { + "rp": { + "account_id": 2001 + } + } + }, + "ext": { + "rp": { + "site_id": 3001 + }, + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0", + "ext": { + "rp": {} + } + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ] + } + }, + "at": 1, + "tmax": 2000 +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-2.json b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-2.json new file mode 100644 index 00000000000..a9103d31670 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-2.json @@ -0,0 +1,114 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId2", + "banner": { + "format": [ + { + "w": 320, + "h": 320 + } + ], + "ext": { + "rp": { + "size_id": 72, + "mime": "text/html" + } + } + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId2", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem2", + "extlineitemid": "extLineItem2", + "sizes": [ + { + "w": 320, + "h": 320 + } + ] + } + } + } + ] + }, + "ext": { + "rp": { + "zone_id": 4001, + "target": { + "line_item": "extLineItem2" + }, + "track": { + "mint": "", + "mint_version": "" + } + }, + "maxbids": 1 + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "ext": { + "rp": { + "account_id": 2001 + } + } + }, + "ext": { + "rp": { + "site_id": 3001 + }, + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0", + "ext": { + "rp": {} + } + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ] + } + }, + "at": 1, + "tmax": 2000 +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-3.json b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-3.json new file mode 100644 index 00000000000..74ca7d629ce --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-3.json @@ -0,0 +1,130 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "ext": { + "rp": { + "size_id": 15, + "mime": "text/html" + } + } + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ] + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ] + } + } + } + ] + }, + "ext": { + "rp": { + "zone_id": 4001, + "target": { + "line_item": "extLineItem3" + }, + "track": { + "mint": "", + "mint_version": "" + } + }, + "maxbids": 1 + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "ext": { + "rp": { + "account_id": 2001 + } + } + }, + "ext": { + "rp": { + "site_id": 3001 + }, + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0", + "ext": { + "rp": {} + } + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ] + } + }, + "at": 1, + "tmax": 2000 +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-4.json b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-4.json new file mode 100644 index 00000000000..8e1d37d697b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-4.json @@ -0,0 +1,114 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId4", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "ext": { + "rp": { + "size_id": 15, + "mime": "text/html" + } + } + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ] + } + } + } + ] + }, + "ext": { + "rp": { + "zone_id": 4001, + "target": { + "line_item": "extLineItem3" + }, + "track": { + "mint": "", + "mint_version": "" + } + }, + "maxbids": 1 + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "ext": { + "rp": { + "account_id": 2001 + } + } + }, + "ext": { + "rp": { + "site_id": 3001 + }, + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0", + "ext": { + "rp": {} + } + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ] + } + }, + "at": 1, + "tmax": 2000 +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-5.json b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-5.json new file mode 100644 index 00000000000..634499ed025 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-request-5.json @@ -0,0 +1,129 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId4", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "ext": { + "rp": { + "size_id": 15, + "mime": "text/html" + } + } + }, + "pmp": { + "private_auction": 0, + "deals": [ + { + "id": "dealId1", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem1", + "extlineitemid": "extLineItem1", + "sizes": [ + { + "w": 300, + "h": 250 + } + ] + } + } + }, + { + "id": "dealId3", + "bidfloor": 0.0, + "ext": { + "line": { + "lineitemid": "lineItem3", + "extlineitemid": "extLineItem3", + "sizes": [ + { + "w": 300, + "h": 250 + } + ] + } + } + } + ] + }, + "ext": { + "rp": { + "zone_id": 4001, + "target": { + "line_item": "extLineItem3" + }, + "track": { + "mint": "", + "mint_version": "" + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "ext": { + "rp": { + "account_id": 2001 + } + } + }, + "ext": { + "rp": { + "site_id": 3001 + }, + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "185.199.110.0", + "ext": { + "rp": {} + } + }, + "user": { + "data": [ + { + "id": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "fcapids": [ + "fcapId3" + ] + } + }, + "at": 1, + "tmax": 2000 +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-1.json b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-1.json new file mode 100644 index 00000000000..4eefeb13b98 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-1.json @@ -0,0 +1,20 @@ +{ + "id": "bidResponseId1", + "seatbid": [ + { + "seat": "seatId1", + "bid": [ + { + "id": "bidId1", + "impid": "impId1", + "dealid": "dealId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-2.json b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-2.json new file mode 100644 index 00000000000..27684539a73 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-2.json @@ -0,0 +1,20 @@ +{ + "id": "bidResponseId2", + "seatbid": [ + { + "seat": "seatId2", + "bid": [ + { + "id": "bidId2", + "impid": "impId2", + "dealid": "dealId2", + "price": 8.43, + "adm": "", + "crid": "crid2", + "w": 320, + "h": 320 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-3.json b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-3.json new file mode 100644 index 00000000000..cc84a452f14 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-3.json @@ -0,0 +1,20 @@ +{ + "id": "bidResponseId3", + "seatbid": [ + { + "seat": "seatId3", + "bid": [ + { + "id": "bidId3", + "impid": "impId1", + "dealid": "dealId3", + "price": 8.43, + "adm": "", + "crid": "crid3", + "w": 300, + "h": 250 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-4.json b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-4.json new file mode 100644 index 00000000000..8fcddce40d2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-4.json @@ -0,0 +1,20 @@ +{ + "id": "bidResponseId4", + "seatbid": [ + { + "seat": "seatId4", + "bid": [ + { + "id": "bidId4", + "impid": "impId4", + "dealid": "dealId3", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-5.json b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-5.json new file mode 100644 index 00000000000..e2d6b4b1275 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-rubicon-bid-response-5.json @@ -0,0 +1,20 @@ +{ + "id": "bidResponseId5", + "seatbid": [ + { + "seat": "seatId5", + "bid": [ + { + "id": "bidId5", + "impid": "impId4", + "dealid": "dealId3", + "price": 8.43, + "adm": "", + "crid": "crid5", + "w": 300, + "h": 250 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-user-data-details-request-1.json b/src/test/resources/org/prebid/server/it/deals/test-user-data-details-request-1.json new file mode 100644 index 00000000000..57e3a12ceec --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-user-data-details-request-1.json @@ -0,0 +1,8 @@ +{ + "ids": [ + { + "type": "khaos", + "id": "J5VLCWQP-26-CWFT" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/deals/test-user-data-details-response-1.json b/src/test/resources/org/prebid/server/it/deals/test-user-data-details-response-1.json new file mode 100644 index 00000000000..098785643ee --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-user-data-details-response-1.json @@ -0,0 +1,35 @@ +{ + "user": { + "data": [ + { + "id": "1111", + "name": "rubicon", + "segment": [ + { + "id": "2222" + }, + { + "id": "3333" + } + ] + }, + { + "id": "4444", + "name": "bluekai", + "segment": [ + { + "id": "5555" + }, + { + "id": "6666" + } + ] + } + ], + "ext": { + "fcapIds": [ + "fcapId3" + ] + } + } +} diff --git a/src/test/resources/org/prebid/server/it/deals/test-user-data-win-event-request-1.json b/src/test/resources/org/prebid/server/it/deals/test-user-data-win-event-request-1.json new file mode 100644 index 00000000000..a59968d6f4e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/deals/test-user-data-win-event-request-1.json @@ -0,0 +1,20 @@ +{ + "bidderCode": "rubicon", + "bidId": "bidId", + "lineItemId": "lineItem1", + "region" : "local", + "userIds": [ + { + "type": "khaos", + "id": "J5VLCWQP-26-CWFT" + } + ], + "frequencyCaps": [ + { + "fcapId": "fcapId1", + "count": 3, + "periods": 7, + "periodType": "day" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/gdpr-vendorlist2/52.json b/src/test/resources/org/prebid/server/it/gdpr-vendorlist2/52.json new file mode 100644 index 00000000000..1c937faea57 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/gdpr-vendorlist2/52.json @@ -0,0 +1 @@ +{"gvlSpecificationVersion":2,"vendorListVersion":52,"tcfPolicyVersion":2,"lastUpdated":"2020-08-20T16:05:24Z","purposes":{"1":{"id":1,"name":"Store and/or access information on a device","description":"Cookies, device identifiers, or other information can be stored or accessed on your device for the purposes presented to you.","descriptionLegal":"Vendors can:\n* Store and access information on the device such as cookies and device identifiers presented to a user."},"2":{"id":2,"name":"Select basic ads","description":"Ads can be shown to you based on the content you\u2019re viewing, the app you\u2019re using, your approximate location, or your device type.","descriptionLegal":"To do basic ad selection vendors can:\n* Use real-time information about the context in which the ad will be shown, to show the ad, including information about the content and the device, such as: device type and capabilities, user agent, URL, IP address\n* Use a user\u2019s non-precise geolocation data\n* Control the frequency of ads shown to a user.\n* Sequence the order in which ads are shown to a user.\n* Prevent an ad from serving in an unsuitable editorial (brand-unsafe) context\nVendors cannot:\n* Create a personalised ads profile using this information for the selection of future ads.\n* N.B. Non-precise means only an approximate location involving at least a radius of 500 meters is permitted."},"3":{"id":3,"name":"Create a personalised ads profile","description":"A profile can be built about you and your interests to show you personalised ads that are relevant to you.","descriptionLegal":"To create a personalised ads profile vendors can:\n* Collect information about a user, including a user's activity, interests, demographic information, or location, to create or edit a user profile for use in personalised advertising.\n* Combine this information with other information previously collected, including from across websites and apps, to create or edit a user profile for use in personalised advertising."},"4":{"id":4,"name":"Select personalised ads","description":"Personalised ads can be shown to you based on a profile about you.","descriptionLegal":"To select personalised ads vendors can:\n* Select personalised ads based on a user profile or other historical user data, including a user\u2019s prior activity, interests, visits to sites or apps, location, or demographic information."},"5":{"id":5,"name":"Create a personalised content profile","description":"A profile can be built about you and your interests to show you personalised content that is relevant to you.","descriptionLegal":"To create a personalised content profile vendors can:\n* Collect information about a user, including a user's activity, interests, visits to sites or apps, demographic information, or location, to create or edit a user profile for personalising content.\n* Combine this information with other information previously collected, including from across websites and apps, to create or edit a user profile for use in personalising content."},"6":{"id":6,"name":"Select personalised content","description":"Personalised content can be shown to you based on a profile about you.","descriptionLegal":"To select personalised content vendors can:\n* Select personalised content based on a user profile or other historical user data, including a user\u2019s prior activity, interests, visits to sites or apps, location, or demographic information."},"7":{"id":7,"name":"Measure ad performance","description":"The performance and effectiveness of ads that you see or interact with can be measured.","descriptionLegal":"To measure ad performance vendors can:\n* Measure whether and how ads were delivered to and interacted with by a user\n* Provide reporting about ads including their effectiveness and performance\n* Provide reporting about users who interacted with ads using data observed during the course of the user's interaction with that ad\n* Provide reporting to publishers about the ads displayed on their property\n* Measure whether an ad is serving in a suitable editorial environment (brand-safe) context\n* Determine the percentage of the ad that had the opportunity to be seen and the duration of that opportunity\n* Combine this information with other information previously collected, including from across websites and apps\nVendors cannot:\n*Apply panel- or similarly-derived audience insights data to ad measurement data without a Legal Basis to apply market research to generate audience insights (Purpose 9)"},"8":{"id":8,"name":"Measure content performance","description":"The performance and effectiveness of content that you see or interact with can be measured.","descriptionLegal":"To measure content performance vendors can:\n* Measure and report on how content was delivered to and interacted with by users.\n* Provide reporting, using directly measurable or known information, about users who interacted with the content\n* Combine this information with other information previously collected, including from across websites and apps.\nVendors cannot:\n* Measure whether and how ads (including native ads) were delivered to and interacted with by a user.\n* Apply panel- or similarly derived audience insights data to ad measurement data without a Legal Basis to apply market research to generate audience insights (Purpose 9)"},"9":{"id":9,"name":"Apply market research to generate audience insights","description":"Market research can be used to learn more about the audiences who visit sites/apps and view ads.","descriptionLegal":"To apply market research to generate audience insights vendors can:\n* Provide aggregate reporting to advertisers or their representatives about the audiences reached by their ads, through panel-based and similarly derived insights.\n* Provide aggregate reporting to publishers about the audiences that were served or interacted with content and/or ads on their property by applying panel-based and similarly derived insights.\n* Associate offline data with an online user for the purposes of market research to generate audience insights if vendors have declared to match and combine offline data sources (Feature 1)\n* Combine this information with other information previously collected including from across websites and apps. \nVendors cannot:\n* Measure the performance and effectiveness of ads that a specific user was served or interacted with, without a Legal Basis to measure ad performance.\n* Measure which content a specific user was served and how they interacted with it, without a Legal Basis to measure content performance."},"10":{"id":10,"name":"Develop and improve products","description":"Your data can be used to improve existing systems and software, and to develop new products","descriptionLegal":"To develop new products and improve products vendors can:\n* Use information to improve their existing products with new features and to develop new products\n* Create new models and algorithms through machine learning\nVendors cannot:\n* Conduct any other data processing operation allowed under a different purpose under this purpose"}},"specialPurposes":{"1":{"id":1,"name":"Ensure security, prevent fraud, and debug","description":"Your data can be used to monitor for and prevent fraudulent activity, and ensure systems and processes work properly and securely.","descriptionLegal":"To ensure security, prevent fraud and debug vendors can:\n* Ensure data are securely transmitted\n* Detect and prevent malicious, fraudulent, invalid, or illegal activity.\n* Ensure correct and efficient operation of systems and processes, including to monitor and enhance the performance of systems and processes engaged in permitted purposes\nVendors cannot:\n* Conduct any other data processing operation allowed under a different purpose under this purpose."},"2":{"id":2,"name":"Technically deliver ads or content","description":"Your device can receive and send information that allows you to see and interact with ads and content.","descriptionLegal":"To deliver information and respond to technical requests vendors can:\n* Use a user\u2019s IP address to deliver an ad over the internet\n* Respond to a user\u2019s interaction with an ad by sending the user to a landing page\n* Use a user\u2019s IP address to deliver content over the internet\n* Respond to a user\u2019s interaction with content by sending the user to a landing page\n* Use information about the device type and capabilities for delivering ads or content, for example, to deliver the right size ad creative or video file in a format supported by the device\nVendors cannot:\n* Conduct any other data processing operation allowed under a different purpose under this purpose"}},"features":{"1":{"id":1,"name":"Match and combine offline data sources","description":"Data from offline data sources can be combined with your online activity in support of one or more purposes","descriptionLegal":"Vendors can:\n* Combine data obtained offline with data collected online in support of one or more Purposes or Special Purposes."},"2":{"id":2,"name":"Link different devices","description":"Different devices can be determined as belonging to you or your household in support of one or more of purposes.","descriptionLegal":"Vendors can:\n* Deterministically determine that two or more devices belong to the same user or household\n* Probabilistically determine that two or more devices belong to the same user or household\n* Actively scan device characteristics for identification for probabilistic identification if users have allowed vendors to actively scan device characteristics for identification (Special Feature 2)"},"3":{"id":3,"name":"Receive and use automatically-sent device characteristics for identification","description":"Your device might be distinguished from other devices based on information it automatically sends, such as IP address or browser type.","descriptionLegal":"Vendors can:\n* Create an identifier using data collected automatically from a device for specific characteristics, e.g. IP address, user-agent string.\n* Use such an identifier to attempt to re-identify a device.\nVendors cannot:\n* Create an identifier using data collected via actively scanning a device for specific characteristics, e.g. installed font or screen resolution without users\u2019 separate opt-in to actively scanning device characteristics for identification.\n* Use such an identifier to re-identify a device."}},"specialFeatures":{"1":{"id":1,"name":"Use precise geolocation data","description":"Your precise geolocation data can be used in support of one or more purposes. This means your location can be accurate to within several meters.","descriptionLegal":"Vendors can:\n* Collect and process precise geolocation data in support of one or more purposes.\nN.B. Precise geolocation means that there are no restrictions on the precision of a user\u2019s location; this can be accurate to within several meters."},"2":{"id":2,"name":"Actively scan device characteristics for identification","description":"Your device can be identified based on a scan of your device's unique combination of characteristics.","descriptionLegal":"Vendors can:\n* Create an identifier using data collected via actively scanning a device for specific characteristics, e.g. installed fonts or screen resolution.\n* Use such an identifier to re-identify a device."}},"stacks":{"1":{"id":1,"purposes":[],"specialFeatures":[1,2],"name":"Precise geolocation data, and identification through device scanning","description":"Precise geolocation and information about device characteristics can be used."},"2":{"id":2,"purposes":[2,7],"specialFeatures":[],"name":"Basic ads, and ad measurement","description":"Basic ads can be served. Ad performance can be measured."},"3":{"id":3,"purposes":[2,3,4],"specialFeatures":[],"name":"Personalised ads","description":"Ads can be personalised based on a profile. More data can be added to better personalise ads."},"4":{"id":4,"purposes":[2,7,9],"specialFeatures":[],"name":"Basic ads, ad measurement, and audience insights","description":"Basic ads can be served. Ad performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"5":{"id":5,"purposes":[2,3,7],"specialFeatures":[],"name":"Basic ads, personalised ads profile, and ad measurement","description":"Basic ads can be served. More data can be added to better personalise ads. Ad performance can be measured."},"6":{"id":6,"purposes":[2,4,7],"specialFeatures":[],"name":"Personalised ads display and ad measurement","description":"Ads can be personalised based on a profile. Ad performance can be measured."},"7":{"id":7,"purposes":[2,4,7,9],"specialFeatures":[],"name":"Personalised ads display, ad measurement, and audience insights","description":"Ads can be personalised based on a profile. Ad performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"8":{"id":8,"purposes":[2,3,4,7],"specialFeatures":[],"name":"Personalised ads, and ad measurement","description":"Ads can be personalised based on a profile. More data can be added to better personalise ads. Ad performance can be measured."},"9":{"id":9,"purposes":[2,3,4,7,9],"specialFeatures":[],"name":"Personalised ads, ad measurement, and audience insights","description":"Ads can be personalised based on a profile. More data can be added to better personalise ads. Ad performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"10":{"id":10,"purposes":[3,4],"specialFeatures":[],"name":"Personalised ads profile and display","description":"Ads can be personalised based on a profile. More data can be added to better personalise ads."},"11":{"id":11,"purposes":[5,6],"specialFeatures":[],"name":"Personalised content","description":"Content can be personalised based on a profile. More data can be added to better personalise content."},"12":{"id":12,"purposes":[6,8],"specialFeatures":[],"name":"Personalised content display, and content measurement","description":"Content can be personalised based on a profile. Content performance can be measured."},"13":{"id":13,"purposes":[6,8,9],"specialFeatures":[],"name":"Personalised content display, content measurement and audience insights","description":"Content can be personalised based on a profile. Content performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"14":{"id":14,"purposes":[5,6,8],"specialFeatures":[],"name":"Personalised content, and content measurement","description":"Content can be personalised based on a profile. More data can be added to better personalise content. Content performance can be measured."},"15":{"id":15,"purposes":[5,6,8,9],"specialFeatures":[],"name":"Personalised content, content measurement and audience insights","description":"Content can be personalised based on a profile. More data can be added to better personalise content. Content performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"16":{"id":16,"purposes":[5,6,8,9,10],"specialFeatures":[],"name":"Personalised content, content measurement, audience insights, and product development","description":"Content can be personalised based on a profile. More data can be added to better personalise content. Content performance can be measured. Insights about the audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems, and software"},"17":{"id":17,"purposes":[7,8,9],"specialFeatures":[],"name":"Ad and content measurement, and audience insights","description":"Ad and content performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"18":{"id":18,"purposes":[7,8],"specialFeatures":[],"name":"Ad and content measurement","description":"Ad and content performance can be measured."},"19":{"id":19,"purposes":[7,9],"specialFeatures":[],"name":"Ad measurement, and audience insights","description":"Ad can be measured. Insights about the audiences who saw the ads and content can be derived."},"20":{"id":20,"purposes":[7,8,9,10],"specialFeatures":[],"name":"Ad and content measurement, audience insights, and product development","description":"Ad and content performance can be measured. Insights about the audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems, and software. Insights about the audiences who saw the ads and content can be derived."},"21":{"id":21,"purposes":[8,9,10],"specialFeatures":[],"name":"Content measurement, audience insights, and product development.","description":"Content performance can be measured. Insights about the audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems, and software."},"22":{"id":22,"purposes":[8,10],"specialFeatures":[],"name":"Content measurement, and product development","description":"Content performance can be measured. Data can be used to build or improve user experience, systems, and software."},"23":{"id":23,"purposes":[2,4,6,7,8],"specialFeatures":[],"name":"Personalised ads and content display, ad and content measurement","description":"Ads and content can be personalised based on a profile. Ad and content performance can be measured."},"24":{"id":24,"purposes":[2,4,6,7,8,9],"specialFeatures":[],"name":"Personalised ads and content display, ad and content measurement, and audience insights","description":"Ads and content can be personalised based on a profile. Ad and content performance can be measured. Insights about the audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems, and software."},"25":{"id":25,"purposes":[2,3,4,5,6,7,8],"specialFeatures":[],"name":"Personalised ads and content, ad and content measurement","description":"Ads and content can be personalised based on a profile. More data can be added to better personalise ads and content. Ad and content performance can be measured."},"26":{"id":26,"purposes":[2,3,4,5,6,7,8,9],"specialFeatures":[],"name":"Personalised ads and content, ad and content measurement, and audience insights","description":"Ads and content can be personalised based on a profile. More data can be added to better personalise ads and content. Ad and content performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"27":{"id":27,"purposes":[3,5],"specialFeatures":[],"name":"Personalised ads, and content profile","description":"More data can be added to personalise ads and content."},"28":{"id":28,"purposes":[2,4,6],"specialFeatures":[],"name":"Personalised ads and content display","description":"Ads and content can be personalised based on a profile."},"29":{"id":29,"purposes":[2,7,8,9],"specialFeatures":[],"name":"Basic ads, ad and content measurement, and audience insights","description":"Basic ads can be served. Ad and content performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"30":{"id":30,"purposes":[2,4,5,6,7,8,9],"specialFeatures":[],"name":"Personalised ads display, personalised content, ad and content measurement, and audience insights","description":"Ads and content can be personalised based on a profile. More data can be added to better personalise content. Ad and content performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"31":{"id":31,"purposes":[2,4,5,6,7,8,9,10],"specialFeatures":[],"name":"Personalised ads display, personalised content, ad and content measurement, audience insights, and product development","description":"Ads and content can be personalised based on a profile. More data can be added to better personalise content. Ad and content performance can be measured. Insights about the audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems, and software."},"32":{"id":32,"purposes":[2,5,6,7,8,9],"specialFeatures":[],"name":"Basic ads, personalised content, ad and content measurement, and audience insights","description":"Basic ads can be served. Content can be personalised based on a profile. More data can be added to better personalise content. Ad and content performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"33":{"id":33,"purposes":[2,5,6,7,8,9,10],"specialFeatures":[],"name":"Basic ads, personalised content, ad and content measurement, audience insights, and product development","description":"Basic ads can be served. Content can be personalised based on a profile. More data can be added to better personalise content. Ad and content performance can be measured. Insights about the audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems, and software."},"34":{"id":34,"purposes":[2,5,6,8,9],"specialFeatures":[],"name":"Basic ads, personalised content, content measurement, and audience insights","description":"Basic ads can be served. Content can be personalised based on a profile. More data can be added to better personalise content. Ad and content performance can be measured. Insights about the audiences who saw the ads and content can be derived."},"35":{"id":35,"purposes":[2,5,6,8,9,10],"specialFeatures":[],"name":"Basic ads, personalised content, content measurement, audience insights, and product development","description":"Basic ads can be served. Content can be personalised based on a profile. More data can be added to better personalise content. Content performance can be measured. Insights about the audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems, and software."},"36":{"id":36,"purposes":[2,5,6,7],"specialFeatures":[],"name":"Basic ads, personalised content, and ad measurement","description":"Basic ads can be served. Content can be personalised based on a profile. More data can be added to better personalise content. Ad performance can be measured."},"37":{"id":37,"purposes":[2,5,6,7,10],"specialFeatures":[],"name":"Basic ads, personalised content, ad measurement, and product development","description":"Basic ads can be served. Content can be personalised based on a profile. More data can be added to better personalise content. Ad performance can be measured. Data can be used to build or improve user experience, systems, and software."},"38":{"id":38,"purposes":[2,3,4,7,10],"specialFeatures":[],"name":"Personalised ads, ad measurement, and product development","description":"Ads can be personalised based on a profile. More data can be added to better personalise ads. Ad performance can be measured. Data can be used to build or improve user experience, systems, and software."},"39":{"id":39,"purposes":[2,3,4,7,9,10],"specialFeatures":[],"name":"Personalised ads, ad measurement, audience insights and product development","description":"Ads can be personalised based on a profile. More data can be added to better personalise ads. Ad performance can be measured. Insights about the audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems and software."},"40":{"id":40,"purposes":[2,3,4,7,8,9,10],"specialFeatures":[],"name":"Personalised ads, ad and content measurement, audience insights and product development","description":"Ads can be personalised based on a profile. More data can be added to better personalise ads. Ad and content performance can be measured. Insights about audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems and software."},"41":{"id":41,"purposes":[2,3,4,6,7,8,9,10],"specialFeatures":[],"name":"Personalised ads, personalised content display, ad and content measurement, audience insights and product development","description":"Ads and content can be personalised based on a profile. More data can be added to better personalise ads. Ad and content performance can be measured. Insights about audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems and software."},"42":{"id":42,"purposes":[2,3,4,5,6,7,8,9,10],"specialFeatures":[],"name":"Personalised ads and content, ad and content measurement, audience insights and product development","description":"Ads and content can be personalised based on a profile. More data can be added to better personalise ads and content. Ad and content performance can be measured. Insights about audiences who saw the ads and content can be derived. Data can be used to build or improve user experience, systems and software."}},"vendors":{"8":{"id":8,"name":"Emerse Sverige AB","purposes":[1,3,4],"legIntPurposes":[2,7,8,9],"flexiblePurposes":[2,9],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.emerse.com/privacy-policy/"},"9":{"id":9,"name":"AdMaxim Inc.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"http://www.admaxim.com/admaxim-privacy-policy/","deletedDate":"2020-06-17T00:00:00Z"},"12":{"id":12,"name":"BeeswaxIO Corporation","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[1],"policyUrl":"https://www.beeswax.com/privacy/"},"28":{"id":28,"name":"TripleLift, Inc.","purposes":[1],"legIntPurposes":[2,7,9,10],"flexiblePurposes":[2,7,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://triplelift.com/privacy/","overflow":{"httpGetLimit":32}},"25":{"id":25,"name":"Verizon Media EMEA Limited","purposes":[1,3,4,5,6],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[2,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.verizonmedia.com/policies/ie/en/verizonmedia/privacy/index.html"},"26":{"id":26,"name":"Venatus Media Limited","purposes":[1,2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[],"policyUrl":"https://www.venatusmedia.com/privacy/"},"1":{"id":1,"name":"Exponential Interactive, Inc d/b/a VDX.tv","purposes":[1,2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,8,9,10],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[],"policyUrl":"https://vdx.tv/privacy/"},"6":{"id":6,"name":"AdSpirit GmbH","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,9],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"http://www.adspirit.de/privacy","overflow":{"httpGetLimit":32}},"30":{"id":30,"name":"BidTheatre AB","purposes":[1,3,4],"legIntPurposes":[2],"flexiblePurposes":[2],"specialPurposes":[1,2],"features":[2],"specialFeatures":[],"policyUrl":"https://www.bidtheatre.com/privacy-policy"},"24":{"id":24,"name":"Epsilon","purposes":[1,2,3,4,5,6,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.conversantmedia.eu/legal/privacy-policy"},"39":{"id":39,"name":"ADITION technologies AG","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.adition.com/datenschutz"},"11":{"id":11,"name":"Quantcast International Limited","purposes":[1,3,4],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[2,3,4,7,8,9,10],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[],"policyUrl":"https://www.quantcast.com/privacy/"},"15":{"id":15,"name":"Adikteev","purposes":[1,3,4,5,6,8,9,10],"legIntPurposes":[2,7],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2],"specialFeatures":[],"policyUrl":"https://www.adikteev.com/privacy-policy-eng/"},"4":{"id":4,"name":"Roq.ad Inc.","purposes":[1,2,3,4,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2,3],"specialFeatures":[1],"policyUrl":"https://www.roq.ad/privacy-policy"},"7":{"id":7,"name":"Vibrant Media Limited","purposes":[1,3,4,5,6,7,8],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.vibrantmedia.com/en/privacy-policy/"},"2":{"id":2,"name":"Captify Technologies Limited","purposes":[1,2,3,4],"legIntPurposes":[7,9,10],"flexiblePurposes":[2],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"http://www.captify.co.uk/privacy-policy/"},"37":{"id":37,"name":"NEURAL.ONE","purposes":[1,2,3,4,5,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://web.neural.one/privacy-policy/"},"13":{"id":13,"name":"Sovrn Holdings Inc","purposes":[1,2,3,5,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.sovrn.com/sovrn-privacy/"},"34":{"id":34,"name":"NEORY GmbH","purposes":[1,3,4,5,6,9],"legIntPurposes":[2,7,8],"flexiblePurposes":[2,3,4,5,6,7,8,9],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.neory.com/privacy.html","overflow":{"httpGetLimit":128}},"32":{"id":32,"name":"Xandr, Inc.","purposes":[1,3,4],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,7,10],"specialPurposes":[1,2],"features":[2],"specialFeatures":[1],"policyUrl":"https://www.xandr.com/privacy/platform-privacy-policy/"},"10":{"id":10,"name":"Index Exchange, Inc. ","purposes":[1,2,7],"legIntPurposes":[],"flexiblePurposes":[2,7],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.indexexchange.com/privacy"},"57":{"id":57,"name":"ADARA MEDIA UNLIMITED","purposes":[1,2,3,4],"legIntPurposes":[7,9,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://adara.com/privacy-promise/"},"63":{"id":63,"name":"Avocet Systems Limited","purposes":[1,2,3,4,5,6],"legIntPurposes":[7,8,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://avocet.io/privacy-portal"},"51":{"id":51,"name":"xAd, Inc. dba GroundTruth","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.groundtruth.com/privacy-policy/"},"49":{"id":49,"name":"TRADELAB","purposes":[1,2,3,4,5,6],"legIntPurposes":[7,8,9,10],"flexiblePurposes":[7,8,9,10],"specialPurposes":[],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://tradelab.com/en/privacy/","overflow":{"httpGetLimit":32}},"45":{"id":45,"name":"Smart Adserver","purposes":[1,2,4,7],"legIntPurposes":[],"flexiblePurposes":[2,7],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://smartadserver.com/end-user-privacy-policy/"},"52":{"id":52,"name":"The Rubicon Project, Inc. ","purposes":[1],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,7,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"http://www.rubiconproject.com/rubicon-project-yield-optimization-privacy-policy/","overflow":{"httpGetLimit":128}},"71":{"id":71,"name":"Roku Advertising Services","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://docs.roku.com/published/userprivacypolicy/en/us"},"79":{"id":79,"name":"MediaMath, Inc.","purposes":[1,3,4],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,3,4,7,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://www.mediamath.com/privacy-policy/"},"91":{"id":91,"name":"Criteo SA","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.criteo.com/privacy/"},"85":{"id":85,"name":"Crimtan Holdings Limited","purposes":[1,3,4],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[2,7,8,9,10],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[1],"policyUrl":"https://crimtan.com/privacy/"},"16":{"id":16,"name":"RTB House S.A.","purposes":[1,2,3,4,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[],"specialFeatures":[],"policyUrl":"https://www.rtbhouse.com/privacy-center/services-privacy-policy/"},"86":{"id":86,"name":"Scene Stealer Limited","purposes":[1],"legIntPurposes":[2,3,4,7,10],"flexiblePurposes":[2,3,4,7],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://scenestealer.tv/privacy-policy/"},"94":{"id":94,"name":"Blis Media Limited","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://www.blis.com/privacy/"},"73":{"id":73,"name":"Simplifi Holdings Inc.","purposes":[1,2,3,4,7,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4],"specialPurposes":[],"features":[2],"specialFeatures":[1],"policyUrl":"https://simpli.fi/site-privacy-policy/"},"33":{"id":33,"name":"ShareThis, Inc","purposes":[1,3,5,6],"legIntPurposes":[9,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://sharethis.com/privacy/"},"20":{"id":20,"name":"N Technologies Inc.","purposes":[1,3,4],"legIntPurposes":[2,5,6,7,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[2],"specialFeatures":[1],"policyUrl":"https://n.rich/privacy-notice"},"53":{"id":53,"name":"Sirdata","purposes":[1,2,3,4,5,6,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.sirdata.com/privacy/"},"69":{"id":69,"name":"OpenX","purposes":[1],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.openx.com/legal/privacy-policy/"},"98":{"id":98,"name":"GroupM UK Limited","purposes":[1,2,3,4,5,6],"legIntPurposes":[7,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.groupm.com/privacy-notice"},"62":{"id":62,"name":"Justpremium BV","purposes":[1,4,5,10],"legIntPurposes":[2,7],"flexiblePurposes":[2,4,7,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://justpremium.com/privacy-policy/"},"36":{"id":36,"name":"RhythmOne DBA Unruly Group Ltd","purposes":[1,2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.rhythmone.com/privacy-policy"},"80":{"id":80,"name":"Sharethrough, Inc","purposes":[1,2,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,4,7,9,10],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://platform-cdn.sharethrough.com/privacy-policy"},"23":{"id":23,"name":"Amobee, Inc. ","purposes":[1,2,3,4],"legIntPurposes":[7,9,10],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.amobee.com/trust/privacy-guidelines"},"67":{"id":67,"name":"LifeStreet Corporation","purposes":[1,3,4],"legIntPurposes":[2,5,6,7,8,10],"flexiblePurposes":[2,5,6,7,8,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://lifestreet.com/privacy/"},"459":{"id":459,"name":"uppr GmbH","purposes":[1],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://netzwerk.uppr.de/privacy-policy.do"},"68":{"id":68,"name":"Sizmek by Amazon","purposes":[1,3,4],"legIntPurposes":[2,7,9,10],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.sizmek.com/privacy-policy/"},"61":{"id":61,"name":"GumGum, Inc.","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://gumgum.com/privacy-policy"},"40":{"id":40,"name":"Active Agent (ADITION technologies AG)","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://www.active-agent.com/de/unternehmen/datenschutzerklaerung/"},"76":{"id":76,"name":"PubMatic, Inc.","purposes":[1,3,4],"legIntPurposes":[2,5,6,7,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://pubmatic.com/privacy-policy/"},"89":{"id":89,"name":"Tapad, Inc.","purposes":[1],"legIntPurposes":[10],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"https://www.tapad.com/eu-privacy-policy"},"66":{"id":66,"name":"adsquare GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.adsquare.com/privacy"},"41":{"id":41,"name":"Adverline","purposes":[1,2,3,4,5,6,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.adverline.com/privacy/","overflow":{"httpGetLimit":128}},"82":{"id":82,"name":"Smaato, Inc.","purposes":[1,2,3,4,7,9],"legIntPurposes":[10],"flexiblePurposes":[2,7,9,10],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[1],"policyUrl":"https://www.smaato.com/privacy/"},"60":{"id":60,"name":"Rakuten Marketing LLC","purposes":[1,3,4],"legIntPurposes":[2,7,9,10],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://rakutenadvertising.com/legal-notices/services-privacy-policy/"},"70":{"id":70,"name":"Yieldlab AG","purposes":[1,2,3,4,7,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://www.yieldlab.de/meta-navigation/datenschutz/","overflow":{"httpGetLimit":128}},"50":{"id":50,"name":"Adform","purposes":[1,2,3,4,7,10],"legIntPurposes":[],"flexiblePurposes":[2,7,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://site.adform.com/privacy-center/platform-privacy/product-and-services-privacy-policy/","overflow":{"httpGetLimit":32}},"100":{"id":100,"name":"Fifty Technology Limited","purposes":[1,2,3,4],"legIntPurposes":[7,9,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[],"policyUrl":"https://fifty.io/privacy-policy.php"},"21":{"id":21,"name":"The Trade Desk","purposes":[1,3,4],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,7,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://www.thetradedesk.com/general/privacy-policy"},"110":{"id":110,"name":"Dynata LLC","purposes":[1,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.opinionoutpost.co.uk/en-gb/policies/privacy"},"42":{"id":42,"name":"Taboola Europe Limited","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,10],"flexiblePurposes":[2,3,4,5,6,7,8,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.taboola.com/privacy-policy"},"77":{"id":77,"name":"comScore, Inc.","purposes":[1,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[7,8,9,10],"specialPurposes":[1],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.scorecardresearch.com/privacy.aspx?newlanguage=1"},"109":{"id":109,"name":"LoopMe Limited","purposes":[1,2,3,4,5,6,7,8],"legIntPurposes":[9,10],"flexiblePurposes":[9],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://loopme.com/privacy-policy/"},"120":{"id":120,"name":"Eyeota Pte Ltd","purposes":[1,3,5,9,10],"legIntPurposes":[],"flexiblePurposes":[3,5,9,10],"specialPurposes":[1],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.eyeota.com/privacy-center"},"93":{"id":93,"name":"Adloox SA","purposes":[],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[1],"features":[],"specialFeatures":[],"policyUrl":"http://adloox.com/disclaimer","overflow":{"httpGetLimit":128}},"132":{"id":132,"name":"Teads ","purposes":[1,3,4],"legIntPurposes":[2,7,9,10],"flexiblePurposes":[2,7,9,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.teads.com/privacy-policy/"},"22":{"id":22,"name":"admetrics GmbH","purposes":[2,7,8,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://admetrics.io/en/privacy_policy/"},"102":{"id":102,"name":"Telaria SAS","purposes":[1,2,3,4,5,6],"legIntPurposes":[7,8,9,10],"flexiblePurposes":[5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://telaria.com/privacy-policy/"},"108":{"id":108,"name":"Rich Audience Technologies SL","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://richaudience.com/privacy/"},"18":{"id":18,"name":"Widespace AB","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.widespace.com/legal/privacy-policy-notice/"},"122":{"id":122,"name":"Avid Media Ltd","purposes":[1,3,4,5,10],"legIntPurposes":[2,7,8],"flexiblePurposes":[2,7,8],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.avidglobalmedia.eu/privacy-policy.html"},"97":{"id":97,"name":"LiveRamp, Inc.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.liveramp.com/service-privacy-policy/"},"138":{"id":138,"name":"ConnectAd Realtime GmbH","purposes":[1,3,4,5],"legIntPurposes":[2,7],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"http://connectadrealtime.com/privacy/"},"72":{"id":72,"name":"Nano Interactive GmbH","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://www.nanointeractive.com/privacy"},"127":{"id":127,"name":"PIXIMEDIA SAS","purposes":[1,3,4,5,6,9,10],"legIntPurposes":[2,7,8],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://piximedia.com/privacy/","overflow":{"httpGetLimit":128}},"136":{"id":136,"name":"Str\u00f6er SSP GmbH (SSP)","purposes":[1],"legIntPurposes":[2,7,9,10],"flexiblePurposes":[2,7,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.stroeer.de/fileadmin/de/Konvergenz_und_Konzepte/Daten_und_Technologien/Stroeer_SSP/Downloads/Datenschutz_Stroeer_SSP.pdf"},"111":{"id":111,"name":"Showheroes SE","purposes":[1,3,4,9,10],"legIntPurposes":[2,7,8],"flexiblePurposes":[2,3,4,7,8,9,10],"specialPurposes":[1,2],"features":[1],"specialFeatures":[],"policyUrl":"https://showheroes.com/privacy/"},"124":{"id":124,"name":"Teemo SA","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1],"specialFeatures":[1],"policyUrl":"https://teemo.co/fr/confidentialite/"},"154":{"id":154,"name":"YOC AG","purposes":[1,3,4,10],"legIntPurposes":[2,7],"flexiblePurposes":[2,7],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://yoc.com/privacy/"},"101":{"id":101,"name":"MiQ","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://wearemiq.com/privacy-policy/"},"149":{"id":149,"name":"ADman Interactive SLU","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2],"specialPurposes":[],"features":[2,3],"specialFeatures":[],"policyUrl":"https://admanmedia.com/politica.html?setLng=es"},"153":{"id":153,"name":"MADVERTISE MEDIA","purposes":[1,2,3,4,5,6,7,9,10],"legIntPurposes":[],"flexiblePurposes":[5,6,10],"specialPurposes":[2],"features":[],"specialFeatures":[1,2],"policyUrl":"https://madvertise.com/en/gdpr/","overflow":{"httpGetLimit":128}},"159":{"id":159,"name":"Underdog Media LLC ","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://underdogmedia.com/privacy-policy/"},"157":{"id":157,"name":"Seedtag Advertising S.L","purposes":[1,2,3,4,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.seedtag.com/en/privacy-policy/"},"145":{"id":145,"name":"Snapsort Inc., operating as Sortable","purposes":[1],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://help.sortable.com/help/privacy-policy"},"131":{"id":131,"name":"ID5 Technology SAS","purposes":[1],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[],"policyUrl":"https://www.id5.io/privacy"},"158":{"id":158,"name":"Reveal Mobile, Inc","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://revealmobile.com/privacy"},"147":{"id":147,"name":"Adacado Technologies Inc. (DBA Adacado)","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[7,8,9,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.adacado.com/privacy-policy-april-25-2018/"},"130":{"id":130,"name":"NextRoll, Inc.","purposes":[1,2,3,4,5,6,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.nextroll.com/privacy"},"129":{"id":129,"name":"IPONWEB GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.iponweb.com/privacy-policy/"},"128":{"id":128,"name":"BIDSWITCH GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,3],"specialFeatures":[1],"policyUrl":"http://www.bidswitch.com/privacy-policy/"},"168":{"id":168,"name":"EASYmedia GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://login.rtbmarket.com/gdpr"},"164":{"id":164,"name":"Outbrain UK Ltd","purposes":[1],"legIntPurposes":[3,4,5,6,7,8,9,10],"flexiblePurposes":[3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[],"policyUrl":"https://www.outbrain.com/legal/privacy#privacy-policy"},"144":{"id":144,"name":"district m inc.","purposes":[1,2,4,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[3],"specialFeatures":[1],"policyUrl":"https://districtm.net/en/page/platforms-data-and-privacy-policy/","overflow":{"httpGetLimit":128}},"163":{"id":163,"name":"Bombora Inc.","purposes":[1,3,8],"legIntPurposes":[7,9,10],"flexiblePurposes":[7,9,10],"specialPurposes":[1],"features":[1,3],"specialFeatures":[],"policyUrl":"https://bombora.com/privacy"},"173":{"id":173,"name":"Yieldmo, Inc.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.yieldmo.com/privacy/"},"88":{"id":88,"name":"TreSensa, Inc.","purposes":[1,3,4],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,3,4,7,10],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[],"policyUrl":"https://www.tresensa.com/eu-privacy"},"78":{"id":78,"name":"Flashtalking, Inc.","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://www.flashtalking.com/privacypolicy/"},"59":{"id":59,"name":"Sift Media, Inc","purposes":[2],"legIntPurposes":[],"flexiblePurposes":[2],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.sift.co/privacy"},"114":{"id":114,"name":"Sublime","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"http://ayads.co/privacy.php"},"133":{"id":133,"name":"digitalAudience","purposes":[1,3,5,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2],"specialFeatures":[],"policyUrl":"https://digitalaudience.io/legal/privacy-cookies/"},"14":{"id":14,"name":"Adkernel LLC","purposes":[1,3,4],"legIntPurposes":[2,7,9],"flexiblePurposes":[3,4],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"http://adkernel.com/privacy-policy/","overflow":{"httpGetLimit":32}},"183":{"id":183,"name":"EMX Digital LLC","purposes":[1,3,4,5,6],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://emxdigital.com/privacy/"},"58":{"id":58,"name":"33Across","purposes":[1,2,4,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2],"specialFeatures":[],"policyUrl":"http://www.33across.com/privacy-policy"},"140":{"id":140,"name":"Platform161","purposes":[1,2,3,4,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[1],"policyUrl":"https://platform161.com/cookie-and-privacy-policy/"},"90":{"id":90,"name":"Teroa S.A.","purposes":[1,2,3,4,5,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.e-planning.net/en/privacy.html"},"141":{"id":141,"name":"1020, Inc. dba Placecast and Ericsson Emodo","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.emodoinc.com/privacy-policy/"},"142":{"id":142,"name":"Media.net Advertising FZ-LLC","purposes":[1,3,4],"legIntPurposes":[2,5,6,7,8,9,10],"flexiblePurposes":[2,5,6,7,8,9],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.media.net/en/privacy-policy"},"209":{"id":209,"name":"Delta Projects AB","purposes":[1,2,3,4],"legIntPurposes":[7,10],"flexiblePurposes":[2,7,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://deltaprojects.com/data-collection-policy"},"195":{"id":195,"name":"advanced store GmbH","purposes":[1,3,4],"legIntPurposes":[2],"flexiblePurposes":[2],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://www.advanced-store.com/de/datenschutz/"},"190":{"id":190,"name":"video intelligence AG","purposes":[],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.vi.ai/privacy-policy/"},"84":{"id":84,"name":"Semasio GmbH","purposes":[1,3,9,10],"legIntPurposes":[],"flexiblePurposes":[3,9,10],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"http://www.semasio.com/privacy-policy/"},"65":{"id":65,"name":"Location Sciences AI Ltd.","purposes":[1,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[],"specialFeatures":[1],"policyUrl":"https://www.locationsciencesgroup.ai/privacy-policy/"},"210":{"id":210,"name":"Zemanta, Inc.","purposes":[1],"legIntPurposes":[3,7,9,10],"flexiblePurposes":[3,7,9,10],"specialPurposes":[1,2],"features":[1],"specialFeatures":[],"policyUrl":"http://www.zemanta.com/legal/privacy"},"200":{"id":200,"name":"Tapjoy, Inc.","purposes":[1],"legIntPurposes":[2,3,4,7],"flexiblePurposes":[2,3,4,7],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.tapjoy.com/legal/#privacy-policy"},"217":{"id":217,"name":"2KDirect, Inc. (dba iPromote)","purposes":[1],"legIntPurposes":[2,3,4,7,9,10],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.ipromote.com/privacy-policy/"},"194":{"id":194,"name":"Rezonence Limited","purposes":[1,3,4,9],"legIntPurposes":[2,7,8,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://rezonence.com/privacy-policy/"},"226":{"id":226,"name":"Publicis Media GmbH","purposes":[1,9],"legIntPurposes":[2,3,4,5,6,7,8,10],"flexiblePurposes":[2,3,4,5,6,7,8,10],"specialPurposes":[],"features":[1],"specialFeatures":[],"policyUrl":"https://www.publicismedia.de/datenschutz/"},"227":{"id":227,"name":"ORTEC B.V.","purposes":[1,2,3,4,5,6,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.ortecadscience.com/privacy-policy/"},"205":{"id":205,"name":"Adssets AB","purposes":[1,3,4,5,8,9],"legIntPurposes":[2,7],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"http://adssets.com/policy/"},"179":{"id":179,"name":"Collective Europe Ltd.","purposes":[1,2,3,4,9],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://www.collectiveuk.com/privacy.html","overflow":{"httpGetLimit":128}},"31":{"id":31,"name":"Ogury Ltd.","purposes":[1,3,4,9],"legIntPurposes":[2,7,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2],"specialFeatures":[],"policyUrl":"https://www.ogury.com/privacy-policy/"},"92":{"id":92,"name":"1plusX AG","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.1plusx.com/privacy-policy/"},"155":{"id":155,"name":"AntVoice","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://www.antvoice.com/en/privacypolicy/"},"115":{"id":115,"name":"smartclip Europe GmbH","purposes":[1,3,4],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,3,4,7,10],"specialPurposes":[1,2],"features":[2],"specialFeatures":[],"policyUrl":"https://privacy-portal.smartclip.net/","overflow":{"httpGetLimit":32}},"126":{"id":126,"name":"DoubleVerify Inc.\u200b","purposes":[2,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.doubleverify.com/privacy/"},"193":{"id":193,"name":"Mediasmart Mobile S.L.","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"http://mediasmart.io/privacy/"},"213":{"id":213,"name":"emetriq GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.emetriq.com/datenschutz/"},"244":{"id":244,"name":"Temelio","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://temelio.com/vie-privee"},"224":{"id":224,"name":"adrule mobile GmbH","purposes":[1,2,3,4,5,6,8],"legIntPurposes":[7],"flexiblePurposes":[2,3,4,5,6,7,8],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://www.adrule.net/de/datenschutz/"},"174":{"id":174,"name":"A Million Ads Ltd","purposes":[2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[1],"policyUrl":"https://www.amillionads.com/privacy-policy"},"192":{"id":192,"name":"remerge GmbH","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://remerge.io/privacy-policy.html"},"256":{"id":256,"name":"Bounce Exchange, Inc","purposes":[1],"legIntPurposes":[7,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.bouncex.com/privacy/"},"234":{"id":234,"name":"ZBO Media","purposes":[1,3,4,9],"legIntPurposes":[2,7,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1],"specialFeatures":[],"policyUrl":"https://zbo.media/mentions-legales/politique-de-confidentialite-service-publicitaire/"},"246":{"id":246,"name":"Smartology Limited","purposes":[1,3,4,8],"legIntPurposes":[2,7],"flexiblePurposes":[2,7],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.smartology.net/privacy-policy/"},"241":{"id":241,"name":"OneTag Limited","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[7],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.onetag.com/privacy/"},"254":{"id":254,"name":"LiquidM Technology GmbH","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://liquidm.com/privacy-policy/"},"215":{"id":215,"name":"ARMIS SAS","purposes":[1,2,7],"legIntPurposes":[10],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://armis.tech/en/armis-personal-data-privacy-policy/"},"167":{"id":167,"name":"Audiens S.r.l.","purposes":[1,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"http://www.audiens.com/privacy"},"240":{"id":240,"name":"7Hops.com Inc. (ZergNet)","purposes":[1],"legIntPurposes":[5,6,8,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://zergnet.com/privacy"},"235":{"id":235,"name":"Bucksense Inc","purposes":[2,3,4,7,9],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,9],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"http://www.bucksense.com/platform-privacy-policy/"},"185":{"id":185,"name":"Bidtellect, Inc","purposes":[1,2,3,4,7,8],"legIntPurposes":[9,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.bidtellect.com/privacy-policy/","overflow":{"httpGetLimit":128}},"211":{"id":211,"name":"AdTheorent, Inc","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"http://adtheorent.com/privacy-policy"},"273":{"id":273,"name":"Bannerflow AB","purposes":[1],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.bannerflow.com/privacy "},"104":{"id":104,"name":"Sonobi, Inc","purposes":[1,2,3,4],"legIntPurposes":[7,8],"flexiblePurposes":[],"specialPurposes":[2],"features":[1],"specialFeatures":[],"policyUrl":"http://sonobi.com/privacy-policy/"},"162":{"id":162,"name":"Unruly Group Ltd","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://unruly.co/privacy/"},"249":{"id":249,"name":"Spolecznosci Sp. z o.o. Sp. k.","purposes":[1,2,3,4,5,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.spolecznosci.pl/polityka-prywatnosci"},"160":{"id":160,"name":"Netsprint SA","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://netsprint.eu/privacy.html","overflow":{"httpGetLimit":32}},"279":{"id":279,"name":"Mirando GmbH & Co KG","purposes":[1],"legIntPurposes":[2,7],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://wwwmirando.de/datenschutz/"},"255":{"id":255,"name":"Onnetwork Sp. z o.o.","purposes":[1,5,6],"legIntPurposes":[2,7,8],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://www.onnetwork.tv/pp_services.php"},"203":{"id":203,"name":"Revcontent, LLC","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,9,10],"flexiblePurposes":[2,3,4,5,6],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://intercom.help/revcontent2/en/articles/2290675-revcontent-s-privacy-policy"},"274":{"id":274,"name":"Golden Bees","purposes":[1,2,3,4,6,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"https://www.goldenbees.fr/en/privacy-charter/","overflow":{"httpGetLimit":128}},"280":{"id":280,"name":"Spot.IM LTD","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.spot.im/privacy/"},"239":{"id":239,"name":"Triton Digital Canada Inc.","purposes":[1,3,4,5,6],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[2,7,8,9,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.tritondigital.com/privacy-policies"},"177":{"id":177,"name":"plista GmbH","purposes":[1],"legIntPurposes":[7,8,10],"flexiblePurposes":[7,8,10],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://www.plista.com/about/privacy/"},"150":{"id":150,"name":"Inskin Media LTD","purposes":[1,3,4,9,10],"legIntPurposes":[2,7],"flexiblePurposes":[2,7],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"http://www.inskinmedia.com/privacy-policy.html"},"252":{"id":252,"name":"Jaduda GmbH","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.jadudamobile.com/datenschutzerklaerung/"},"248":{"id":248,"name":"Converge-Digital","purposes":[1],"legIntPurposes":[2],"flexiblePurposes":[2],"specialPurposes":[],"features":[3],"specialFeatures":[],"policyUrl":"https://converge-digital.com/privacy-policy/","overflow":{"httpGetLimit":32}},"161":{"id":161,"name":"Smadex SL","purposes":[1,2,3,4,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://smadex.com/end-user-privacy-policy/"},"285":{"id":285,"name":"Comcast International France SAS","purposes":[1],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.freewheel.com/privacy-policy"},"228":{"id":228,"name":"McCann Discipline LTD","purposes":[1,2,5,6,7,8],"legIntPurposes":[],"flexiblePurposes":[2,5,6,7,8],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.primis.tech/privacy-policy/"},"299":{"id":299,"name":"AdClear GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.adclear.de/datenschutzerklaerung/"},"277":{"id":277,"name":"Codewise VL Sp. z o.o. Sp. k","purposes":[1,2,3,4,7,9],"legIntPurposes":[10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://voluumdsp.com/end-user-privacy-policy/"},"259":{"id":259,"name":"ADYOULIKE SA","purposes":[1,3,4],"legIntPurposes":[2,7,8],"flexiblePurposes":[8],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.adyoulike.com/privacy_policy.php"},"289":{"id":289,"name":"mobalo GmbH","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1],"specialFeatures":[1],"policyUrl":"https://www.mobalo.com/en/privacy/"},"272":{"id":272,"name":"A.Mob","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.we-are-adot.com/privacy-policy/"},"253":{"id":253,"name":"Improve Digital BV","purposes":[1,3,4,9],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,7,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.improvedigital.com/platform-privacy-policy","overflow":{"httpGetLimit":128}},"304":{"id":304,"name":"On Device Research Limited","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://s.on-device.com/privacyPolicy"},"314":{"id":314,"name":"Keymantics","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.keymantics.com/assets/privacy-policy.pdf"},"317":{"id":317,"name":"mainADV Srl","purposes":[1,2,3,4,5,6,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://www.mainad.com/privacy-policy/"},"278":{"id":278,"name":"Integral Ad Science, Inc.","purposes":[],"legIntPurposes":[7,10],"flexiblePurposes":[],"specialPurposes":[1],"features":[],"specialFeatures":[],"policyUrl":"https://integralads.com/privacy-policy/"},"315":{"id":315,"name":"Celtra, Inc.","purposes":[1,2,4,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://www.celtra.com/privacy-policy/"},"165":{"id":165,"name":"SpotX, Inc.","purposes":[1,3,4],"legIntPurposes":[2,7,9,10],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.spotx.tv/privacy-policy/","overflow":{"httpGetLimit":32}},"47":{"id":47,"name":"ADMAN - Phaistos Networks, S.A.","purposes":[1,2,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://www.adman.gr/privacy"},"134":{"id":134,"name":"SMARTSTREAM.TV GmbH","purposes":[1],"legIntPurposes":[2,3,4,7,9,10],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.smartstream.tv/en/productprivacy","overflow":{"httpGetLimit":32}},"325":{"id":325,"name":"Knorex","purposes":[1,2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.knorex.com/privacy"},"316":{"id":316,"name":"Gamned","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://www.gamned.com/privacy-policy/"},"318":{"id":318,"name":"Accorp Sp. z o.o.","purposes":[1,3,4,9],"legIntPurposes":[7,10],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"http://www.instytut-pollster.pl/privacy-policy/"},"199":{"id":199,"name":"ADUX","purposes":[1,2,3,4,6,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://www.adux.com/donnees-personelles/"},"294":{"id":294,"name":"Jivox Corporation","purposes":[1,2,3,4,5,7],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,7],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.jivox.com/privacy","overflow":{"httpGetLimit":32}},"143":{"id":143,"name":"Connatix Native Exchange Inc.","purposes":[1,2,4,6,7,8,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[],"policyUrl":"https://connatix.com/privacy-policy/"},"297":{"id":297,"name":"Polar Mobile Group Inc.","purposes":[1,3,4],"legIntPurposes":[2,7,8,10],"flexiblePurposes":[2,7,8,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://privacy.polar.me"},"319":{"id":319,"name":"Clipcentric, Inc.","purposes":[1],"legIntPurposes":[2,7,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://clipcentric.com/privacy.bhtml"},"290":{"id":290,"name":"Readpeak Oy","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"http://readpeak.com/privacy-policy/"},"323":{"id":323,"name":"DAZN Media Services Limited","purposes":[1,3,4,7],"legIntPurposes":[2,8,10],"flexiblePurposes":[8,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.goal.com/en-gb/legal/privacy-policy"},"119":{"id":119,"name":"Fusio by S4M","purposes":[1,3,4,10],"legIntPurposes":[],"flexiblePurposes":[3,4,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"http://www.s4m.io/privacy-policy/"},"302":{"id":302,"name":"Mobile Professionals BV","purposes":[1,2,3,4,5,7,8,9],"legIntPurposes":[],"flexiblePurposes":[3,5,7,8],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"https://mobpro.com/privacy.html"},"212":{"id":212,"name":"usemax advertisement (Emego GmbH)","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://www.usemax.de/?l=privacy"},"264":{"id":264,"name":"Adobe Advertising Cloud","purposes":[1,2,3,4,10],"legIntPurposes":[7],"flexiblePurposes":[2],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.adobe.com/privacy/experience-cloud.html"},"44":{"id":44,"name":"The ADEX GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://theadex.com/privacy-opt-out/"},"282":{"id":282,"name":"Welect GmbH","purposes":[10],"legIntPurposes":[2,7],"flexiblePurposes":[2],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://www.welect.de/datenschutz"},"238":{"id":238,"name":"StackAdapt","purposes":[1,2,3,4],"legIntPurposes":[7,9,10],"flexiblePurposes":[],"specialPurposes":[2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://www.stackadapt.com/privacy"},"284":{"id":284,"name":"WEBORAMA","purposes":[1,3,4],"legIntPurposes":[2,5,6,7,8,9,10],"flexiblePurposes":[2,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://weborama.com/privacy_en/"},"301":{"id":301,"name":"zeotap GmbH","purposes":[1,3,4,5,6,7,9,10],"legIntPurposes":[],"flexiblePurposes":[3,4,5,6,7,9,10],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://zeotap.com/privacy_policy"},"275":{"id":275,"name":"TabMo SAS","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://static.tabmo.io.s3.amazonaws.com/privacy-policy/index.html"},"310":{"id":310,"name":"Adevinta Spain S.L.U.","purposes":[1,2,3,4,9],"legIntPurposes":[7,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.adevinta.com/about/privacy/"},"139":{"id":139,"name":"Permodo GmbH","purposes":[1,2,7,8,10],"legIntPurposes":[],"flexiblePurposes":[2,7,8,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://permodo.com/de/privacy.html","overflow":{"httpGetLimit":32}},"262":{"id":262,"name":"Fyber ","purposes":[1,2,3,4,5,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.fyber.com/legal/privacy-policy/"},"331":{"id":331,"name":"ad6media","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,10],"flexiblePurposes":[2,3,4,5,6],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[1,2],"policyUrl":"https://www.ad6media.fr/privacy"},"345":{"id":345,"name":"The Kantar Group Limited","purposes":[1,2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,8,9,10],"specialPurposes":[2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"http://www.kantar.com/cookies-policies"},"270":{"id":270,"name":"Marfeel Solutions, SL","purposes":[1],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.marfeel.com/privacy-policy/"},"333":{"id":333,"name":"InMobi Pte Ltd","purposes":[1,2,3,4,9,10],"legIntPurposes":[],"flexiblePurposes":[9],"specialPurposes":[1],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.inmobi.com/privacy-policy-for-eea"},"202":{"id":202,"name":"Telaria, Inc","purposes":[1,2,3,4,5,6],"legIntPurposes":[7,8,9,10],"flexiblePurposes":[5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://telaria.com/privacy-policy/"},"328":{"id":328,"name":"Gemius SA","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[],"policyUrl":"https://www.gemius.com/cookie-policy.html"},"281":{"id":281,"name":"Wizaly","purposes":[1,7,8,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.wizaly.com/terms-of-use#privacy-policy"},"354":{"id":354,"name":"Apester Ltd","purposes":[2,4],"legIntPurposes":[6,7,8,9,10],"flexiblePurposes":[7],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://apester.com/privacy-policy/"},"359":{"id":359,"name":"AerServ LLC","purposes":[1,2,3,4,9,10],"legIntPurposes":[],"flexiblePurposes":[9],"specialPurposes":[1],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.inmobi.com/privacy-policy-for-eea"},"265":{"id":265,"name":"Instinctive, Inc.","purposes":[1,2,3,4,5,6,7,8],"legIntPurposes":[9,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://instinctive.io/privacy"},"303":{"id":303,"name":"Orion Semantics","purposes":[1,2,3,4,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"http://static.orion-semantics.com/privacy.html"},"261":{"id":261,"name":"Signal Digital Inc.","purposes":[1],"legIntPurposes":[3,5,7,8],"flexiblePurposes":[3],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.signal.co/privacy-policy/"},"83":{"id":83,"name":"Visarity Technologies GmbH","purposes":[2,6,7,8],"legIntPurposes":[],"flexiblePurposes":[2],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://primo.design/docs/PrivacyPolicyPrimo.html"},"343":{"id":343,"name":"DIGITEKA Technologies","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[1],"policyUrl":"https://www.ultimedia.com/POLICY.html"},"231":{"id":231,"name":"AcuityAds Inc.","purposes":[1,3,4],"legIntPurposes":[2,7,8,10],"flexiblePurposes":[3,4],"specialPurposes":[1,2],"features":[2],"specialFeatures":[],"policyUrl":"https://privacy.acuityads.com/corporate-privacy-policy.html","overflow":{"httpGetLimit":128}},"216":{"id":216,"name":"Mindlytix SAS","purposes":[1,2,3,5,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[1,2],"specialFeatures":[],"policyUrl":"http://mindlytix.com/privacy/"},"360":{"id":360,"name":"Permutive Technologies, Inc.","purposes":[1],"legIntPurposes":[3,5,7,8,9],"flexiblePurposes":[3,5,7,8,9],"specialPurposes":[],"features":[1,2],"specialFeatures":[],"policyUrl":"https://permutive.com/privacy/"},"361":{"id":361,"name":"Permutive Limited","purposes":[1],"legIntPurposes":[3,5,7,8,9],"flexiblePurposes":[3,5,7,8,9],"specialPurposes":[],"features":[1,2],"specialFeatures":[],"policyUrl":"https://permutive.com/privacy/"},"311":{"id":311,"name":"Mobfox US LLC","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://www.mobfox.com/privacy-policy/"},"358":{"id":358,"name":"MGID Inc.","purposes":[2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.mgid.com/privacy-policy"},"152":{"id":152,"name":"Meetrics GmbH","purposes":[1,9],"legIntPurposes":[7],"flexiblePurposes":[7],"specialPurposes":[1],"features":[],"specialFeatures":[],"policyUrl":"https://www.meetrics.com/en/data-privacy/"},"251":{"id":251,"name":"Yieldlove GmbH","purposes":[1],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,7,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://www.yieldlove.com/cookie-policy"},"371":{"id":371,"name":"Seeding Alliance GmbH","purposes":[1,3,4,5,6],"legIntPurposes":[2,7,8,10],"flexiblePurposes":[2,7,8,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://seeding-alliance.de/datenschutz/"},"347":{"id":347,"name":"Ezoic Inc.","purposes":[1,7,8,9],"legIntPurposes":[6,10],"flexiblePurposes":[6,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.ezoic.com/terms/"},"218":{"id":218,"name":"Bigabid Media ltd","purposes":[1,3,4],"legIntPurposes":[2,5,6,7,8,9,10],"flexiblePurposes":[],"specialPurposes":[1],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.bigabid.com/privacy-policy"},"350":{"id":350,"name":"Free Stream Media Corp. dba Samba TV","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://samba.tv/legal/privacy-policy/","overflow":{"httpGetLimit":128}},"351":{"id":351,"name":"Samba TV UK Limited","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://samba.tv/legal/privacy-policy/","overflow":{"httpGetLimit":128}},"380":{"id":380,"name":"Vidoomy Media SL","purposes":[2,3,4,5,6,7,8],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"http://vidoomy.com/privacy-policy.html"},"378":{"id":378,"name":"communicationAds GmbH & Co. KG","purposes":[1],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.communicationads.net/aboutus/privacy/"},"184":{"id":184,"name":"mediarithmics SAS","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,7],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.mediarithmics.com/en-us/content/privacy-policy"},"368":{"id":368,"name":"VECTAURY","purposes":[1,2,3,4,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.vectaury.io/en/personal-data"},"373":{"id":373,"name":"Nielsen Marketing Cloud","purposes":[1,3,5],"legIntPurposes":[7,8,9,10],"flexiblePurposes":[],"specialPurposes":[1],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://www.nielsen.com/us/en/privacy-statement/exelate-privacy-policy.html"},"388":{"id":388,"name":"numberly","purposes":[1,3,4,5,6,9],"legIntPurposes":[2,7,8,10],"flexiblePurposes":[2,7,8,10],"specialPurposes":[1],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://numberly.com/en/privacy/"},"250":{"id":250,"name":"Qriously Ltd","purposes":[2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://www.brandwatch.com/legal/qriously-privacy-notice/"},"223":{"id":223,"name":"Audience Trading Platform Ltd.","purposes":[1],"legIntPurposes":[7,8],"flexiblePurposes":[7,8],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"https://atp.io/privacy-policy"},"384":{"id":384,"name":"Pixalate, Inc.","purposes":[],"legIntPurposes":[7,8,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1,2],"policyUrl":"https://pixalate.com/privacypolicy/"},"387":{"id":387,"name":"Triapodi Ltd.","purposes":[2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[],"policyUrl":"https://appreciate.mobi/page.html#/end-user-privacy-policy"},"312":{"id":312,"name":"Exactag GmbH","purposes":[1,3,7,8],"legIntPurposes":[],"flexiblePurposes":[3,7,8],"specialPurposes":[1],"features":[2],"specialFeatures":[],"policyUrl":"https://www.exactag.com/en/data-privacy/","overflow":{"httpGetLimit":128}},"178":{"id":178,"name":"Hybrid Theory","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[7,8,9,10],"specialPurposes":[1],"features":[],"specialFeatures":[],"policyUrl":"https://hybridtheory.com/privacy-policy/"},"377":{"id":377,"name":"AddApptr GmbH","purposes":[1],"legIntPurposes":[7,10],"flexiblePurposes":[7,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://www.addapptr.com/data-privacy"},"382":{"id":382,"name":"The Reach Group GmbH","purposes":[1,3,4,5,6,9],"legIntPurposes":[2,7,8],"flexiblePurposes":[2,3,4,5,6,7,8,9],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://trg.de/en/privacy-statement/","overflow":{"httpGetLimit":128}},"206":{"id":206,"name":"Hybrid Adtech GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://hybrid.ai/data_protection_policy"},"385":{"id":385,"name":"Oracle Data Cloud","purposes":[1,3,5,9,10],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.oracle.com/legal/privacy/marketing-cloud-data-cloud-privacy-policy.html"},"242":{"id":242,"name":"twiago GmbH","purposes":[1,2,3,4,7,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.twiago.com/datenschutz/","overflow":{"httpGetLimit":32}},"402":{"id":402,"name":"Effiliation","purposes":[1,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://inter.effiliation.com/politique-confidentialite.html"},"413":{"id":413,"name":"Eulerian Technologies","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.eulerian.com/en/privacy/"},"415":{"id":415,"name":"Seenthis AB","purposes":[],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://seenthis.co/privacy-notice-2018-04-18.pdf"},"263":{"id":263,"name":"Nativo, Inc.","purposes":[1,3,4],"legIntPurposes":[2,7,9,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2],"specialFeatures":[],"policyUrl":"https://www.nativo.com/interest-based-ads"},"329":{"id":329,"name":"Browsi Mobile Ltd","purposes":[1,7,8],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://gobrowsi.com/browsi-privacy-policy/"},"337":{"id":337,"name":"SheMedia, LLC","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2,3],"specialFeatures":[],"policyUrl":"https://www.shemedia.com/ad-services-privacy-policy"},"422":{"id":422,"name":"Brand Metrics Sweden AB","purposes":[1,6,7,8,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://collector.brandmetrics.com/brandmetrics_privacypolicy.pdf"},"394":{"id":394,"name":"AudienceProject Aps","purposes":[1,3,4,5,6],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[2,7,8,9],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://privacy.audienceproject.com"},"243":{"id":243,"name":"Cloud Technologies S.A.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.cloudtechnologies.pl/en/internet-advertising-privacy-policy"},"416":{"id":416,"name":"Commanders Act","purposes":[1,2,3,4,5,6,7,8,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.commandersact.com/en/privacy/"},"434":{"id":434,"name":"DynAdmic","purposes":[1,2,4,7],"legIntPurposes":[3,6,8],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[1],"policyUrl":"http://eu.dynadmic.com/privacy-policy/"},"435":{"id":435,"name":"SINGLESPOT SAS ","purposes":[1,3,4,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.singlespot.com/privacy_policy?locale=fr"},"409":{"id":409,"name":"Arrivalist Co.","purposes":[1,7,8,9],"legIntPurposes":[],"flexiblePurposes":[7,8],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.arrivalist.com/privacy","deletedDate":"2020-08-18T00:00:00Z"},"436":{"id":436,"name":"INVIBES GROUP","purposes":[1,3,4,5,6,9],"legIntPurposes":[2,7,8,10],"flexiblePurposes":[2,7,8,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"http://www.invibes.com/terms"},"418":{"id":418,"name":"PROXISTORE","purposes":[1,2,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[1,2],"policyUrl":"https://www.proxistore.com/common/en/cgv"},"429":{"id":429,"name":"Signals","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://signalsdata.com/platform-cookie-policy/"},"335":{"id":335,"name":"Beachfront Media LLC","purposes":[1],"legIntPurposes":[2,3,4,7,8,9],"flexiblePurposes":[2,3,4,7,8,9],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[1],"policyUrl":"http://beachfront.com/privacy-policy/"},"374":{"id":374,"name":"Bmind a Sales Maker Company, S.L.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[],"policyUrl":"http://www.bmind.es/legal-notice/"},"438":{"id":438,"name":"INVIDI technologies AB","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[],"features":[1,2],"specialFeatures":[],"policyUrl":"http://www.invidi.com/wp-content/uploads/2020/02/ad-tech-services-privacy-policy.pdf"},"450":{"id":450,"name":"Neodata Group srl","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.neodatagroup.com/en/security-policy","overflow":{"httpGetLimit":32}},"444":{"id":444,"name":"Playbuzz Ltd (aka EX.CO)","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,9,10],"flexiblePurposes":[2,3,5,6],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://ex.co/privacy-policy/"},"412":{"id":412,"name":"Cxense ASA","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.cxense.com/about-us/privacy-policy"},"455":{"id":455,"name":"GDMServices, Inc. d/b/a FiksuDSP","purposes":[1,2,3,4,10],"legIntPurposes":[7,8],"flexiblePurposes":[2,3,4,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://fiksu.com/privacy-policy/"},"423":{"id":423,"name":"travel audience GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://travelaudience.com/product-privacy-policy/"},"381":{"id":381,"name":"Solocal","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://frontend.adhslx.com/privacy.html?"},"365":{"id":365,"name":"Forensiq LLC","purposes":[],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[1],"features":[1,3],"specialFeatures":[1],"policyUrl":"https://impact.com/privacy-policy/"},"447":{"id":447,"name":"Adludio Ltd","purposes":[2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1],"specialFeatures":[1],"policyUrl":"https://www.adludio.com/privacy-policy/"},"410":{"id":410,"name":"Adtelligent Inc.","purposes":[1,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://adtelligent.com/privacy-policy/"},"137":{"id":137,"name":"Str\u00f6er SSP GmbH (DSP)","purposes":[1],"legIntPurposes":[2,3,4,7,9,10],"flexiblePurposes":[2,3,4,9],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://www.stroeer.de/fileadmin/de/Konvergenz_und_Konzepte/Daten_und_Technologien/Stroeer_SSP/Downloads/Datenschutz_Stroeer_SSP.pdf"},"462":{"id":462,"name":"Bidstack Limited","purposes":[2,3,4,5,6,7,8,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,10],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"https://www.bidstack.com/privacy-policy/"},"466":{"id":466,"name":"TACTIC\u2122 Real-Time Marketing AS","purposes":[],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://tacticrealtime.com/privacy/"},"431":{"id":431,"name":"White Ops, Inc.","purposes":[],"legIntPurposes":[7,9,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,3],"specialFeatures":[2],"policyUrl":"https://www.whiteops.com/privacy"},"336":{"id":336,"name":"Telecoming S.A.","purposes":[2,4],"legIntPurposes":[7,9],"flexiblePurposes":[2,4],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://www.telecoming.com/privacy-policy/","overflow":{"httpGetLimit":128}},"440":{"id":440,"name":"DEFINE MEDIA GMBH","purposes":[1,4],"legIntPurposes":[2,7,9,10],"flexiblePurposes":[2,7,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://www.definemedia.de/datenschutz-conative/"},"375":{"id":375,"name":"Affle International","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"https://affle.com/privacy-policy "},"475":{"id":475,"name":"TAPTAP Digital SL","purposes":[1,2,3,4,5,6,7],"legIntPurposes":[8,10],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://www.taptapnetworks.com/privacy_policy/"},"448":{"id":448,"name":"Targetspot Belgium SPRL","purposes":[1,2,3,4,7,9],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,9],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"http://marketing.targetspot.com/Targetspot/Legal/TargetSpot%20Privacy%20Policy%20-%20June%202018.pdf","overflow":{"httpGetLimit":128}},"428":{"id":428,"name":"Internet BillBoard a.s.","purposes":[1],"legIntPurposes":[2,3,4,7],"flexiblePurposes":[2,3,4],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://www.ibillboard.com/en/privacy-information/"},"486":{"id":486,"name":"Madington","purposes":[1],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://delivered-by-madington.com/dat-privacy-policy/"},"468":{"id":468,"name":"NeuStar, Inc.","purposes":[1],"legIntPurposes":[3,7,8,9,10],"flexiblePurposes":[7,8,9,10],"specialPurposes":[1],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.home.neustar/privacy"},"458":{"id":458,"name":"AdColony, Inc.","purposes":[1,3,4],"legIntPurposes":[2,7,9,10],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"adcolony.com/privacy-policy/"},"293":{"id":293,"name":"SpringServe, LLC","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[3],"specialFeatures":[2],"policyUrl":"https://springserve.com/privacy-policy/"},"484":{"id":484,"name":"STRIATUM SAS","purposes":[],"legIntPurposes":[7,10],"flexiblePurposes":[],"specialPurposes":[1],"features":[],"specialFeatures":[],"policyUrl":"https://adledge.com/data-privacy/"},"493":{"id":493,"name":"Carbon (AI) Limited","purposes":[1,2,3,4,5,6,7,8,10],"legIntPurposes":[],"flexiblePurposes":[2,7,8],"specialPurposes":[2],"features":[1,2,3],"specialFeatures":[2],"policyUrl":"https://carbonrmp.com/privacy.html"},"495":{"id":495,"name":"Arcspire Limited","purposes":[2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"https://public.arcspire.io/privacy.pdf","overflow":{"httpGetLimit":128}},"424":{"id":424,"name":"KUPONA GmbH","purposes":[4,7],"legIntPurposes":[],"flexiblePurposes":[4],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.kupona.de/dsgvo/"},"408":{"id":408,"name":"Fidelity Media","purposes":[1,2,7],"legIntPurposes":[],"flexiblePurposes":[2,7],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://fidelity-media.com/privacy-policy/"},"467":{"id":467,"name":"Haensel AMS GmbH","purposes":[1],"legIntPurposes":[7],"flexiblePurposes":[7],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"https://haensel-ams.com/data-privacy/"},"488":{"id":488,"name":"Opinary GmbH","purposes":[1],"legIntPurposes":[2,7,8,10],"flexiblePurposes":[2,7,8,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://opinary.com/privacy-policy/"},"490":{"id":490,"name":"PLAYGROUND XYZ EMEA LTD","purposes":[1,2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://playground.xyz/privacy"},"491":{"id":491,"name":"Triboo Data Analytics","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.shinystat.com/it/informativa_privacy_generale.html"},"502":{"id":502,"name":"NEXD","purposes":[1,7,10],"legIntPurposes":[9],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://nexd.com/privacy-policy","overflow":{"httpGetLimit":128}},"508":{"id":508,"name":"Lucid Holdings, LLC","purposes":[1,7,8,9],"legIntPurposes":[],"flexiblePurposes":[7,8,9],"specialPurposes":[1],"features":[1,2,3],"specialFeatures":[],"policyUrl":"luc.id/privacy-policy"},"512":{"id":512,"name":"PubNative GmbH","purposes":[1,2,3,4,7,8,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[2],"policyUrl":"https://pubnative.net/privacy-notice/"},"516":{"id":516,"name":"Pexi B.V.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://pexi.nl/privacy-policy/"},"507":{"id":507,"name":"AdsWizz Inc.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.adswizz.com/our-privacy-policy/"},"482":{"id":482,"name":"UberMedia, Inc.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://ubermedia.com/summary-of-privacy-policy/"},"505":{"id":505,"name":"Shopalyst Inc","purposes":[1,7,8],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://www.shortlyst.com/eu/privacy_terms.html"},"517":{"id":517,"name":"SunMedia ","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.sunmedia.tv/en/cookies"},"511":{"id":511,"name":"Admixer EU GmbH","purposes":[1,2,3,4,5,7,9],"legIntPurposes":[10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://admixer.com/privacy/","overflow":{"httpGetLimit":128}},"479":{"id":479,"name":"INFINIA MOBILE S.L.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"http://www.infiniamobile.com/privacy_policy"},"509":{"id":509,"name":"ATG Ad Tech Group GmbH","purposes":[2,7],"legIntPurposes":[],"flexiblePurposes":[2,7],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://ad-tech-group.com/privacy-policy/"},"521":{"id":521,"name":"netzeffekt GmbH","purposes":[1,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://www.netzeffekt.de/en/imprint"},"524":{"id":524,"name":"The Ozone Project Limited","purposes":[1,2,3,4,5,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2,3],"specialFeatures":[1],"policyUrl":"https://ozoneproject.com/privacy-policy"},"528":{"id":528,"name":"Kayzen","purposes":[1,2,3,4,9,10],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1],"specialFeatures":[],"policyUrl":"https://kayzen.io/data-privacy-policy"},"527":{"id":527,"name":"Jampp LTD","purposes":[1,2,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://jampp.com/privacy.html"},"535":{"id":535,"name":"INNITY","purposes":[1,2,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.innity.com/privacy-policy.php"},"530":{"id":530,"name":"Near Pte Ltd","purposes":[1,2,3,4,5,6,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,9,10],"specialPurposes":[],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://near.co/privacy","overflow":{"httpGetLimit":128}},"539":{"id":539,"name":"AdDefend GmbH","purposes":[1],"legIntPurposes":[2,3,4,7],"flexiblePurposes":[2,3,4,7],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.addefend.com/en/privacy-policy/"},"501":{"id":501,"name":"Alliance Gravity Data Media","purposes":[1,2,3,4,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2],"specialFeatures":[],"policyUrl":"https://www.alliancegravity.com/politiquedeprotectiondesdonneespersonnelles"},"519":{"id":519,"name":"Newsroom AI Ltd","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://studio.nws.ai/privacy"},"531":{"id":531,"name":"Smartclip Hispania SL","purposes":[1,2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2,3],"specialFeatures":[1],"policyUrl":"http://rgpd-smartclip.com/"},"536":{"id":536,"name":"GlobalWebIndex","purposes":[1,7,8,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"http://legal.trendstream.net/non-panellist_privacy_policy"},"544":{"id":544,"name":"Kochava Inc.","purposes":[],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.kochava.com/support-privacy/"},"543":{"id":543,"name":"PaperG, Inc. dba Thunder Industries","purposes":[1,3,4,5,6],"legIntPurposes":[2,7,8],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.makethunder.com/privacy"},"547":{"id":547,"name":"Reach Media GmbH","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.videoreach.com/about/privacy-policy/"},"546":{"id":546,"name":"Smart Traffik","purposes":[1,8],"legIntPurposes":[7],"flexiblePurposes":[7,8],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://okube-attribution.com/politique-de-confidentialite/"},"541":{"id":541,"name":"DeepIntent, Inc.","purposes":[1],"legIntPurposes":[2,3,4,7,9,10],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.deepintent.com/platform-privacy-policy/"},"545":{"id":545,"name":"Reignn Platform Ltd","purposes":[1,2,3,4,7,8,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[2],"policyUrl":"http://reignn.com/user-privacy-policy"},"439":{"id":439,"name":"Bit Q Holdings Limited","purposes":[1,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.rippll.com/privacy"},"553":{"id":553,"name":"Adhese","purposes":[1,2,3,4,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://adhese.com/privacy-and-cookie-policy"},"556":{"id":556,"name":"adhood.com","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"http://v3.adhood.com/en/site/politikavekurallar/gizlilik.php?lang=en"},"550":{"id":550,"name":"Happydemics","purposes":[7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,3],"specialFeatures":[],"policyUrl":"https://www.iubenda.com/privacy-policy/69056167/full-legal"},"554":{"id":554,"name":"RMSi Radio Marketing Service interactive GmbH","purposes":[1,3,4],"legIntPurposes":[2,7,9],"flexiblePurposes":[2],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.rms.de/datenschutz/"},"498":{"id":498,"name":"Mediakeys Platform","purposes":[1,6,10],"legIntPurposes":[2,3,4,7,9],"flexiblePurposes":[2,3,4,9],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://drbanner.com/privacypolicy_en/"},"565":{"id":565,"name":"Adobe Audience Manager, Adobe Experience Platform","purposes":[1,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[],"specialFeatures":[],"policyUrl":"https://www.adobe.com/privacy/policy.html"},"571":{"id":571,"name":"ViewPay","purposes":[1,2,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,4,5,6,7,8,9,10],"specialPurposes":[],"features":[3],"specialFeatures":[],"policyUrl":"http://viewpay.tv/mentions-legales/"},"568":{"id":568,"name":"Jointag S.r.l.","purposes":[1,2,3,4,5,6,7,8],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1,2],"policyUrl":"https://www.jointag.com/privacy/kariboo/publisher/third/"},"570":{"id":570,"name":"Czech Publisher Exchange z.s.p.o.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.cpex.cz/pro-uzivatele/ochrana-soukromi/"},"559":{"id":559,"name":"Otto (GmbH & Co KG)","purposes":[1],"legIntPurposes":[2,3,4,7,9,10],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.otto.de/shoppages/service/datenschutz"},"569":{"id":569,"name":"Kairos Fire","purposes":[1,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://www.kairosfire.com/privacy"},"577":{"id":577,"name":"Neustar on behalf of The Procter & Gamble Company","purposes":[1,2,3,4,6,9],"legIntPurposes":[5,7,8],"flexiblePurposes":[2,3,4,5,6,7,8,9],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.pg.com/privacy/english/privacy_statement.shtml"},"590":{"id":590,"name":"Sourcepoint Technologies, Inc.","purposes":[],"legIntPurposes":[6,8],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.sourcepoint.com/privacy-policy"},"587":{"id":587,"name":"Localsensor B.V.","purposes":[1,2,3,4,7,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.localsensor.com/privacy.html"},"580":{"id":580,"name":"Goldbach Group AG","purposes":[1],"legIntPurposes":[2,3,4,7,8,9,10],"flexiblePurposes":[2,3,4,7,8,9,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://goldbach.com/ch/de/datenschutz"},"593":{"id":593,"name":"Programatica de publicidad S.L.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://datmean.com/politica-privacidad/"},"574":{"id":574,"name":"Realeyes OU","purposes":[1,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1],"specialFeatures":[],"policyUrl":"https://realview.realeyesit.com/privacy"},"598":{"id":598,"name":"audio content & control GmbH","purposes":[1],"legIntPurposes":[2,7,9],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://www.audio-cc.com/audiocc_privacy_policy.pdf"},"596":{"id":596,"name":"InsurAds Technologies SA.","purposes":[1,2,3,4,5,6,9,10],"legIntPurposes":[7,8],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://www.insurads.com/privacy.html"},"549":{"id":549,"name":"Bandsintown Amplified LLC","purposes":[2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"http://corp.bandsintown.com/privacy"},"584":{"id":584,"name":"Dynamic 1001 GmbH","purposes":[1,7,10],"legIntPurposes":[],"flexiblePurposes":[7,10],"specialPurposes":[2],"features":[3],"specialFeatures":[2],"policyUrl":"https://dynamic-tracking.com/Datenschutz.aspx"},"601":{"id":601,"name":"WebAds B.V","purposes":[1,2],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://privacy.webads.eu/"},"599":{"id":599,"name":"Maximus Live LLC","purposes":[],"legIntPurposes":[7,8],"flexiblePurposes":[],"specialPurposes":[1],"features":[],"specialFeatures":[1],"policyUrl":"https://maximusx.com/privacy-policy/"},"606":{"id":606,"name":"Impactify ","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://impactify.io/privacy-policy/"},"602":{"id":602,"name":"Online Solution Int Limited","purposes":[1,2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,8,9,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://adsafety.net/privacy.html","overflow":{"httpGetLimit":32}},"612":{"id":612,"name":"Adnami Aps","purposes":[],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"www.adnami.io/privacy"},"591":{"id":591,"name":"Consumable, Inc.","purposes":[1,2,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"http://consumable.com/privacy-policy.html"},"614":{"id":614,"name":"Market Resource Partners LLC","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[2],"policyUrl":"https://www.mrpfd.com/privacy-policy/"},"615":{"id":615,"name":"Adsolutions BV","purposes":[],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.adsolutions.com/privacy-policy/"},"607":{"id":607,"name":"ucfunnel Co., Ltd.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[1],"policyUrl":"https://www.ucfunnel.com/privacy-policy"},"609":{"id":609,"name":"Predicio","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"http://www.predic.io/privacy","overflow":{"httpGetLimit":128}},"617":{"id":617,"name":"Onfocus (Adagio)","purposes":[1,2,7,8],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://adagio.io/privacy"},"620":{"id":620,"name":"Blue","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[1],"policyUrl":"http://www.getblue.io/privacy/"},"610":{"id":610,"name":"Azerion Holding B.V.","purposes":[1,3,4,9],"legIntPurposes":[2,5,6,7,8,10],"flexiblePurposes":[2,5,6],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[1],"policyUrl":"https://azerion.com/business/privacy.html","overflow":{"httpGetLimit":128}},"621":{"id":621,"name":"Seznam.cz, a.s.","purposes":[1,3,4,5,6],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[],"specialPurposes":[],"features":[2,3],"specialFeatures":[1,2],"policyUrl":"https://www.seznam.cz/ochranaudaju"},"624":{"id":624,"name":"Norstat AS","purposes":[1,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.norstatpanel.com/en/data-protection"},"95":{"id":95,"name":"Lotame Solutions, inc","purposes":[1,3,5],"legIntPurposes":[7,8,9,10],"flexiblePurposes":[],"specialPurposes":[],"features":[2,3],"specialFeatures":[],"policyUrl":"https://www.lotame.com/about-lotame/privacy/lotame-corporate-websites-privacy-policy/"},"618":{"id":618,"name":"BEINTOO SPA","purposes":[1,2,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1],"specialFeatures":[1],"policyUrl":"http://www.beintoo.com/privacy-cookie-policy/"},"625":{"id":625,"name":"BILENDI SA","purposes":[1,6,7,8,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1],"specialFeatures":[],"policyUrl":"https://www.maximiles.com/privacy-policy"},"628":{"id":628,"name":": Tappx","purposes":[1,2,4,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2],"specialFeatures":[1,2],"policyUrl":"https://www.tappx.com/en/privacy-policy/"},"630":{"id":630,"name":"Contact Impact GmbH","purposes":[1],"legIntPurposes":[2,3,4,7,10],"flexiblePurposes":[2,3,4,7,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://contactimpact.de/privacy"},"626":{"id":626,"name":"Hivestack Inc.","purposes":[1,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://hivestack.com/privacy-policy"},"631":{"id":631,"name":"Relay42 Netherlands B.V.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://relay42.com/privacy"},"638":{"id":638,"name":"Passendo Aps","purposes":[1,2,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://passendo.com/users-privacy-policy"},"644":{"id":644,"name":"Gamoshi LTD","purposes":[2,7],"legIntPurposes":[],"flexiblePurposes":[2,7],"specialPurposes":[2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.gamoshi.com/privacy-policy"},"639":{"id":639,"name":"Smile Wanted Group","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[2],"policyUrl":"https://www.smilewanted.com/privacy.php"},"645":{"id":645,"name":"Noster Finance S.L.","purposes":[1,2,3,4,5,6,9,10],"legIntPurposes":[7,8],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.finect.com/terminos-legales/politica-de-cookies"},"653":{"id":653,"name":"Smartme Analytics","purposes":[7,8,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,3],"specialFeatures":[],"policyUrl":"http://smartmeapp.com/info/smartme/aviso_legal.php","deletedDate":"2020-07-03T00:00:00Z"},"613":{"id":613,"name":"Adserve.zone / Artworx AS","purposes":[1],"legIntPurposes":[2,7],"flexiblePurposes":[2],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://adserve.zone/adserveprivacypolicy.html"},"573":{"id":573,"name":"Dailymotion SA","purposes":[1,3,4],"legIntPurposes":[2,5,6,7,8,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2],"specialFeatures":[],"policyUrl":"https://www.dailymotion.com/legal/privacy","overflow":{"httpGetLimit":32}},"652":{"id":652,"name":"Skaze","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"http://www.skaze.fr/rgpd/"},"646":{"id":646,"name":"Notify","purposes":[],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1],"specialFeatures":[],"policyUrl":"https://notify-group.com/en/mentions-legales/"},"648":{"id":648,"name":"TrueData Solutions, Inc.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.truedata.co/privacy-policy/"},"647":{"id":647,"name":"Axel Springer Teaser Ad GmbH","purposes":[1],"legIntPurposes":[2,3,4,7,10],"flexiblePurposes":[2,3,4,7,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.adup-tech.com/privacy"},"659":{"id":659,"name":"Research and Analysis of Media in Sweden AB","purposes":[1],"legIntPurposes":[7,8,9],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"https://www2.rampanel.com/privacy-policy/"},"656":{"id":656,"name":"Think Clever Media","purposes":[1,2,3,4,5,6,9],"legIntPurposes":[7,8,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.contentignite.com/privacy-policy/"},"657":{"id":657,"name":"GP One GmbH","purposes":[1],"legIntPurposes":[2,3,4],"flexiblePurposes":[2,3,4],"specialPurposes":[1,2],"features":[],"specialFeatures":[1,2],"policyUrl":"https://www.gsi-one.org/de/privacy-policy.html"},"655":{"id":655,"name":"Sportradar AG","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.sportradar.com/about-us/privacy/"},"662":{"id":662,"name":"SoundCast","purposes":[1,2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://soundcast.fm/en/data-privacy"},"665":{"id":665,"name":"Digital East GmbH","purposes":[2,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.digitaleast.mobi/en/legal/privacy-policy/"},"650":{"id":650,"name":"Telefonica Investigaci\u00f3n y Desarrollo S.A.U","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"http://www.cognitivemarketing.tid.es/"},"666":{"id":666,"name":"BeOp","purposes":[1,2],"legIntPurposes":[7,8,10],"flexiblePurposes":[2,7,8,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://beop.io/privacy"},"663":{"id":663,"name":"Mobsuccess","purposes":[1,2,3,4,7,9],"legIntPurposes":[],"flexiblePurposes":[2,7],"specialPurposes":[1,2],"features":[1],"specialFeatures":[],"policyUrl":"https://www.mobsuccess.com/en/privacy"},"658":{"id":658,"name":"BLIINK SAS","purposes":[1,2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,8,9,10],"specialPurposes":[2],"features":[],"specialFeatures":[1],"policyUrl":"https://bliink.io/privacy-policy"},"667":{"id":667,"name":"Liftoff Mobile, Inc.","purposes":[1,3,4],"legIntPurposes":[2,5,6,7,8,10],"flexiblePurposes":[2,3,4,5,6,7,8,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://liftoff.io/privacy-policy/"},"668":{"id":668,"name":"WhatRocks Inc. ","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[1,2],"policyUrl":"https://www.whatrocks.co/en/privacy-policy "},"674":{"id":674,"name":"Duration Media, LLC.","purposes":[1,2,3,4,5,6,7,8,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.durationmedia.net/privacy-policy"},"675":{"id":675,"name":"Instreamatic inc.","purposes":[2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[3],"specialFeatures":[],"policyUrl":"http://instreamatic.com/privacy-policy/"},"676":{"id":676,"name":"BusinessClick","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://www.businessclick.com/documents/RegulaminProgramuBusinessClick-2019.pdf"},"672":{"id":672,"name":"Cedato Technologies Ltd","purposes":[1,2],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.cedato.com/privacy-policy/"},"664":{"id":664,"name":"adMarketplace, Inc.","purposes":[1,2,3,4],"legIntPurposes":[7,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.admarketplace.com/privacy-policy/"},"561":{"id":561,"name":"AuDigent","purposes":[1,2,3,4,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2,3],"specialFeatures":[],"policyUrl":"http://audigent.com/platform-privacy-policy"},"682":{"id":682,"name":"Radio Net Media Limited","purposes":[1,2,3,4],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.adtonos.com/service-privacy-policy/","overflow":{"httpGetLimit":128}},"684":{"id":684,"name":"Blue Billywig BV","purposes":[],"legIntPurposes":[7],"flexiblePurposes":[7],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://www.bluebillywig.com/privacy-statement/"},"686":{"id":686,"name":"The MediaGrid Inc.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.themediagrid.com/privacy-policy/"},"685":{"id":685,"name":"Arkeero","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://arkeero.com/privacy-2/"},"687":{"id":687,"name":"MISSENA","purposes":[1,2,3,4,5,6,7,8],"legIntPurposes":[],"flexiblePurposes":[2,7],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"http://missena.com/confidentialite/"},"690":{"id":690,"name":"Go.pl sp. z o.o.","purposes":[1,2,3,4],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://go.pl/polityka-prywatnosci/"},"691":{"id":691,"name":"Lifesight Pte. Ltd.","purposes":[2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.lifesight.io/privacy-policy/"},"697":{"id":697,"name":"ADWAYS SAS","purposes":[],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://www.adways.com/confidentialite/?lang=en"},"699":{"id":699,"name":"HyperTV Inc.","purposes":[1,2,4,9],"legIntPurposes":[7,10],"flexiblePurposes":[],"specialPurposes":[2],"features":[1],"specialFeatures":[],"policyUrl":"https://www.hypertvx.com/privacy/"},"703":{"id":703,"name":"MindTake Research GmbH","purposes":[1,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[2,3],"specialFeatures":[],"policyUrl":"https://www.mindtake.com/en/reppublika-privacy-policy"},"706":{"id":706,"name":"VRTCAL Markets Inc","purposes":[],"legIntPurposes":[2],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://vrtcal.com/docs/PrivacyPolicy-Advertising.pdf"},"681":{"id":681,"name":"MyTraffic","purposes":[1,9],"legIntPurposes":[],"flexiblePurposes":[9],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://www.mytraffic.io/en/privacy"},"649":{"id":649,"name":"adality GmbH","purposes":[],"legIntPurposes":[2],"flexiblePurposes":[],"specialPurposes":[],"features":[1],"specialFeatures":[],"policyUrl":"https://adality.de/en/privacy/"},"711":{"id":711,"name":"SITU8ED SA","purposes":[1,3,4,5,6,7,8,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.situ8ed.com/privacy-policy/"},"712":{"id":712,"name":"Inspired Mobile Limited","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://byinspired.com/privacypolicy.pdf"},"688":{"id":688,"name":"Effinity","purposes":[],"legIntPurposes":[2],"flexiblePurposes":[2,7],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.effiliation.com/politique-de-confidentialite/","overflow":{"httpGetLimit":128}},"702":{"id":702,"name":"Kwanko","purposes":[1,2,7,8],"legIntPurposes":[],"flexiblePurposes":[2,7,8],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.kwanko.com/fr/rgpd/"},"714":{"id":714,"name":"Survata Inc.","purposes":[1],"legIntPurposes":[7,9],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.survata.com/respondent-privacy-policy/","overflow":{"httpGetLimit":128}},"713":{"id":713,"name":"Dataseat Ltd","purposes":[2,3,4,5,6,7,8,9],"legIntPurposes":[],"flexiblePurposes":[2,7,8],"specialPurposes":[],"features":[3],"specialFeatures":[],"policyUrl":"https://dataseat.com/privacy-policy"},"716":{"id":716,"name":"OnAudience Ltd","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.onaudience.com/internet-advertising-privacy-policy"},"708":{"id":708,"name":"Dugout Limited ","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://dugout.com/privacy-policy"},"694":{"id":694,"name":"Snapupp Technologies SL","purposes":[2,3,4,5,6,9],"legIntPurposes":[7,8,10],"flexiblePurposes":[10],"specialPurposes":[],"features":[3],"specialFeatures":[],"policyUrl":"https://www.enterprise.noddus.com/privacy-policy","overflow":{"httpGetLimit":128}},"683":{"id":683,"name":"Cookie Market LTD","purposes":[1,3,4,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2,3],"specialFeatures":[1],"policyUrl":"http://cookie.market/privacyPolicy.php"},"720":{"id":720,"name":"AAX LLC","purposes":[1,3,4],"legIntPurposes":[2,7,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://aax.media/privacy/","overflow":{"httpGetLimit":32}},"678":{"id":678,"name":"Axonix LTD","purposes":[2],"legIntPurposes":[7,10],"flexiblePurposes":[2],"specialPurposes":[1,2],"features":[3],"specialFeatures":[1],"policyUrl":"https://axonix.com/privacy-cookie-policy/","overflow":{"httpGetLimit":128}},"719":{"id":719,"name":"Online Advertising Network Sp. z o.o.","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[1],"policyUrl":"https://www.oan.pl/en/privacy-policy"},"707":{"id":707,"name":"Dentsu Aegis Network Italia SpA","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.dentsuaegisnetwork.com/it/it/policies/info-cookie"},"721":{"id":721,"name":"Beaconspark Ltd","purposes":[1,2,3,4,5,7],"legIntPurposes":[6,8],"flexiblePurposes":[2,3,4,5,6,7,8],"specialPurposes":[1,2],"features":[1],"specialFeatures":[],"policyUrl":"https://www.engageya.com/privacy"},"724":{"id":724,"name":"Between Exchange","purposes":[1],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://en.betweenx.com/pdata.pdf"},"728":{"id":728,"name":"Appier PTE Ltd","purposes":[1,3,4,5,6,9],"legIntPurposes":[2,7,8,10],"flexiblePurposes":[2,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.appier.com/privacy-policy/"},"729":{"id":729,"name":"Cavai AS & UK ","purposes":[],"legIntPurposes":[7,8],"flexiblePurposes":[7,8],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://cav.ai/privacy-policy/"},"730":{"id":730,"name":"INFOnline GmbH","purposes":[],"legIntPurposes":[8],"flexiblePurposes":[8],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.infonline.de/en/privacy-policy/"},"722":{"id":722,"name":"agof - daily campaign facts","purposes":[1,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[],"specialFeatures":[],"policyUrl":"http://www.agof.de/datenschutz/"},"723":{"id":723,"name":"Adzymic Pte Ltd","purposes":[1,2,3,4,7,8],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"http://www.adzymic.co/privacy","overflow":{"httpGetLimit":128}},"725":{"id":725,"name":"Pubfinity LLC","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[],"policyUrl":"https://pubfinity.com/privacy-policy/","overflow":{"httpGetLimit":128}},"733":{"id":733,"name":"Anzu Virtual Reality LTD","purposes":[1,2,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[1],"specialFeatures":[1],"policyUrl":"https://anzu.io/privacy/"},"737":{"id":737,"name":"Monet Engine Inc","purposes":[1,2,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://appmonet.com/privacy-policy/"},"740":{"id":740,"name":"6Sense Insights, Inc.","purposes":[1],"legIntPurposes":[2,3,4,5,7,8,10],"flexiblePurposes":[2,3,4,5,7,8,10],"specialPurposes":[],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://6sense.com/privacy-policy/","overflow":{"httpGetLimit":128}},"744":{"id":744,"name":"Vidazoo Ltd","purposes":[3,4],"legIntPurposes":[2,7,8,10],"flexiblePurposes":[3,4],"specialPurposes":[1,2],"features":[2],"specialFeatures":[2],"policyUrl":"https://vidazoo.gitbook.io/vidazoo-legal/privacy-policy","overflow":{"httpGetLimit":128}},"731":{"id":731,"name":"GeistM Technologies LTD","purposes":[],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.geistm.com/privacy"},"741":{"id":741,"name":"Brand Advance Limited","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2,3],"specialFeatures":[1,2],"policyUrl":"https://www.wearebrandadvance.com/website-privacy-policy"},"735":{"id":735,"name":"Deutsche Post AG","purposes":[1,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1],"specialFeatures":[],"policyUrl":"https://www.deutschepost.de/de/c/consentric/datenschutz.html"},"734":{"id":734,"name":"Cint AB","purposes":[1,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1],"specialFeatures":[],"policyUrl":"https://www.cint.com/participant-privacy-notice"},"709":{"id":709,"name":"NC Audience Exchange, LLC (NewsIQ)","purposes":[1,3,4,5],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[2,3,4,5,7,8,9,10],"specialPurposes":[2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.ncaudienceexchange.com/privacy/"},"739":{"id":739,"name":"Blingby LLC","purposes":[1,8,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://blingby.com/privacy"},"727":{"id":727,"name":"Pinpoll GmbH (Private Limited)","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1],"specialFeatures":[],"policyUrl":"https://www.pinpoll.com/#data_protection_declaration"},"732":{"id":732,"name":"Performax.cz, s.r.o.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"https://reg.tiscali.cz/privacy-policy"},"736":{"id":736,"name":"BidMachine Inc.","purposes":[1,2,3,4,5,6],"legIntPurposes":[7,8,10],"flexiblePurposes":[7,8,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://explorestack.com/privacy-policy/"},"738":{"id":738,"name":"adbility media GmbH","purposes":[2,3,4,9],"legIntPurposes":[7],"flexiblePurposes":[2,3,4,9],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.adbility-media.com/datenschutzerklaerung/"},"742":{"id":742,"name":"Audiencerate LTD","purposes":[1,2,3,5,6],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.audiencerate.com/privacy/"},"743":{"id":743,"name":"MOVIads Sp. z o.o. Sp. k.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2,3],"specialFeatures":[1],"policyUrl":"https://moviads.pl/polityka-prywatnosci/","overflow":{"httpGetLimit":128}},"745":{"id":745,"name":"Justtag Sp. z o.o.","purposes":[1,3,4,9],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://www.justtag.com/pdf/PRIVACY_POLICY_Koalametrics_english.pdf"},"746":{"id":746,"name":"Adxperience SAS","purposes":[4],"legIntPurposes":[2,7],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1],"policyUrl":"https://adxperience.com/privacy-policy/"},"747":{"id":747,"name":"Kairion GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[],"features":[2,3],"specialFeatures":[1,2],"policyUrl":"https://kairion.de/datenschutzbestimmungen/"},"748":{"id":748,"name":"AUDIOMOB LTD","purposes":[1,2,3,4,7,8,9],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,8,9],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://www.audiomob.io/privacy"},"749":{"id":749,"name":"Good-Loop Ltd","purposes":[1,3,4,5,6],"legIntPurposes":[2,7,8,9,10],"flexiblePurposes":[7,8,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://doc.good-loop.com/policy/privacy-policy.html"},"750":{"id":750,"name":"THE NEWCO S.R.L.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.thenewco.it/privacy_policy_servizi_prodotti.html"},"751":{"id":751,"name":"Kiosked Ltd","purposes":[],"legIntPurposes":[2,7],"flexiblePurposes":[2,7],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://kiosked.com/privacy-policy/"},"753":{"id":753,"name":"ScaleMonk Inc.","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.scalemonk.com/privacy-policy/index.html"},"754":{"id":754,"name":"DistroScale, Inc.","purposes":[1,2],"legIntPurposes":[],"flexiblePurposes":[2],"specialPurposes":[2],"features":[3],"specialFeatures":[],"policyUrl":"http://www.distroscale.com/privacy-policy/"},"756":{"id":756,"name":"Fandom, Inc.","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://www.fandom.com/privacy-policy"},"757":{"id":757,"name":"UAB Meazy","purposes":[1,2,3,4,5,7,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://meazy.co/privacy-policy"},"758":{"id":758,"name":"GfK Netherlands B.V.","purposes":[1,7,8,9],"legIntPurposes":[],"flexiblePurposes":[7,8,9],"specialPurposes":[],"features":[1],"specialFeatures":[],"policyUrl":"https://gfkpanel.nl/privacy"},"759":{"id":759,"name":"RevJet","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1],"specialFeatures":[],"policyUrl":"https://www.revjet.com/privacy","overflow":{"httpGetLimit":128}},"760":{"id":760,"name":"VEXPRO TECHNOLOGIES LTD","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[2],"policyUrl":"https://onedash.com/privacy-policy.html"},"761":{"id":761,"name":"Digiseg ApS","purposes":[1,3,5,7,8,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[1],"specialFeatures":[],"policyUrl":"https://digiseg.io/privacy-center/"},"762":{"id":762,"name":"Protected Media LTD","purposes":[1],"legIntPurposes":[3,5,7,8,9,10],"flexiblePurposes":[],"specialPurposes":[1],"features":[3],"specialFeatures":[],"policyUrl":"https://www.protected.media/privacy-policy/"},"764":{"id":764,"name":"Lucidity","purposes":[1,2,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://golucidity.com/privacy-policy/"},"765":{"id":765,"name":"Grabit Interactive Media Inc dba KERV Interctive","purposes":[2,3,4,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2],"specialFeatures":[],"policyUrl":"https://kervit.com/privacy-policy/"},"766":{"id":766,"name":"ADCELL | Firstlead GmbH","purposes":[1,2],"legIntPurposes":[],"flexiblePurposes":[2],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.adcell.de/agb#sector_6"},"767":{"id":767,"name":"Clinch Labs LTD","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1],"policyUrl":"https://clinch.co/pages/privacy.html"},"768":{"id":768,"name":"Global Media & Entertainment Limited","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"http://global.com/privacy-policy/"},"769":{"id":769,"name":"MEDIAMETRIE","purposes":[1,8],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://www.mediametrie.fr/fr/gestion-des-cookies"},"770":{"id":770,"name":"MARKETPERF CORP","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,10],"flexiblePurposes":[2,3,4,5,6],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://www.marketperf.com/assets/images/app/marketperf/pdf/privacy-policy.pdf"},"771":{"id":771,"name":"bam! interactive marketing GmbH ","purposes":[1,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1,2],"policyUrl":"https://bam-interactive.de/privacy-policy/"},"772":{"id":772,"name":"Oracle Data Cloud - Moat","purposes":[],"legIntPurposes":[7,8,10],"flexiblePurposes":[],"specialPurposes":[1],"features":[],"specialFeatures":[],"policyUrl":"https://www.oracle.com/legal/privacy/services-privacy-policy.html"},"773":{"id":773,"name":"360e-com Sp. z o.o.","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"https://www.clickonometrics.com/optout/"},"774":{"id":774,"name":"Wagawin GmbH","purposes":[1],"legIntPurposes":[2,7],"flexiblePurposes":[2],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.wagawin.com/privacy-en/#productprivacy"},"775":{"id":775,"name":"SelectMedia International LTD","purposes":[1,2],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.selectmedia.asia/terms-and-privacy/"},"776":{"id":776,"name":"Mars Media Group","purposes":[2],"legIntPurposes":[],"flexiblePurposes":[2],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"https://pay-per-leads.com/terms"},"777":{"id":777,"name":"One Planet Only","purposes":[1],"legIntPurposes":[2,3,4,7],"flexiblePurposes":[2,3,4,7],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://oneplanetonly.com/files/PRIVACY%20POLICY.pdf","overflow":{"httpGetLimit":32}},"778":{"id":778,"name":"Discover-Tech ltd","purposes":[2,3,4,5,6,7,8,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[1],"policyUrl":"https://discover-tech.io/dsp-privacy-policy/"},"779":{"id":779,"name":"Adtarget Medya A.S.","purposes":[1,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[3],"specialFeatures":[1],"policyUrl":"https://adtarget.com.tr/adtarget-privacy-policy-2020.pdf"},"780":{"id":780,"name":"Aniview LTD","purposes":[1,2,7,8],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://www.aniview.com/privacy-policy/"},"781":{"id":781,"name":"FeedAd GmbH","purposes":[1,3,4,5,6,8,9],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,7,10],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[1,2],"policyUrl":"https://feedad.com/privacy/","overflow":{"httpGetLimit":128}},"782":{"id":782,"name":"AirGrid LTD","purposes":[1,3,5,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[],"specialFeatures":[],"policyUrl":"airgid.io/privacy-policy"},"783":{"id":783,"name":"Audienzz AG","purposes":[1],"legIntPurposes":[2,3,4,5,6,7,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://audienzz.ch/wp-content/uploads/2020/03/AGB_audienzz_2020.pdf"},"784":{"id":784,"name":"Nubo LTD","purposes":[],"legIntPurposes":[2],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.recod3.com/privacypolicy.php"},"785":{"id":785,"name":"agof - daily digital facts","purposes":[1,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1],"features":[],"specialFeatures":[],"policyUrl":"http://www.agof.de/datenschutz/"},"755":{"id":755,"name":"Google Advertising Products","purposes":[1,3,4],"legIntPurposes":[2,5,6,7,9,10],"flexiblePurposes":[2,5,6,7,9,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[],"policyUrl":"https://policies.google.com/privacy"},"786":{"id":786,"name":"TargetVideo GmbH","purposes":[1,2,3,4],"legIntPurposes":[7,8,10],"flexiblePurposes":[2,3,4,7,8,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.target-video.com/datenschutz/"},"787":{"id":787,"name":"Resolution Media M\u00fcnchen GmbH","purposes":[1,7],"legIntPurposes":[],"flexiblePurposes":[7],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://wwww.nonstoppartner.net"},"788":{"id":788,"name":"Ad Alliance GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[1],"policyUrl":"https://www.ad-alliance.de/datenschutz/","overflow":{"httpGetLimit":128}},"789":{"id":789,"name":"IP Deutschland GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[1],"policyUrl":"https://www.ip.de/lp/impressum/datenschutz.cfm","overflow":{"httpGetLimit":32}},"790":{"id":790,"name":"AdGear Technologies, Inc.","purposes":[1,3,4],"legIntPurposes":[2,7,9,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[],"policyUrl":"https://adgear.com/en/privacy"},"791":{"id":791,"name":"Media Square","purposes":[1,2],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"http://www.mediasquare.fr/e-privacy/"},"792":{"id":792,"name":"BritePool Inc","purposes":[1],"legIntPurposes":[3,7,10],"flexiblePurposes":[3,7,10],"specialPurposes":[1],"features":[1,2],"specialFeatures":[],"policyUrl":"https://britepool.com/gdpr-privacy-notice"},"794":{"id":794,"name":"Kubient Inc. ","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[1,2],"policyUrl":"https://kubient.com/privacy-policy/"},"795":{"id":795,"name":"Factor Eleven GmbH","purposes":[1,2,3,4,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.factor-eleven.de/datenschutz/"},"796":{"id":796,"name":"EASY Marketing GmbH","purposes":[1,7],"legIntPurposes":[],"flexiblePurposes":[7],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://easy-m.de/"},"797":{"id":797,"name":"Artefact Deutschland GmbH","purposes":[1,7],"legIntPurposes":[],"flexiblePurposes":[7],"specialPurposes":[1,2],"features":[2,3],"specialFeatures":[],"policyUrl":"https://aaa.artefact.com/privacy-policy.do"},"798":{"id":798,"name":"Adverticum cPlc.","purposes":[1,2,3,4,5,6,8,9,10],"legIntPurposes":[7],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://adverticum.net/english/privacy-and-data-processing-information/"},"799":{"id":799,"name":"Adpone SL","purposes":[1,2,4,5,7,9],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[2],"specialFeatures":[],"policyUrl":"http://www.adpone.com/privacy-policy/"},"800":{"id":800,"name":"Reppublika- The Research Toolbox GmbH","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[10],"specialPurposes":[1],"features":[2],"specialFeatures":[1],"policyUrl":"https://www.reppublika.com/privacy-policy"},"801":{"id":801,"name":"Bannernow, Inc.","purposes":[1,4],"legIntPurposes":[2,7],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://bannernow.com/privacy"},"802":{"id":802,"name":"NOW GmbH","purposes":[1,3,4,7],"legIntPurposes":[2,5,6,8,9,10],"flexiblePurposes":[2,3,4,5,6,7,8,9,10],"specialPurposes":[1,2],"features":[1,2,3],"specialFeatures":[1,2],"policyUrl":"https://static.now-services.de/privacy/index.html"},"803":{"id":803,"name":"Click Tech Limited","purposes":[1,2],"legIntPurposes":[3],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[1],"specialFeatures":[],"policyUrl":"https://en.yeahmobi.com/html/privacypolicy/","overflow":{"httpGetLimit":128}},"804":{"id":804,"name":"LinkedIn Ireland Unlimited Company","purposes":[1,3,4],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,3,4,7,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.linkedin.com/legal/privacy-policy"},"805":{"id":805,"name":"LEESTEN INC","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[1,2],"policyUrl":"https://www.leesten.io/privacy-policy"},"807":{"id":807,"name":"Moloco, Inc.","purposes":[1],"legIntPurposes":[2,3,4,7,8,9,10],"flexiblePurposes":[2,3,4,7,8,9,10],"specialPurposes":[1,2],"features":[3],"specialFeatures":[2],"policyUrl":"http://www.molocoads.com/private-policy.html"},"808":{"id":808,"name":"Pure Local Media GmbH","purposes":[],"legIntPurposes":[2,7,10],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://purelocalmedia.de/?page_id=593"},"809":{"id":809,"name":"adnanny.com SLU","purposes":[1,2,3,4,7],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"http://adnanny.com/en/privacy/"},"810":{"id":810,"name":"lead alliance GmbH","purposes":[1],"legIntPurposes":[7],"flexiblePurposes":[7],"specialPurposes":[1,2],"features":[3],"specialFeatures":[],"policyUrl":"https://www.lead-alliance.net/dataprotection2"},"811":{"id":811,"name":"iPROM","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://iprom.eu/privacy_policy/GDPR"},"793":{"id":793,"name":"Amazon Advertising","purposes":[1,2,3,4,7,9,10],"legIntPurposes":[],"flexiblePurposes":[2,3,4,7,9,10],"specialPurposes":[1,2],"features":[],"specialFeatures":[],"policyUrl":"https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201909010"},"813":{"id":813,"name":"Adjust GmbH","purposes":[1,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[7,8,9,10],"specialPurposes":[1],"features":[2,3],"specialFeatures":[2],"policyUrl":"https://www.adjust.com/terms/privacy-policy/"},"814":{"id":814,"name":"UAB Aktyvus sektorius - Eskimi","purposes":[1,3,4],"legIntPurposes":[2,7,10],"flexiblePurposes":[2,7,10],"specialPurposes":[1,2],"features":[1,2],"specialFeatures":[1],"policyUrl":"https://business.eskimi.com/privacypolicy/"},"815":{"id":815,"name":"Blockthrough, Inc.","purposes":[],"legIntPurposes":[7],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://blockthrough.com/privacy-policy/"},"816":{"id":816,"name":"NoBid, Inc.","purposes":[1,2,3,5,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[2],"features":[],"specialFeatures":[],"policyUrl":"https://www.nobid.io/privacy-policy/"},"817":{"id":817,"name":"Miaozhen Information Technology Co. Ltd","purposes":[1,2,3,4,5,6,7,8,9,10],"legIntPurposes":[],"flexiblePurposes":[],"specialPurposes":[],"features":[2],"specialFeatures":[],"policyUrl":"https://www.miaozhen.com/en/privacy"}}} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-bidder-request-reject-request.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-bidder-request-reject-request.json new file mode 100644 index 00000000000..1b87b4bdf1b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-bidder-request-reject-request.json @@ -0,0 +1,60 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "device": { + "language": "en" + }, + "site": { + "page": "http://www.example.com", + "publisher": { + "id": "9001" + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + }, + "regs": { + "ext": { + "us_privacy": "1YNN" + } + } + }, + "ext": { + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-bidder-request-reject-response.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-bidder-request-reject-response.json new file mode 100644 index 00000000000..c9ce8ef7e40 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-bidder-request-reject-response.json @@ -0,0 +1,14 @@ +{ + "id": "tid", + "seatbid": [], + "cur": "USD", + "ext": { + "responsetimemillis": { + "rubicon": 0 + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-processed-auction-request-reject-request.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-processed-auction-request-reject-request.json new file mode 100644 index 00000000000..48c0f472517 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-processed-auction-request-reject-request.json @@ -0,0 +1,38 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "page": "http://www.example.com", + "publisher": { + "id": "8001" + } + }, + "tmax": 5000 +} diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-processed-bidder-response-reject-request.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-processed-bidder-response-reject-request.json new file mode 100644 index 00000000000..c078d047114 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-processed-bidder-response-reject-request.json @@ -0,0 +1,60 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "device": { + "language": "en" + }, + "site": { + "page": "http://www.example.com", + "publisher": { + "id": "11001" + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + }, + "regs": { + "ext": { + "us_privacy": "1YNN" + } + } + }, + "ext": { + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-processed-bidder-response-reject-response.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-processed-bidder-response-reject-response.json new file mode 100644 index 00000000000..15184653ff7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-processed-bidder-response-reject-response.json @@ -0,0 +1,14 @@ +{ + "id": "tid", + "seatbid": [], + "cur": "USD", + "ext": { + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-raw-auction-request-reject-request.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-raw-auction-request-reject-request.json new file mode 100644 index 00000000000..80922d5800b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-raw-auction-request-reject-request.json @@ -0,0 +1,38 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "page": "http://www.example.com", + "publisher": { + "id": "7001" + } + }, + "tmax": 5000 +} diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-raw-bidder-response-reject-request.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-raw-bidder-response-reject-request.json new file mode 100644 index 00000000000..ce836c1e145 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-raw-bidder-response-reject-request.json @@ -0,0 +1,60 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "device": { + "language": "en" + }, + "site": { + "page": "http://www.example.com", + "publisher": { + "id": "10001" + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + }, + "regs": { + "ext": { + "us_privacy": "1YNN" + } + } + }, + "ext": { + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-raw-bidder-response-reject-response.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-raw-bidder-response-reject-response.json new file mode 100644 index 00000000000..15184653ff7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-auction-raw-bidder-response-reject-response.json @@ -0,0 +1,14 @@ +{ + "id": "tid", + "seatbid": [], + "cur": "USD", + "ext": { + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json new file mode 100644 index 00000000000..4d979c7d0df --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json @@ -0,0 +1,76 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "ext": { + "rp": { + "size_id": 15, + "alt_size_ids": [ + 10 + ], + "mime": "text/html" + } + } + }, + "ext": { + "rp": { + "zone_id": 4001, + "target": { + "page": [ + "http://www.example.com" + ] + }, + "track": { + "mint": "", + "mint_version": "" + } + }, + "maxbids": 1 + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "ext": { + "rp": { + "account_id": 2001 + } + } + }, + "ext": { + "rp": { + "site_id": 3001 + }, + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "language": "en", + "ext": { + "rp": { + } + } + }, + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + } + }, + "at": 1, + "tmax": 5000 +} diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-response-1.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-response-1.json new file mode 100644 index 00000000000..c717d63849c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-response-1.json @@ -0,0 +1,19 @@ +{ + "id": "bidResponseId1", + "seatbid": [ + { + "bid": [ + { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250 + } + ], + "seat": "seatId1" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-request.json b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-request.json new file mode 100644 index 00000000000..de627bc01da --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-request.json @@ -0,0 +1,62 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "device": { + "language": "en" + }, + "site": { + "page": "http://www.example.com", + "publisher": { + "id": "6001" + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + }, + "regs": { + "ext": { + "us_privacy": "1YNN" + } + } + }, + "ext": { + "prebid": { + "debug": 1, + "trace": "verbose", + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json new file mode 100644 index 00000000000..6f10c94d02f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json @@ -0,0 +1,293 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "debug": { + "resolvedrequest": { + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "6001" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "222.111.222.0", + "language": "fr" + }, + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "ext": { + "prebid": { + "debug": 1, + "trace": "verbose", + "auctiontimestamp": 0, + "channel": { + "name": "web" + } + }, + "sample-it-module": { + "raw-auction-request-trace": "I've been here", + "processed-auction-request-trace": "I've been here" + } + } + } + }, + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0, + "modules": { + "trace": { + "stages": [ + { + "stage": "entrypoint", + "outcomes": [ + { + "entity": "http-request", + "groups": [ + { + "invocationresults": [ + { + "hookid": { + "module-code": "sample-it-module", + "hook-impl-code": "entrypoint" + }, + "status": "success", + "action": "update" + } + ] + } + ] + } + ] + }, + { + "stage": "raw-auction-request", + "outcomes": [ + { + "entity": "auction-request", + "groups": [ + { + "invocationresults": [ + { + "hookid": { + "module-code": "sample-it-module", + "hook-impl-code": "raw-auction-request" + }, + "status": "success", + "action": "update", + "debugmessages": [ + "raw auction request debug message 1", + "raw auction request debug message 1" + ], + "analyticstags": { + "activities": [ + { + "name": "device-id", + "status": "success", + "results": [ + { + "status": "success", + "values": { + "some-field": "some-value" + }, + "appliedto": { + "impids": [ + "impId1" + ], + "request": true + } + } + ] + } + ] + } + } + ] + } + ] + } + ] + }, + { + "stage": "processed-auction-request", + "outcomes": [ + { + "entity": "auction-request", + "groups": [ + { + "invocationresults": [ + { + "hookid": { + "module-code": "sample-it-module", + "hook-impl-code": "processed-auction-request" + }, + "status": "success", + "action": "update" + } + ] + } + ] + } + ] + }, + { + "stage": "bidder-request", + "outcomes": [ + { + "entity": "rubicon", + "groups": [ + { + "invocationresults": [ + { + "hookid": { + "module-code": "sample-it-module", + "hook-impl-code": "bidder-request" + }, + "status": "success", + "action": "update" + } + ] + } + ] + } + ] + }, + { + "stage": "raw-bidder-response", + "outcomes": [ + { + "entity": "rubicon", + "groups": [ + { + "invocationresults": [ + { + "hookid": { + "module-code": "sample-it-module", + "hook-impl-code": "raw-bidder-response" + }, + "status": "success", + "action": "update" + } + ] + } + ] + } + ] + }, + { + "stage": "processed-bidder-response", + "outcomes": [ + { + "entity": "rubicon", + "groups": [ + { + "invocationresults": [ + { + "hookid": { + "module-code": "sample-it-module", + "hook-impl-code": "processed-bidder-response" + }, + "status": "success", + "action": "update" + } + ] + } + ] + } + ] + }, + { + "stage": "auction-response", + "outcomes": [ + { + "entity": "auction-response", + "groups": [ + { + "invocationresults": [ + { + "hookid": { + "module-code": "sample-it-module", + "hook-impl-code": "auction-response" + }, + "status": "success", + "action": "update" + } + ] + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json new file mode 100644 index 00000000000..8f6cbbecfac --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json @@ -0,0 +1,78 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "ext": { + "rp": { + "size_id": 15, + "alt_size_ids": [ + 10 + ], + "mime": "text/html" + } + } + }, + "tagid": "tagid-from-bidder-request-hook", + "ext": { + "rp": { + "zone_id": 4001, + "target": { + "page": [ + "http://www.example.com" + ] + }, + "track": { + "mint": "", + "mint_version": "" + } + }, + "maxbids": 1 + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "ext": { + "rp": { + "account_id": 2001 + } + } + }, + "ext": { + "rp": { + "site_id": 3001 + }, + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "222.111.222.0", + "language": "fr", + "ext": { + "rp": { + } + } + }, + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + } + }, + "at": 1, + "tmax": 5000 +} diff --git a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-response-1.json b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-response-1.json new file mode 100644 index 00000000000..c717d63849c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-response-1.json @@ -0,0 +1,19 @@ +{ + "id": "bidResponseId1", + "seatbid": [ + { + "bid": [ + { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250 + } + ], + "seat": "seatId1" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/info-bidders/test-info-bidder-details-response.json b/src/test/resources/org/prebid/server/it/info-bidders/test-info-bidder-details-response.json index f0cec89cd02..5c9f462d1a0 100644 --- a/src/test/resources/org/prebid/server/it/info-bidders/test-info-bidder-details-response.json +++ b/src/test/resources/org/prebid/server/it/info-bidders/test-info-bidder-details-response.json @@ -1,4 +1,6 @@ { + "status":"ACTIVE", + "usesHttps": false, "maintainer": { "email": "header-bidding@rubiconproject.com" }, @@ -15,4 +17,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-acuityads-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-acuityads-bid-request.json new file mode 100644 index 00000000000..34779dfa5aa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-acuityads-bid-request.json @@ -0,0 +1,36 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-acuityads-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-acuityads-bid-response.json new file mode 100644 index 00000000000..da5c7fc51cd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-acuityads-bid-response.json @@ -0,0 +1,18 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048" + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-request.json b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-request.json new file mode 100644 index 00000000000..2c348376e23 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "acuityads": { + "host": "test.host.com", + "accountid": "testValue" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json new file mode 100644 index 00000000000..ba9ea7db56d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json @@ -0,0 +1,35 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "acuityads", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "acuityads": "{{ acuityads.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adf/test-adf-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-adf-bid-request.json new file mode 100644 index 00000000000..00719932166 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-adf-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "native": { + "request": "{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":2,\"required\":1,\"title\":{\"len\":90}},{\"id\":6,\"required\":1,\"img\":{\"type\":3,\"wmin\":128,\"hmin\":128,\"mimes\":[\"image/jpg\",\"image/jpeg\",\"image/png\"]}},{\"id\":7,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", + "ver": "1.2" + }, + "tagid": "1", + "ext": { + "bidder": { + "mid": "1" + } + } + } + ], + "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": 2000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adf/test-adf-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-adf-bid-response.json new file mode 100644 index 00000000000..38499050153 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-adf-bid-response.json @@ -0,0 +1,17 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 10, + "adomain": [], + "crid": "test-creative-id-1" + } + ] + } + ], + "cur": "EUR" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-request.json new file mode 100644 index 00000000000..cf041e04f93 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-request.json @@ -0,0 +1,22 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "native": { + "request": "{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":2,\"required\":1,\"title\":{\"len\":90}},{\"id\":6,\"required\":1,\"img\":{\"type\":3,\"wmin\":128,\"hmin\":128,\"mimes\":[\"image/jpg\",\"image/jpeg\",\"image/png\"]}},{\"id\":7,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", + "ver": "1.2" + }, + "ext": { + "adf": { + "mid": "1" + } + } + } + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json new file mode 100644 index 00000000000..f91084a6421 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json @@ -0,0 +1,36 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 11.393, + "adomain": [ + ], + "crid": "test-creative-id-1", + "ext": { + "prebid": { + "type": "native" + }, + "origbidcpm": 10, + "origbidcur": "EUR" + } + } + ], + "seat": "adf", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "adf": "{{ adf.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-adform-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adform/test-adform-bid-response-1.json index 83fc16be832..6432182febc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-adform-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adform/test-adform-bid-response-1.json @@ -1,10 +1,10 @@ { "response": "banner", "banner": "banner", - "win_bid": "10.5", - "currency": "currency", + "win_bid": "0.5", + "win_cur": "USD", "width": 400, "height": 300, "dealId": "dealId", "win_crid": "crid12" -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-request.json index e88886add30..517e1f0d469 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId12", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "adform": { @@ -24,61 +20,10 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, "regs": { "ext": { "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-response.json index d7c6f51aa9f..3ebea4cb55d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adform/test-auction-adform-response.json @@ -1,40 +1,22 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "impId12", - "impid": "impId12", - "price": 10.5, + "id": "imp_id", + "impid": "imp_id", + "price": 0.5, "adm": "banner", "crid": "crid12", "w": 400, "h": 300, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "10.50", - "hb_size_adform": "400x300", - "hb_size": "400x300", - "hb_bidder": "adform", - "hb_cache_id_adform": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce", - "hb_cache_id": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce", - "hb_bidder_adform": "adform", - "hb_pb_adform": "10.50", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_adform": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_adform": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}ca2a4dd3-f974-4eff-be5c-986bf4e083ce", - "cacheId": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce" - } - } - } + "type": "banner" + }, + "origbidcpm": 0.5, + "origbidcur": "USD" } } ], @@ -45,11 +27,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "adform": "{{ adform.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adform": "{{ adform.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-cache-adform-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adform/test-cache-adform-request.json deleted file mode 100644 index f0dead6a19f..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-cache-adform-request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "impId12", - "impid": "impId12", - "price": 10.5, - "adm": "banner", - "crid": "crid12", - "w": 400, - "h": 300 - } - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-cache-adform-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adform/test-cache-adform-response.json deleted file mode 100644 index a1adc7415cc..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adform/test-cache-adform-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-adgeneration-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-adgeneration-bid-response.json index 1453eb2db90..79198a44920 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-adgeneration-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-adgeneration-bid-response.json @@ -1,6 +1,6 @@ { - "id": "tid", - "ad": "\n \n \n \n\n\n \n \n
    \n \n
    \n ", + "adm": "", "crid": "Dummy_supership.jp", "dealid": "test-deal-id", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_cache_path_adgeneration": "{{ cache.path }}", - "hb_bidder_adgeneration": "adgeneration", - "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_pb_adgeneration": "20.00", - "hb_pb": "20.00", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_cache_host_adgeneration": "{{ cache.host }}", - "hb_bidder": "adgeneration", - "hb_size_adgeneration": "300x250", - "hb_cache_id_adgeneration": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_host": "{{ cache.host }}", - "hb_deal_adgeneration": "test-deal-id", - "hb_deal": "test-deal-id" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - } - } + "type": "banner" + }, + "origbidcpm": 46.6, + "origbidcur": "USD" } } ], @@ -48,12 +28,11 @@ "cur": "USD", "ext": { "responsetimemillis": { - "adgeneration": "{{ adgeneration.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adgeneration": "{{ adgeneration.response_time_ms }}" }, "tmaxrequest": 5000, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-cache-adgeneration-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-cache-adgeneration-request.json deleted file mode 100644 index 63fb9c70fc4..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-cache-adgeneration-request.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "58278", - "impid": "58278", - "price": 46.6, - "adm": "\n \n \n \n\n\n \n \n
    \n \n
    \n ", - "crid": "Dummy_supership.jp", - "dealid": "test-deal-id", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-request.json index 3073a20f66b..650237a3183 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "adhese": { @@ -32,68 +28,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json index 20b4699efc1..00b93ddcb2a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { "id": "1", - "impid": "impId001", + "impid": "imp_id", "price": 1.00, "dealid": "888", "adm": "
    ", @@ -13,34 +13,12 @@ "w": 728, "h": 90, "ext": { - "bidder": { - "adType": "leaderboard" - }, + "adType": "leaderboard", "prebid": { - "type": "banner", - "targeting": { - "hb_cache_host_adhese": "{{ cache.host }}", - "hb_cache_path_adhese": "{{ cache.path }}", - "hb_cache_id": "a5d3a873-d06e-4f2f-8556-120e05d62b28", - "hb_pb": "1.00", - "hb_size_adhese": "728x90", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "728x90", - "hb_pb_adhese": "1.00", - "hb_cache_id_adhese": "a5d3a873-d06e-4f2f-8556-120e05d62b28", - "hb_bidder_adhese": "adhese", - "hb_bidder": "adhese", - "hb_cache_host": "{{ cache.host }}", - "hb_deal": "888", - "hb_deal_adhese": "888" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}a5d3a873-d06e-4f2f-8556-120e05d62b28", - "cacheId": "a5d3a873-d06e-4f2f-8556-120e05d62b28" - } - } - } + "type": "banner" + }, + "origbidcpm": 1.00, + "origbidcur": "USD" } } ], @@ -51,12 +29,11 @@ "cur": "USD", "ext": { "responsetimemillis": { - "adhese": "{{ adhese.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adhese": "{{ adhese.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-cache-adhese-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-cache-adhese-request.json deleted file mode 100644 index d2827ac7466..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-cache-adhese-request.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "1", - "impid": "impId001", - "price": 1, - "adm": "
    ", - "crid": "60613369", - "dealid": "888", - "w": 728, - "h": 90, - "ext": { - "adType": "leaderboard" - } - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-cache-adhese-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-cache-adhese-response.json deleted file mode 100644 index 773491fa9b0..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-cache-adhese-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "a5d3a873-d06e-4f2f-8556-120e05d62b28" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-request.json index 9a0d61c74cb..4c3358f3169 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-request.json @@ -1,20 +1,16 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 320, + "h": 250 } } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "ext": { "amp": 0 @@ -22,62 +18,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AK-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-response.json index 492f537eca6..d91df0ca089 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-adkernel-bid-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid02", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 2.25, "cid": "1001", "crid": "2002", @@ -18,5 +18,5 @@ ] } ], - "bidid": "wehM-93KGr0" + "bidid": "bid_id" } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-request.json index 181833b7898..85a222e8b58 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-request.json @@ -1,22 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "video": { - "mimes": [ - "video/mp4" - ], - "w": 640, - "h": 480 + "w": 320, + "h": 250 }, "ext": { "adkernel": { @@ -26,58 +15,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json index 47f5ecfa16c..d6cfad2d245 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid02", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 2.25, "adm": "", "adid": "2002", @@ -16,32 +16,9 @@ "crid": "2002", "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "2.20", - "hb_bidder_adkernel": "adkernel", - "hb_cache_id_adkernel": "71615e3d-8a18-4099-a807-3952199b532a", - "hb_pb_adkernel": "2.20", - "hb_cache_path": "{{ cache.path }}", - "hb_uuid": "69c0e0c9-ce20-4d91-99d1-d821afd75727", - "hb_cache_host_adkernel": "{{ cache.host }}", - "hb_bidder": "adkernel", - "hb_cache_id": "71615e3d-8a18-4099-a807-3952199b532a", - "hb_cache_host": "{{ cache.host }}", - "hb_uuid_adkernel": "69c0e0c9-ce20-4d91-99d1-d821afd75727", - "hb_cache_path_adkernel": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}71615e3d-8a18-4099-a807-3952199b532a", - "cacheId": "71615e3d-8a18-4099-a807-3952199b532a" - }, - "vastXml": { - "url": "{{ cache.resource_url }}69c0e0c9-ce20-4d91-99d1-d821afd75727", - "cacheId": "69c0e0c9-ce20-4d91-99d1-d821afd75727" - } - } - } + "type": "banner" + }, + "origbidcpm": 2.25 } } ], @@ -52,11 +29,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "adkernel": "{{ adkernel.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adkernel": "{{ adkernel.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-cache-adkernel-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-cache-adkernel-request.json deleted file mode 100644 index 8bbdb8380ee..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-cache-adkernel-request.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid02", - "impid": "impId001", - "price": 2.25, - "adm": "", - "adid": "2002", - "adomain": [ - "tag-example.com" - ], - "cid": "1001", - "crid": "2002" - } - }, - { - "type": "xml", - "value": "", - "expiry": 120 - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-cache-adkernel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-cache-adkernel-response.json deleted file mode 100644 index 9573e6b0140..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-cache-adkernel-response.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "responses": [ - { - "uuid": "71615e3d-8a18-4099-a807-3952199b532a" - }, - { - "uuid": "69c0e0c9-ce20-4d91-99d1-d821afd75727" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-1.json deleted file mode 100644 index cd339c41a1a..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-1.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId021", - "banner": { - "format": [ - { - "w": 300, - "h": 200 - } - ], - "w": 300, - "h": 250 - } - } - ], - "site": { - "domain": "", - "page": "http://www.example.com", - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AK-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-2.json deleted file mode 100644 index 961361f1c38..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request-2.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId022", - "video": { - "mimes": [ - "video/mp4" - ], - "w": 640, - "h": 480 - } - } - ], - "site": { - "domain": "", - "page": "http://www.example.com", - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AK-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request.json new file mode 100644 index 00000000000..b1b2d34c8f5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-request.json @@ -0,0 +1,33 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + } + } + ], + "site": { + "domain": "", + "page": "http://www.example.com", + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response-1.json deleted file mode 100644 index 4a86ec76006..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response-1.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "wehM-93KGr0_0_0", - "impid": "impId021", - "price": 0.5, - "cid": "3706", - "crid": "crid021", - "adid": "19005", - "adm": "adm021", - "cat": [ - "IAB2" - ], - "adomain": [ - "test.com" - ], - "h": 250, - "w": 300 - } - ] - } - ], - "bidid": "wehM-93KGr0" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response-2.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response-2.json deleted file mode 100644 index b407605baa7..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response-2.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid02", - "impid": "impId022", - "price": 2.25, - "cid": "1001", - "crid": "crid022", - "adid": "2002", - "adm": "adm022", - "cat": [ - "IAB2" - ], - "adomain": [ - "video-example.com" - ] - } - ] - } - ], - "bidid": "wehM-93KGr0" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json new file mode 100644 index 00000000000..9f868cc7baa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json @@ -0,0 +1,27 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 0.5, + "cid": "3706", + "crid": "crid021", + "adid": "19005", + "adm": "adm021", + "cat": [ + "IAB2" + ], + "adomain": [ + "test.com" + ], + "h": 250, + "w": 300 + } + ] + } + ], + "bidid": "bid_id" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-request.json index b583dabf956..825e72043aa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-request.json @@ -1,94 +1,20 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId021", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 200 - } - ] + "w": 300, + "h": 250 }, "ext": { "adkernelAdn": { "pubId": 101 } } - }, - { - "id": "impId022", - "video": { - "mimes": [ - "video/mp4" - ], - "w": 640, - "h": 480 - }, - "ext": { - "adkernelAdn": { - "pubId": 102 - } - } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json index 86bf5172040..f1d38a7780f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json @@ -1,55 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid02", - "impid": "impId022", - "price": 2.25, - "adm": "adm022", - "adid": "2002", - "adomain": [ - "video-example.com" - ], - "cid": "1001", - "crid": "crid022", - "cat": [ - "IAB2" - ], - "ext": { - "prebid": { - "type": "video", - "targeting": { - "hb_pb": "2.20", - "hb_cache_id_adkernelAdn": "2d20ad14-7070-4edd-8b9f-fbb11eb53de9", - "hb_uuid": "a3558327-7696-4606-a5e9-43413bd7faea", - "hb_bidder_adkernelAdn": "adkernelAdn", - "hb_bidder": "adkernelAdn", - "hb_uuid_adkernelAdn": "a3558327-7696-4606-a5e9-43413bd7faea", - "hb_cache_id": "2d20ad14-7070-4edd-8b9f-fbb11eb53de9", - "hb_pb_adkernelAdn": "2.20", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_adkernelAdn": "{{ cache.host }}", - "hb_cache_path": "/cache", - "hb_cache_path_adkernelAdn": "/cache" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}2d20ad14-7070-4edd-8b9f-fbb11eb53de9", - "cacheId": "2d20ad14-7070-4edd-8b9f-fbb11eb53de9" - }, - "vastXml": { - "url": "{{ cache.resource_url }}a3558327-7696-4606-a5e9-43413bd7faea", - "cacheId": "a3558327-7696-4606-a5e9-43413bd7faea" - } - } - } - } - }, - { - "id": "wehM-93KGr0_0_0", - "impid": "impId021", + "id": "bid_id", + "impid": "imp_id", "price": 0.5, "adm": "adm021", "adid": "19005", @@ -65,28 +21,9 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "0.50", - "hb_cache_id_adkernelAdn": "aedec380-879a-4029-8e7a-5a51676c887c", - "hb_size": "300x250", - "hb_size_adkernelAdn": "300x250", - "hb_bidder_adkernelAdn": "adkernelAdn", - "hb_bidder": "adkernelAdn", - "hb_cache_id": "aedec380-879a-4029-8e7a-5a51676c887c", - "hb_pb_adkernelAdn": "0.50", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_adkernelAdn": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_adkernelAdn": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}aedec380-879a-4029-8e7a-5a51676c887c", - "cacheId": "aedec380-879a-4029-8e7a-5a51676c887c" - } - } - } + "type": "banner" + }, + "origbidcpm": 0.5 } } ], @@ -97,11 +34,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "adkernelAdn": "{{ adkernelAdn.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adkernelAdn": "{{ adkernelAdn.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-cache-adkerneladn-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-cache-adkerneladn-request.json deleted file mode 100644 index 5c65a9406db..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-cache-adkerneladn-request.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid02", - "impid": "impId022", - "price": 2.25, - "adm": "adm022", - "adid": "2002", - "adomain": [ - "video-example.com" - ], - "cid": "1001", - "crid": "crid022", - "cat": [ - "IAB2" - ] - } - }, - { - "type": "json", - "value": { - "id": "wehM-93KGr0_0_0", - "impid": "impId021", - "price": 0.5, - "adm": "adm021", - "adid": "19005", - "adomain": [ - "test.com" - ], - "cid": "3706", - "crid": "crid021", - "cat": [ - "IAB2" - ], - "w": 300, - "h": 250 - } - }, - { - "type": "xml", - "value": "adm022", - "expiry": 120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-cache-adkerneladn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-cache-adkerneladn-response.json deleted file mode 100644 index 6637734dadd..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-cache-adkerneladn-response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "responses": [ - { - "uuid": "2d20ad14-7070-4edd-8b9f-fbb11eb53de9" - }, - { - "uuid": "aedec380-879a-4029-8e7a-5a51676c887c" - }, - { - "uuid": "a3558327-7696-4606-a5e9-43413bd7faea" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-1.json index df169930104..fa01987b033 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-1.json @@ -1,29 +1,25 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id1", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, - "tagid": "first-tagid", + "tagid": "tag_id1", "ext": { "bidder": { - "TagID": "first-tagid" + "TagID": "tag_id1" } } } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -31,62 +27,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AD-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-2.json index b65b6c6d8ff..77d53ed6e49 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-request-2.json @@ -1,8 +1,8 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId002", + "id": "imp_id2", "video": { "mimes": [ "video/mp4" @@ -10,19 +10,19 @@ "w": 640, "h": 480 }, - "tagid": "second-tagid", + "tagid": "tag_id2", "ext": { "bidder": { - "TagID": "second-tagid" + "TagID": "tag_id2" } } } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -30,62 +30,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AD-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-response-1.json index 5cbb60b868c..726546f34dc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-response-1.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id1", + "impid": "imp_id1", "price": 1.25, "crid": "crid001", "adm": "adm001", @@ -15,5 +15,5 @@ ] } ], - "bidid": "bid001" + "bidid": "bid_id1" } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-response-2.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-response-2.json index 966ec782f17..a9766c51768 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-response-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-adman-bid-response-2.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid01", - "impid": "impId002", + "id": "bid_id2", + "impid": "imp_id2", "price": 2.25, "cid": "1001", "crid": "crid002", @@ -23,5 +23,5 @@ ] } ], - "bidid": "bid01" + "bidid": "bid_id2" } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json index aac9638c266..50671a937ea 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-request.json @@ -1,24 +1,24 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id1", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { - "adman": { - "TagID": "first-tagid" + "prebid": { + "bidder": { + "adman": { + "TagID": "tag_id1" + } + } } } }, { - "id": "impId002", + "id": "imp_id2", "video": { "mimes": [ "video/mp4" @@ -27,67 +27,23 @@ "h": 480 }, "ext": { - "adman": { - "TagID": "second-tagid" + "prebid": { + "bidder": { + "adman": { + "TagID": "tag_id2" + } + } } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json index 65f7d7b4f59..61812a85523 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id1", + "impid": "imp_id1", "price": 1.25, "adm": "adm001", "crid": "crid001", @@ -13,33 +13,14 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "1.20", - "hb_cache_id_adman": "9092799c-93b0-4e11-a232-2c0151d5d275", - "hb_cache_path_adman": "{{ cache.path }}", - "hb_cache_path": "{{ cache.path }}", - "hb_pb_adman": "1.20", - "hb_size": "300x250", - "hb_bidder_adman": "adman", - "hb_size_adman": "300x250", - "hb_bidder": "adman", - "hb_cache_id": "9092799c-93b0-4e11-a232-2c0151d5d275", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_adman": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}9092799c-93b0-4e11-a232-2c0151d5d275", - "cacheId": "9092799c-93b0-4e11-a232-2c0151d5d275" - } - } - } + "type": "banner" + }, + "origbidcpm": 1.25 } }, { - "id": "bid01", - "impid": "impId002", + "id": "bid_id2", + "impid": "imp_id2", "price": 2.25, "adm": "adm002", "adid": "29681110", @@ -55,34 +36,9 @@ "h": 480, "ext": { "prebid": { - "type": "video", - "targeting": { - "hb_cache_id_adman": "83cdc325-c816-4d2e-bf2c-9213a70671dd", - "hb_cache_path_adman": "{{ cache.path }}", - "hb_size_adman": "640x480", - "hb_cache_id": "83cdc325-c816-4d2e-bf2c-9213a70671dd", - "hb_uuid_adman": "99dc3357-34ac-4819-9f68-0820039a542f", - "hb_pb": "2.20", - "hb_cache_path": "{{ cache.path }}", - "hb_uuid": "99dc3357-34ac-4819-9f68-0820039a542f", - "hb_pb_adman": "2.20", - "hb_size": "640x480", - "hb_bidder_adman": "adman", - "hb_bidder": "adman", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_adman": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}83cdc325-c816-4d2e-bf2c-9213a70671dd", - "cacheId": "83cdc325-c816-4d2e-bf2c-9213a70671dd" - }, - "vastXml": { - "url": "{{ cache.resource_url }}99dc3357-34ac-4819-9f68-0820039a542f", - "cacheId": "99dc3357-34ac-4819-9f68-0820039a542f" - } - } - } + "type": "video" + }, + "origbidcpm": 2.25 } } ], @@ -93,11 +49,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "adman": "{{ adman.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adman": "{{ adman.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-cache-adman-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-cache-adman-request.json deleted file mode 100644 index 58471d39985..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-cache-adman-request.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "adm": "adm001", - "crid": "crid001", - "w": 300, - "h": 250 - } - }, - { - "type": "json", - "value": { - "id": "bid01", - "impid": "impId002", - "price": 2.25, - "adm": "adm002", - "adid": "29681110", - "adomain": [ - "video-example.com" - ], - "cid": "1001", - "crid": "crid002", - "cat": [ - "IAB2" - ], - "w": 640, - "h": 480 - } - }, - { - "type": "xml", - "value": "adm002", - "expiry": 120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-cache-adman-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-cache-adman-response.json deleted file mode 100644 index 64a88c27456..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-cache-adman-response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "responses": [ - { - "uuid": "9092799c-93b0-4e11-a232-2c0151d5d275" - }, - { - "uuid": "83cdc325-c816-4d2e-bf2c-9213a70671dd" - }, - { - "uuid": "99dc3357-34ac-4819-9f68-0820039a542f" - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-request.json index 2eca793ae38..5659d1990d9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-request.json @@ -1,38 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", - "bidfloor": 0.1, - "ext": { - "customParams": { - "foo": "bar" - } - } - }, - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "w": 1024, - "h": 576, - "skipmin": 0, - "skipafter": 0 + "w": 300, + "h": 250 }, "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", "bidfloor": 0.1, @@ -44,10 +17,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -55,72 +28,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AD-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, - "tmax": 3000, + "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.811 - } - } - }, - "targeting": { - "includebidderkeys": true, - "includewinners": true, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json index 455a146256a..5561b33da3b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json @@ -1,5 +1,5 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ @@ -7,8 +7,8 @@ "adm": "hi", "crid": "test_banner_crid", "cid": "test_cid", - "impid": "impId001", - "id": "1", + "impid": "imp_id", + "id": "bid_id", "price": 0.01, "ext": { "format": "BANNER" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-request.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-request.json index ea1a4d3eb51..f4f95c4c95c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-request.json @@ -1,42 +1,12 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "admixer": { - "zone": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", - "customFloor": 0.1, - "customParams": { - "foo": "bar" - } - } - } - }, - { - "id": "impId002", - "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "skipafter": 0, - "skipmin": 0, - "w": 1024, - "h": 576 + "w": 300, + "h": 250 }, "ext": { "admixer": { @@ -49,80 +19,10 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "ip": "193.168.244.1", - "language": "en", - "ifa": "ifaId", - "ua": "userAgent" - }, - "site": { - "domain": "example.com", - "ext": { - "amp": 0 - }, - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - } - }, - "at": 1, - "tmax": 3000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, + "tmax": 5000, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "includebidderkeys": true, - "includewinners": true, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json index 8e404dd82c2..4352d750af3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json @@ -1,5 +1,5 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ @@ -8,33 +8,14 @@ "cid": "test_cid", "crid": "test_banner_crid", "ext": { - "bidder": { - "format": "BANNER" - }, + "format": "BANNER", "prebid": { - "type": "banner", - "targeting": { - "hb_bidder": "admixer", - "hb_bidder_admixer": "admixer", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_admixer": "{{ cache.host }}", - "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_id_admixer": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_admixer": "{{ cache.path }}", - "hb_pb": "0.00", - "hb_pb_admixer": "0.00" - }, - "cache": { - "bids": { - "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - } - } + "type": "banner" + }, + "origbidcpm": 0.01 }, - "id": "1", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 0.01 } ], @@ -45,12 +26,11 @@ "cur": "USD", "ext": { "responsetimemillis": { - "cache": "{{ cache.response_time_ms }}", "admixer": "{{ admixer.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, - "tmaxrequest": 3000 + "tmaxrequest": 5000 } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-cache-admixer-request.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-cache-admixer-request.json deleted file mode 100644 index c6692479e4a..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-cache-admixer-request.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "1", - "impid": "impId001", - "price": 0.01, - "adm": "hi", - "cid": "test_cid", - "crid": "test_banner_crid", - "ext": { - "format": "BANNER" - } - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-cache-admixer-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-cache-admixer-response.json deleted file mode 100644 index 93d0b8de2cd..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-cache-admixer-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-adocean-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-adocean-bid-response-1.json index 808472a828b..24304dd8da4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-adocean-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-adocean-bid-response-1.json @@ -5,19 +5,7 @@ "winurl": "https://win-url.com", "statsUrl": "https://stats-url.com", "code": " ", - "currency": "EUR", - "width": "300", - "height": "250", - "crid": "0af345b42983cc4bc0", - "error": "false" - }, - { - "id": "adoceanmyaozpniqismexs", - "price": "12", - "winurl": "https://win-url.com", - "statsUrl": "https://stats-url.com", - "code": " ", - "currency": "EUR", + "currency": "USD", "width": "300", "height": "250", "crid": "0af345b42983cc4bc0", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json index eef8d0cef8e..21314cddc9e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-request.json @@ -1,95 +1,29 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId12", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { - "adocean": { - "emiter": "myao.adocean.pl", - "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", - "slaveId": "adoceanmyaozpniqismex" + "prebid": { + "bidder": { + "adocean": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + } } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId", - "ua": "userAgent", - "ip": "193.168.244.1" - }, - "site": { - "publisher": { - "id": "publisherId", - "page": "http://www.example.com" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "EUR" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "test": 1, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "buyeruid": "AO-UID", - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, "regs": { "ext": { "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json index 9fa05deba4f..deb5fa17616 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { "id": "adoceanmyaozpniqismex", - "impid": "impId12", + "impid": "imp_id", "price": 10, "adm": " ", "crid": "0af345b42983cc4bc0", @@ -13,28 +13,10 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "10.00", - "hb_cache_host_adocean": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_bidder_adocean": "adocean", - "hb_cache_path_adocean": "{{ cache.path }}", - "hb_bidder": "adocean", - "hb_cache_id_adocean": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce", - "hb_cache_id": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce", - "hb_pb_adocean": "10.00", - "hb_cache_host": "{{ cache.host }}", - "hb_size_adocean": "300x250" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}ca2a4dd3-f974-4eff-be5c-986bf4e083ce", - "cacheId": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce" - } - } - } + "type": "banner" + }, + "origbidcpm": 10, + "origbidcur": "USD" } } ], @@ -42,138 +24,14 @@ "group": 0 } ], - "cur": "EUR", + "cur": "USD", "ext": { - "debug": { - "httpcalls": { - "adocean": [ - { - "uri": "{{ adocean.exchange_uri }}/_10000000/ad.json?pbsrv_v=1.0.0&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&aid=adoceanmyaozpniqismex%3AimpId12&gdpr_consent=consentValue&gdpr=1&hcuserid=AO-UID", - "responsebody": "[{\"id\":\"adoceanmyaozpniqismex\",\"price\":\"10\",\"winurl\":\"https://win-url.com\",\"statsUrl\":\"https://stats-url.com\",\"code\":\" \",\"currency\":\"EUR\",\"width\":\"300\",\"height\":\"250\",\"crid\":\"0af345b42983cc4bc0\",\"error\":\"false\"},{\"id\":\"adoceanmyaozpniqismexs\",\"price\":\"12\",\"winurl\":\"https://win-url.com\",\"statsUrl\":\"https://stats-url.com\",\"code\":\" \",\"currency\":\"EUR\",\"width\":\"300\",\"height\":\"250\",\"crid\":\"0af345b42983cc4bc0\",\"error\":\"false\"}]", - "status": 200 - } - ], - "cache": [ - { - "uri": "{{ cache.endpoint }}", - "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"adoceanmyaozpniqismex\",\"impid\":\"impId12\",\"price\":10,\"adm\":\" \",\"crid\":\"0af345b42983cc4bc0\",\"w\":300,\"h\":250}}]}", - "responsebody": "{\"responses\":[{\"uuid\":\"ca2a4dd3-f974-4eff-be5c-986bf4e083ce\"}]}", - "status": 200 - } - ] - }, - "resolvedrequest": { - "id": "tid", - "imp": [ - { - "id": "impId12", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "adocean": { - "emiter": "myao.adocean.pl", - "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", - "slaveId": "adoceanmyaozpniqismex" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AO-UID", - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "test": 1, - "at": 1, - "tmax": 5000, - "cur": [ - "EUR" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.811 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } - } - }, "responsetimemillis": { - "adocean": "{{ adocean.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adocean": "{{ adocean.response_time_ms }}" }, "tmaxrequest": 5000, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-cache-adocean-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-cache-adocean-request.json deleted file mode 100644 index 950f2ead307..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-cache-adocean-request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "adoceanmyaozpniqismex", - "impid": "impId12", - "price": 10, - "adm": " ", - "crid": "0af345b42983cc4bc0", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-cache-adocean-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-cache-adocean-response.json deleted file mode 100644 index a1adc7415cc..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-cache-adocean-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json index a6d57161046..49f9fbe2863 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-request-1.json @@ -1,24 +1,25 @@ { - "id": "req1-unit1-unit1", + "id": "request_id-adunit", "imp": [ { - "id": "imp1", + "id": "imp_id", "banner": { "w": 100, "h": 200 }, "ext": { "bidder": { - "adunit": "unit1" + "adunit": "adunit", + "client": "testClient" } } } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -26,62 +27,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AP-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, - "tmax": 3000, + "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json index 322fc8fa503..ba80a545eed 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id-adunit", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "imp1", + "id": "bid_id", + "impid": "imp_id", "price": 3.33, "adid": "adid001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json index cf8621f7d95..671adea62e9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-request.json @@ -1,84 +1,24 @@ { - "id": "req1-unit1", + "id": "request_id", "imp": [ { - "id": "imp1", + "id": "imp_id", "banner": { "w": 100, "h": 200 }, "ext": { "adoppler": { - "adunit": "unit1" + "adunit": "adunit", + "client": "testClient" } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, - "tmax": 3000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, + "tmax": 5000, "regs": { "ext": { "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json index fe66263a7ee..db0b1aeaa49 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json @@ -1,11 +1,11 @@ { - "id": "req1-unit1", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "imp1", + "id": "bid_id", + "impid": "imp_id", "price": 3.33, "adm": "adm001", "adid": "adid001", @@ -17,31 +17,12 @@ ], "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "3.30", - "hb_bidder_adoppler": "adoppler", - "hb_cache_id_adoppler": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_path_adoppler": "{{ cache.path }}", - "hb_cache_host_adoppler": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_bidder": "adoppler", - "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_host": "{{ cache.host }}", - "hb_pb_adoppler": "3.30" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - } + "type": "banner" }, - "bidder": { - "ads": { - "video": { - "duration": 121 - } + "origbidcpm": 3.33, + "ads": { + "video": { + "duration": 121 } } } @@ -54,12 +35,11 @@ "cur": "USD", "ext": { "responsetimemillis": { - "adoppler": "{{ adoppler.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adoppler": "{{ adoppler.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, - "tmaxrequest": 3000 + "tmaxrequest": 5000 } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-cache-adoppler-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-cache-adoppler-request.json deleted file mode 100644 index 0acc8877698..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-cache-adoppler-request.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "imp1", - "price": 3.33, - "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", - "cat": [ - "IAB1", - "IAB2" - ], - "ext": { - "ads": { - "video": { - "duration": 121 - } - } - } - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-cache-adoppler-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-cache-adoppler-response.json deleted file mode 100644 index 93d0b8de2cd..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-cache-adoppler-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adot/test-adot-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-adot-bid-request.json new file mode 100644 index 00000000000..32f575a45e2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-adot-bid-request.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": {} + } + } + ], + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adot/test-adot-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-adot-bid-response.json new file mode 100644 index 00000000000..bbb49c2d35b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-adot-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "crid": "crid001", + "price": 1.16346, + "adm": "some-test-ad", + "w": 320, + "h": 250, + "ext": { + "adot": { + "media_type": "banner" + } + } + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-request.json new file mode 100644 index 00000000000..096683ee76e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-request.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "adot": {} + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json new file mode 100644 index 00000000000..dcff8f22c64 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.16346, + "adm": "some-test-ad", + "crid": "crid001", + "w": 320, + "h": 250, + "ext": { + "adot": { + "media_type": "banner" + }, + "origbidcpm": 1.16346, + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "adot", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "adot": "{{ adot.response_time_ms }}" + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 1626182712962 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-request.json index 8d461e8bb05..c517186c78e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "bidder": { @@ -19,10 +15,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -30,62 +26,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AP-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json index e9553f306ce..7f09ff7886b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 6.66, "adid": "adid001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-request.json index f2ab294b189..6d2fe110ee6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "adpone": { @@ -18,58 +14,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json index d364f881b12..e24dfce9f48 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 6.66, "adm": "adm001", "adid": "adid001", @@ -15,28 +15,9 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_cache_host_adpone": "{{ cache.host }}", - "hb_cache_path_adpone": "{{ cache.path }}", - "hb_cache_id": "01005d2c-555d-41c6-995d-335663819de0", - "hb_pb": "6.60", - "hb_size_adpone": "300x250", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_pb_adpone": "6.60", - "hb_cache_id_adpone": "01005d2c-555d-41c6-995d-335663819de0", - "hb_bidder_adpone": "adpone", - "hb_bidder": "adpone", - "hb_cache_host": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}01005d2c-555d-41c6-995d-335663819de0", - "cacheId": "01005d2c-555d-41c6-995d-335663819de0" - } - } - } + "type": "banner" + }, + "origbidcpm": 6.66 } } ], @@ -47,11 +28,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "adpone": "{{ adpone.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adpone": "{{ adpone.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-cache-adpone-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-cache-adpone-request.json deleted file mode 100644 index 8b7ec8bb518..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-cache-adpone-request.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 6.66, - "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-cache-adpone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-cache-adpone-response.json deleted file mode 100644 index ca78df8c051..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-cache-adpone-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "01005d2c-555d-41c6-995d-335663819de0" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-adprime-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-adprime-bid-request.json new file mode 100644 index 00000000000..ce248831c1f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-adprime-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "tag_id", + "ext": { + "bidder": { + "TagID": "tag_id" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-adprime-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-adprime-bid-response.json new file mode 100644 index 00000000000..35a9d7ca47f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-adprime-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ], + "bidid": "bid_id" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-request.json new file mode 100644 index 00000000000..da97797b1e7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "adprime": { + "TagID": "tag_id" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json new file mode 100644 index 00000000000..157e64739a3 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json @@ -0,0 +1,36 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "adm": "adm001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 1.25 + } + } + ], + "seat": "adprime", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "adprime": "{{ adprime.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-request-1.json index 5feb88fc600..07d975ad024 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-request-1.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId14", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "bidfloor": 20, "ext": { @@ -23,10 +19,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -34,62 +30,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AD-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json index f396137f590..03c5ee91218 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "620160380", - "impid": "impId14", + "id": "bid_id", + "impid": "imp_id", "price": 8.43, "adm": "adm14", "crid": "crid14", @@ -13,7 +13,7 @@ "h": 250 } ], - "seat": "seatId14", + "seat": "adtarget", "group": 0 } ] diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-request.json index b3f96954db2..ff511571317 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId14", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "adtarget": { @@ -21,61 +17,10 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json index c6f9798b729..23465125a4e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "620160380", - "impid": "impId14", + "id": "bid_id", + "impid": "imp_id", "price": 8.43, "adm": "adm14", "crid": "crid14", @@ -13,28 +13,9 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "8.40", - "hb_bidder_adtarget": "adtarget", - "hb_size_adtarget": "300x250", - "hb_size": "300x250", - "hb_pb_adtarget": "8.40", - "hb_bidder": "adtarget", - "hb_cache_id": "f5c5f34c-ad41-4894-b42b-dd5c86978a4a", - "hb_cache_id_adtarget": "f5c5f34c-ad41-4894-b42b-dd5c86978a4a", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_adtarget": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_adtarget": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}f5c5f34c-ad41-4894-b42b-dd5c86978a4a", - "cacheId": "f5c5f34c-ad41-4894-b42b-dd5c86978a4a" - } - } - } + "type": "banner" + }, + "origbidcpm": 8.43 } } ], @@ -45,11 +26,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "adtarget": "{{ adtarget.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adtarget": "{{ adtarget.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-cache-adtarget-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-cache-adtarget-request.json deleted file mode 100644 index 25d582fd85c..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-cache-adtarget-request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "620160380", - "impid": "impId14", - "price": 8.43, - "adm": "adm14", - "crid": "crid14", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-cache-adtarget-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-cache-adtarget-response.json deleted file mode 100644 index 735d17ddc94..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-cache-adtarget-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "f5c5f34c-ad41-4894-b42b-dd5c86978a4a" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json index 59a7c4cba4e..8205d27acb4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId14", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "bidfloor": 20, "ext": { @@ -23,10 +19,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -34,62 +30,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AT-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response-1.json index f396137f590..15d06b7c923 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response-1.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "620160380", - "impid": "impId14", + "id": "bid_id", + "impid": "imp_id", "price": 8.43, "adm": "adm14", "crid": "crid14", @@ -13,7 +13,7 @@ "h": 250 } ], - "seat": "seatId14", + "seat": "adtelligent", "group": 0 } ] diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-request.json index 8f8ac568361..df81a694c47 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId14", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "adtelligent": { @@ -21,58 +17,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json index 4eeef9fce47..458b300cb66 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "620160380", - "impid": "impId14", + "id": "bid_id", + "impid": "imp_id", "price": 8.43, "adm": "adm14", "crid": "crid14", @@ -13,28 +13,9 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "8.40", - "hb_bidder_adtelligent": "adtelligent", - "hb_size_adtelligent": "300x250", - "hb_size": "300x250", - "hb_pb_adtelligent": "8.40", - "hb_bidder": "adtelligent", - "hb_cache_id": "f5c5f34c-ad41-4894-b42b-dd5c86978a4a", - "hb_cache_id_adtelligent": "f5c5f34c-ad41-4894-b42b-dd5c86978a4a", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_adtelligent": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_adtelligent": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}f5c5f34c-ad41-4894-b42b-dd5c86978a4a", - "cacheId": "f5c5f34c-ad41-4894-b42b-dd5c86978a4a" - } - } - } + "type": "banner" + }, + "origbidcpm": 8.43 } } ], @@ -45,11 +26,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "adtelligent": "{{ adtelligent.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "adtelligent": "{{ adtelligent.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-cache-adtelligent-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-cache-adtelligent-request.json deleted file mode 100644 index 25d582fd85c..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-cache-adtelligent-request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "620160380", - "impid": "impId14", - "price": 8.43, - "adm": "adm14", - "crid": "crid14", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-cache-adtelligent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-cache-adtelligent-response.json deleted file mode 100644 index 735d17ddc94..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-cache-adtelligent-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "f5c5f34c-ad41-4894-b42b-dd5c86978a4a" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-request.json index e1f0195bbb3..37f9cd9d81e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-request.json @@ -1,19 +1,9 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "testimpid", + "id": "imp_id", "banner": { - "format": [ - { - "w": 320, - "h": 250 - }, - { - "w": 320, - "h": 300 - } - ], "w": 320, "h": 250 }, @@ -29,62 +19,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "AV-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-response.json index ca4e6ee1db4..8ad1ca21f66 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-advangelists-bid-response.json @@ -1,5 +1,5 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ @@ -7,8 +7,8 @@ "crid": "24080", "adid": "2068416", "price": 0.01, - "id": "testid", - "impid": "testimpid", + "id": "bid_id", + "impid": "imp_id", "cid": "8048" } ], diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-request.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-request.json index 00c7c183f0f..1bc2f300158 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-request.json @@ -1,19 +1,9 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "testimpid", + "id": "imp_id", "banner": { - "format": [ - { - "w": 320, - "h": 250 - }, - { - "w": 320, - "h": 300 - } - ], "w": 320, "h": 250 }, @@ -23,61 +13,10 @@ "placement": "dummyplacement" } }, - "tagid": "impId021" + "tagid": "tag_id" } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json index 6bdcf3da2fa..df8ec148aa1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json @@ -1,37 +1,20 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "testid", - "impid": "testimpid", + "id": "bid_id", + "impid": "imp_id", "price": 0.01, "adid": "2068416", "cid": "8048", "crid": "24080", "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "0.00", - "hb_cache_id_advangelists": "3c0769d8-0dd9-465c-8bf3-f570605ba698", - "hb_bidder_advangelists": "advangelists", - "hb_bidder": "advangelists", - "hb_cache_id": "3c0769d8-0dd9-465c-8bf3-f570605ba698", - "hb_pb_advangelists": "0.00", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_advangelists": "{{ cache.host }}", - "hb_cache_path": "/cache", - "hb_cache_path_advangelists": "/cache" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}3c0769d8-0dd9-465c-8bf3-f570605ba698", - "cacheId": "3c0769d8-0dd9-465c-8bf3-f570605ba698" - } - } - } + "type": "banner" + }, + "origbidcpm": 0.01 } } ], @@ -42,11 +25,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "advangelists": "{{ advangelists.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "advangelists": "{{ advangelists.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-cache-advangelists-request.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-cache-advangelists-request.json deleted file mode 100644 index ca8e3ab2f6a..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-cache-advangelists-request.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "crid": "24080", - "adid": "2068416", - "price": 0.01, - "id": "testid", - "impid": "testimpid", - "cid": "8048" - } - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-cache-advangelists-response.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-cache-advangelists-response.json deleted file mode 100644 index c0100536be1..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-cache-advangelists-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "3c0769d8-0dd9-465c-8bf3-f570605ba698" - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-adxcg-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-adxcg-bid-request.json new file mode 100644 index 00000000000..a288d9da032 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-adxcg-bid-request.json @@ -0,0 +1,41 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 480 + }, + "ext": { + "bidder": { + "adzoneid": "adzoneid" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-adxcg-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-adxcg-bid-response.json new file mode 100644 index 00000000000..55c7cd2ee2d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-adxcg-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 320, + "w": 480 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-request.json new file mode 100644 index 00000000000..85a575a39dd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 480 + }, + "ext": { + "adxcg": { + "adzoneid": "adzoneid" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json new file mode 100644 index 00000000000..d98a2838d6a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 480, + "h": 320, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "adxcg", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "adxcg": "{{ adxcg.response_time_ms }}" + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-request.json new file mode 100644 index 00000000000..3efa13f3534 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "somePlacement", + "ext": { + "bidder": { + "placement": "somePlacement" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json new file mode 100644 index 00000000000..e291739474c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-request.json new file mode 100644 index 00000000000..41ad02e1bbc --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "adyoulike": { + "placement": "somePlacement" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json new file mode 100644 index 00000000000..1fc346d781a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "adyoulike", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "adyoulike": "{{ adyoulike.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-1.json deleted file mode 100644 index a22c9678a27..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-1.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "id":"tid", - "imp":[ - { - "id":"impId001", - "banner":{ - "format":[ - { - "w":300, - "h":250 - } - ] - }, - "tagid":"test" - } - ], - "site":{ - "domain":"example.com", - "page":"http://www.example.com", - "publisher":{ - "id":"publisherId" - }, - "ext":{ - "amp":0 - } - }, - "device":{ - "ua":"userAgent", - "dnt":2, - "ip":"193.168.244.1", - "pxratio":4.2, - "language":"en", - "ifa":"ifaId" - }, - "user":{ - "buyeruid":"AJA-UID", - "ext":{ - "consent":"consentValue", - "digitrust":{ - "id":"id", - "keyv":123, - "pref":0 - } - } - }, - "at":1, - "tmax":5000, - "cur":[ - "USD" - ], - "source":{ - "fd":1, - "tid":"tid" - }, - "regs":{ - "ext":{ - "gdpr":0 - } - }, - "ext":{ - "prebid":{ - "currency":{ - "rates":{ - "EUR":{ - "USD":1.2406 - }, - "USD":{ - "EUR":0.811 - } - } - }, - "targeting":{ - "pricegranularity":{ - "precision":2, - "ranges":[ - { - "max":20, - "increment":0.1 - } - ] - }, - "includewinners":true, - "includebidderkeys":true - }, - "cache":{ - "bids":{ - - }, - "vastxml":{ - "ttlseconds":120 - } - }, - "auctiontimestamp":1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-2.json deleted file mode 100644 index 8dffa5581ca..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request-2.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "id":"tid", - "imp":[ - { - "id":"impId002", - "video":{ - "mimes":[ - "video/mp4" - ], - "protocols":[ - 2, - 5 - ], - "w":1024, - "h":576 - }, - "tagid":"aja-test" - } - ], - "site":{ - "domain":"example.com", - "page":"http://www.example.com", - "publisher":{ - "id":"publisherId" - }, - "ext":{ - "amp":0 - } - }, - "device":{ - "ua":"userAgent", - "dnt":2, - "ip":"193.168.244.1", - "pxratio":4.2, - "language":"en", - "ifa":"ifaId" - }, - "user":{ - "buyeruid":"AJA-UID", - "ext":{ - "consent":"consentValue", - "digitrust":{ - "id":"id", - "keyv":123, - "pref":0 - } - } - }, - "at":1, - "tmax":5000, - "cur":[ - "USD" - ], - "source":{ - "fd":1, - "tid":"tid" - }, - "regs":{ - "ext":{ - "gdpr":0 - } - }, - "ext":{ - "prebid":{ - "currency":{ - "rates":{ - "EUR":{ - "USD":1.2406 - }, - "USD":{ - "EUR":0.811 - } - } - }, - "targeting":{ - "pricegranularity":{ - "precision":2, - "ranges":[ - { - "max":20, - "increment":0.1 - } - ] - }, - "includewinners":true, - "includebidderkeys":true - }, - "cache":{ - "bids":{ - - }, - "vastxml":{ - "ttlseconds":120 - } - }, - "auctiontimestamp":1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request.json new file mode 100644 index 00000000000..ab6b2a43314 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-request.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "test" + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response-2.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response-2.json deleted file mode 100644 index 7a8e8c2bd88..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response-2.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid002", - "impid": "impId002", - "price": 9.99, - "crid": "crid002", - "cid": "cid002", - "adomain": [ - "psacentral.org" - ], - "h": 250, - "w": 300 - } - ], - "seat": "datablocks" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json new file mode 100644 index 00000000000..d2f3908c4b3 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 10, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-request.json index 8461aa2d80e..86a339c1198 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-request.json @@ -1,107 +1,23 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "aja": { "asi": "test" } } - }, - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "w": 1024, - "h": 576 - }, - "ext": { - "aja": { - "asi": "aja-test" - } - } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json index a5bce5a6beb..5b8ce2dfb32 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json @@ -1,101 +1,38 @@ { - "id":"tid", - "seatbid":[ + "id": "request_id", + "seatbid": [ { - "bid":[ + "bid": [ { - "id":"bid002", - "impid":"impId002", - "price":9.99, - "adomain":[ - "psacentral.org" - ], - "cid":"cid002", - "crid":"crid002", - "w":300, - "h":250, - "ext":{ - "prebid":{ - "type":"video", - "targeting":{ - "hb_cache_path_aja":"{{ cache.path }}", - "hb_cache_host_aja":"{{ cache.host }}", - "hb_bidder_aja":"aja", - "hb_cache_id":"e7965b2e-0aa3-4252-a22c-580ed010e619", - "hb_cache_id_aja":"e7965b2e-0aa3-4252-a22c-580ed010e619", - "hb_pb_aja":"9.90", - "hb_pb":"9.90", - "hb_uuid_aja":"44a52b06-b29f-4819-a05f-db36b9e7b8fc", - "hb_cache_path":"{{ cache.path }}", - "hb_size_aja":"300x250", - "hb_uuid":"44a52b06-b29f-4819-a05f-db36b9e7b8fc", - "hb_size":"300x250", - "hb_bidder":"aja", - "hb_cache_host":"{{ cache.host }}" - }, - "cache":{ - "bids":{ - "url":"{{ cache.resource_url }}e7965b2e-0aa3-4252-a22c-580ed010e619", - "cacheId":"e7965b2e-0aa3-4252-a22c-580ed010e619" - }, - "vastXml":{ - "url":"{{ cache.resource_url }}44a52b06-b29f-4819-a05f-db36b9e7b8fc", - "cacheId":"44a52b06-b29f-4819-a05f-db36b9e7b8fc" - } - } - } - } - }, - { - "id":"bid001", - "impid":"impId001", - "price":3.33, - "adm":"adm001", - "adid":"adid001", - "cid":"cid001", - "crid":"crid001", - "w":300, - "h":250, - "ext":{ - "prebid":{ - "type":"banner", - "targeting":{ - "hb_pb_aja":"3.30", - "hb_pb":"3.30", - "hb_cache_path_aja":"{{ cache.path }}", - "hb_cache_path":"{{ cache.path }}", - "hb_size_aja":"300x250", - "hb_size":"300x250", - "hb_cache_host_aja":"{{ cache.host }}", - "hb_bidder_aja":"aja", - "hb_bidder":"aja", - "hb_cache_id":"f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_host":"{{ cache.host }}", - "hb_cache_id_aja":"f0ab9105-cb21-4e59-b433-70f5ad6671cb" - }, - "cache":{ - "bids":{ - "url":"{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "cacheId":"f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - } + "id": "bid_id", + "impid": "imp_id", + "price": 10, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 10, + "prebid": { + "type": "banner" } } } ], - "seat":"aja", - "group":0 + "seat": "aja", + "group": 0 } ], - "cur":"USD", - "ext":{ - "responsetimemillis":{ - "cache":"{{ cache.response_time_ms }}", - "aja":"{{ aja.response_time_ms }}" + "cur": "USD", + "ext": { + "responsetimemillis": { + "aja": "{{ aja.response_time_ms }}" }, - "tmaxrequest":5000, - "prebid":{ - "auctiontimestamp":1000 + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 1626187513063 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-cache-aja-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-cache-aja-request.json deleted file mode 100644 index 5bb0050c74f..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-cache-aja-request.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "puts":[ - { - "type":"json", - "value":{ - "id":"bid001", - "impid":"impId001", - "price":3.33, - "adm":"adm001", - "adid":"adid001", - "cid":"cid001", - "crid":"crid001", - "w":300, - "h":250 - } - }, - { - "type":"json", - "value":{ - "id":"bid002", - "impid":"impId002", - "price":9.99, - "adomain":[ - "psacentral.org" - ], - "cid":"cid002", - "crid":"crid002", - "w":300, - "h":250 - } - }, - { - "type":"xml", - "value":"prebid.org wrapper", - "expiry":120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-cache-aja-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-cache-aja-response.json deleted file mode 100644 index 7d2718f9414..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-cache-aja-response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "responses": [ - { - "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - }, - { - "uuid": "e7965b2e-0aa3-4252-a22c-580ed010e619" - }, - { - "uuid": "44a52b06-b29f-4819-a05f-db36b9e7b8fc" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-request.json new file mode 100644 index 00000000000..fad4d9ff3b0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "sid": "testSid", + "token": "testToken" + } + } + } + ], + "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": 2000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json new file mode 100644 index 00000000000..e291739474c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-request.json b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-request.json new file mode 100644 index 00000000000..c546583ac9d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "algorix": { + "sid": "testSid", + "token": "testToken" + } + } + } + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json new file mode 100644 index 00000000000..564d7b809f0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "algorix", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "algorix": "{{ algorix.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 2000 + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-request.json new file mode 100644 index 00000000000..af35997f9b2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-request.json @@ -0,0 +1,47 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "tagid": "adUnitId", + "ext": { + "bidder": { + "tagId": "tag_id", + "adUnitId": "adUnitId" + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "tag_id", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-response.json new file mode 100644 index 00000000000..cccc2e18017 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-amx-bid-response.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "adm": "00:00:15", + "nurl": "https://example.com/nurl", + "cid": "8048", + "ext": { + "himp": [ + "https://example.com/imp-tracker/pixel.gif?param=1¶m2=2" + ], + "startdelay": 0 + } + } + ], + "type": "video" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-request.json new file mode 100644 index 00000000000..86fc7c03d16 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-request.json @@ -0,0 +1,31 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "ext": { + "amx": { + "tagId": "tag_id", + "adUnitId": "adUnitId" + } + }, + "tagid": "adUnitId" + } + ], + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json new file mode 100644 index 00000000000..7ce9213f2fc --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json @@ -0,0 +1,41 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "adm": "00:00:15", + "nurl": "", + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 0.01, + "himp": [ + "https://example.com/imp-tracker/pixel.gif?param=1¶m2=2" + ], + "startdelay": 0 + } + } + ], + "seat": "amx", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "amx": "{{ amx.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-1.json deleted file mode 100644 index 7764d9a5dca..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-1.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId001", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 300, - "h": 250 - }, - "ext": { - "bidder": { - "token": "1234" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-2.json deleted file mode 100644 index 1908c31cef2..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request-2.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "w": 1024, - "h": 576 - }, - "ext": { - "bidder": { - "token": "12345" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request.json new file mode 100644 index 00000000000..b29345fe86f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-request.json @@ -0,0 +1,41 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "token": "1234" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-response-1.json deleted file mode 100644 index 95a93284e04..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-response-1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid001", - "impid": "impId001", - "price": 3.33, - "adid": "adid001", - "crid": "crid001", - "cid": "cid001", - "adm": "adm001", - "h": 250, - "w": 300 - } - ] - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-response-2.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-response-2.json deleted file mode 100644 index 77c1598fac3..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-response-2.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid002", - "impid": "impId002", - "price": 5.55, - "adid": "adid002", - "crid": "crid002", - "cid": "cid002", - "adm": "adm002", - "h": 576, - "w": 1024 - } - ] - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-response.json new file mode 100644 index 00000000000..e291739474c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-applogy-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-request.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-request.json index 641d7062bae..03d39510d1a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-request.json @@ -1,94 +1,20 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "applogy": { "token": "1234" } } - }, - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "w": 1024, - "h": 576 - }, - "ext": { - "applogy": { - "token": "12345" - } - } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-response.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-response.json index e196306801e..c8f1c73f6f6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-auction-applogy-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 3.33, "adm": "adm001", "adid": "adid001", @@ -15,70 +15,9 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "3.30", - "hb_size_applogy": "300x250", - "hb_bidder_applogy": "applogy", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_cache_host_applogy": "{{ cache.host }}", - "hb_cache_path_applogy": "{{ cache.path }}", - "hb_cache_id_applogy": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_bidder": "applogy", - "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_pb_applogy": "3.30", - "hb_cache_host": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - } - } - } - }, - { - "id": "bid002", - "impid": "impId002", - "price": 5.55, - "adm": "adm002", - "adid": "adid002", - "cid": "cid002", - "crid": "crid002", - "w": 1024, - "h": 576, - "ext": { - "prebid": { - "type": "video", - "targeting": { - "hb_cache_host_applogy": "{{ cache.host }}", - "hb_cache_path_applogy": "{{ cache.path }}", - "hb_cache_id": "e7965b2e-0aa3-4252-a22c-580ed010e619", - "hb_pb_applogy": "5.50", - "hb_pb": "5.50", - "hb_size_applogy": "1024x576", - "hb_bidder_applogy": "applogy", - "hb_cache_path": "{{ cache.path }}", - "hb_uuid": "44a52b06-b29f-4819-a05f-db36b9e7b8fc", - "hb_size": "1024x576", - "hb_cache_id_applogy": "e7965b2e-0aa3-4252-a22c-580ed010e619", - "hb_bidder": "applogy", - "hb_cache_host": "{{ cache.host }}", - "hb_uuid_applogy": "44a52b06-b29f-4819-a05f-db36b9e7b8fc" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}e7965b2e-0aa3-4252-a22c-580ed010e619", - "cacheId": "e7965b2e-0aa3-4252-a22c-580ed010e619" - }, - "vastXml": { - "url": "{{ cache.resource_url }}44a52b06-b29f-4819-a05f-db36b9e7b8fc", - "cacheId": "44a52b06-b29f-4819-a05f-db36b9e7b8fc" - } - } - } + "type": "banner" + }, + "origbidcpm": 3.33 } } ], @@ -89,11 +28,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "applogy": "{{ applogy.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "applogy": "{{ applogy.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-cache-applogy-request.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-cache-applogy-request.json deleted file mode 100644 index 883ffec2c36..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-cache-applogy-request.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 3.33, - "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", - "w": 300, - "h": 250 - } - }, - { - "type": "json", - "value": { - "id": "bid002", - "impid": "impId002", - "price": 5.55, - "adm": "adm002", - "adid": "adid002", - "cid": "cid002", - "crid": "crid002", - "w": 1024, - "h": 576 - } - }, - { - "type": "xml", - "value": "adm002", - "expiry": 120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-cache-applogy-response.json b/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-cache-applogy-response.json deleted file mode 100644 index 7d2718f9414..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/applogy/test-cache-applogy-response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "responses": [ - { - "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - }, - { - "uuid": "e7965b2e-0aa3-4252-a22c-580ed010e619" - }, - { - "uuid": "44a52b06-b29f-4819-a05f-db36b9e7b8fc" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json index c5033bf45c5..ae649271eb2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-request.json @@ -1,82 +1,24 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "test-imp-banner-id", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 500, - "h": 400 + "w": 300, + "h": 250 }, "ext": { - "avocet": { - "placement": "5ea9601ac865f911007f1b6a" + "prebid": { + "bidder": { + "avocet": { + "placement": "5ea9601ac865f911007f1b6a" + } + } } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json index b3514e3d759..cc260a44b94 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "7706636740145184841", - "impid": "test-imp-banner-id", + "id": "bid_id", + "impid": "imp_id", "price": 0.5, "adm": "some-test-ad", "adid": "29681110", @@ -18,34 +18,13 @@ "w": 1024, "h": 576, "ext": { - "bidder": { - "avocet": { - "duration": 30 - } + "avocet": { + "duration": 30 }, "prebid": { - "type": "video", - "targeting": { - "hb_pb": "0.50", - "hb_bidder_avocet": "avocet", - "hb_cache_path_avocet": "{{ cache.path }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_host_avocet": "{{ cache.host }}", - "hb_bidder": "avocet", - "hb_cache_id": "78f9a6dd-d08c-4b80-ba0f-0159b9add9bf", - "hb_cache_host": "{{ cache.host }}", - "hb_pb_avocet": "0.50", - "hb_cache_id_avocet": "78f9a6dd-d08c-4b80-ba0f-0159b9add9bf", - "hb_size": "1024x576", - "hb_size_avocet": "1024x576" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}78f9a6dd-d08c-4b80-ba0f-0159b9add9bf", - "cacheId": "78f9a6dd-d08c-4b80-ba0f-0159b9add9bf" - } - } - } + "type": "video" + }, + "origbidcpm": 0.5 } } ], @@ -55,129 +34,11 @@ ], "cur": "USD", "ext": { - "debug": { - "httpcalls": { - "cache": [ - { - "uri": "{{ cache.endpoint }}", - "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-banner-id\",\"price\":0.5,\"adm\":\"some-test-ad\",\"adid\":\"29681110\",\"adomain\":[\"advertsite.com\"],\"cid\":\"772\",\"crid\":\"29681110\",\"api\":1,\"w\":1024,\"h\":576,\"ext\":{\"avocet\":{\"duration\":30}}}}]}", - "responsebody": "{\"responses\":[{\"uuid\":\"78f9a6dd-d08c-4b80-ba0f-0159b9add9bf\"}]}", - "status": 200 - } - ], - "avocet": [ - { - "uri": "{{ avocet.exchange_uri }}", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"placement\":\"5ea9601ac865f911007f1b6a\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"AV-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", - "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-banner-id\",\"price\":0.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"advertsite.com\"],\"cid\":\"772\",\"crid\":\"29681110\",\"h\":576,\"w\":1024,\"api\":1,\"ext\":{\"avocet\":{\"duration\":30}}}]}]}", - "status": 200 - } - ] - }, - "resolvedrequest": { - "id": "tid", - "imp": [ - { - "id": "test-imp-banner-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 500, - "h": 400 - }, - "ext": { - "avocet": { - "placement": "5ea9601ac865f911007f1b6a" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } - } - }, "responsetimemillis": { - "cache": "{{ cache.response_time_ms }}", "avocet": "{{ avocet.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-request-1.json index 9b30034e435..bd5141197c4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-request-1.json @@ -1,17 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "test-imp-banner-id", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 500, - "h": 400 + "w": 300, + "h": 250 }, "ext": { "bidder": { @@ -21,10 +15,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -32,67 +26,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid" : "AV-UID", - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-response-1.json index 95aea20e958..33cefe24cb9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-avocet-bid-response-1.json @@ -1,15 +1,17 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "7706636740145184841", - "impid": "test-imp-banner-id", + "id": "bid_id", + "impid": "imp_id", "price": 0.500000, "adid": "29681110", "adm": "some-test-ad", - "adomain": ["advertsite.com"], + "adomain": [ + "advertsite.com" + ], "cid": "772", "crid": "29681110", "h": 576, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-cache-avocet-request.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-cache-avocet-request.json deleted file mode 100644 index bc89734d9f0..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-cache-avocet-request.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "7706636740145184841", - "impid": "test-imp-banner-id", - "price": 0.500000, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": ["advertsite.com"], - "cid": "772", - "crid": "29681110", - "api" : 1, - "h": 576, - "w": 1024, - "ext": { - "avocet": { - "duration": 30 - } - } - } - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-cache-avocet-response.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-cache-avocet-response.json deleted file mode 100644 index 0b2fbde6c47..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-cache-avocet-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "78f9a6dd-d08c-4b80-ba0f-0159b9add9bf" - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-request.json b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-request.json new file mode 100644 index 00000000000..dae37969b57 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "axonix": { + "supplyId": "someSupplyId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json new file mode 100644 index 00000000000..31a15adc1f9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "axonix", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "axonix": "{{ axonix.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-axonix-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-axonix-bid-request.json new file mode 100644 index 00000000000..0f5d0235a40 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-axonix-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 0, + "ext": { + "bidder": { + "supplyId": "someSupplyId" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-axonix-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-axonix-bid-response.json new file mode 100644 index 00000000000..e291739474c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-axonix-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-request.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-request.json index 623d78352da..8aed8adf217 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-request.json @@ -1,38 +1,8 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId01", - "video": { - "mimes": [ - "mimes" - ] - }, - "ext": { - "beachfront": { - "appId": "beachfrontAppId1", - "bidfloor": 1, - "videoResponseType": "nurl" - } - } - }, - { - "id": "impId02", - "video": { - "mimes": [ - "mimes" - ] - }, - "ext": { - "beachfront": { - "appId": "beachfrontAppId", - "bidfloor": 2, - "videoResponseType": "adm" - } - } - }, - { - "id": "impId03", + "id": "imp_id", "banner": { "format": [ { @@ -49,58 +19,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json index b98f55e0f01..5338cc8c4d4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "impId03Banner", - "impid": "impId03", + "id": "imp_idBanner", + "impid": "imp_id", "price": 2.942807912826538, "adm": "
    ", "crid": "crid_3", @@ -13,28 +13,10 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "2.90", - "hb_bidder_beachfront": "beachfront", - "hb_size_beachfront": "300x250", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_host_beachfront": "{{ cache.host }}", - "hb_size": "300x250", - "hb_cache_path_beachfront": "{{ cache.path }}", - "hb_bidder": "beachfront", - "hb_cache_id_beachfront": "9c33f779-d352-4d85-8ad2-8a245dc276ce", - "hb_cache_id": "9c33f779-d352-4d85-8ad2-8a245dc276ce", - "hb_pb_beachfront": "2.90", - "hb_cache_host": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}9c33f779-d352-4d85-8ad2-8a245dc276ce", - "cacheId": "9c33f779-d352-4d85-8ad2-8a245dc276ce" - } - } - } + "type": "banner" + }, + "origbidcpm": 2.942807912826538, + "origbidcur": "USD" } } ], @@ -45,11 +27,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "beachfront": "{{ beachfront.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "beachfront": "{{ beachfront.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-1.json deleted file mode 100644 index 98ece0b09ac..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-1.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "isPrebid": true, - "appId": "beachfrontAppId1", - "videoResponseType": "nurl", - "request": { - "id": "tid", - "imp": [ - { - "id": "impId01", - "video": { - "mimes": [ - "mimes" - ], - "w": 300, - "h": 250 - }, - "bidfloor": 1, - "secure": 0 - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "devicetype": 2, - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "BF-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-2.json deleted file mode 100644 index f5443122e4e..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-2.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "appId": "beachfrontAppId", - "videoResponseType": "adm", - "request": { - "id": "tid", - "imp": [ - { - "id": "impId02", - "video": { - "mimes": [ - "mimes" - ], - "w": 300, - "h": 250 - }, - "bidfloor": 2, - "secure": 0 - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "devicetype": 2, - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "BF-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-3.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-3.json deleted file mode 100644 index b5160d305d0..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request-3.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "slots": [ - { - "slot": "impId03", - "id": "beachfrontAppId", - "bidfloor": 3, - "sizes": [ - { - "w": 300, - "h": 250 - } - ] - } - ], - "domain": "example.com", - "page": "http://www.example.com", - "secure": 0, - "isMobile": 0, - "ua": "userAgent", - "dnt": 2, - "user": { - "buyeruid": "BF-UID" - }, - "adapterName": "BF_PREBID_S2S", - "adapterVersion": "0.9.0", - "ip": "193.168.244.1", - "requestId": "tid" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request.json new file mode 100644 index 00000000000..d70774fb558 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-request.json @@ -0,0 +1,25 @@ +{ + "slots": [ + { + "slot": "imp_id", + "id": "beachfrontAppId", + "bidfloor": 3, + "sizes": [ + { + "w": 300, + "h": 250 + } + ] + } + ], + "domain": "www.example.com", + "page": "http://www.example.com", + "secure": 0, + "isMobile": 0, + "ua": "userAgent", + "adapterName": "BF_PREBID_S2S", + "adapterVersion": "0.9.2", + "ip": "193.168.244.1", + "requestId": "request_id", + "real204": true +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-response-1.json deleted file mode 100644 index 42d482e0d94..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-response-1.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "tid", - "seatBid": [ - { - "bid": [ - { - "id": "bid1", - "impid": "impId01", - "price": 7.77, - "nurl": "https://useast.bfmio.com/getBids?aid=bid:70b99087-1b92-4e81-bc42-05c940fd6014:11bc5dd5-7421-4dd8-c926-40fa653bec76:20.0:20.0&v=1&dsp=5d8391a85f35945a70c9ddf0,0.01&i_type=pre", - "crid": "70b99087-1b92-4e81-bc42-05c940fd6014", - "w": 300, - "h": 250, - "ext": { - "prebid": { - "type": "video" - }, - "bidder": {} - } - } - ], - "seat": "beachfront" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-response-2.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-response-2.json deleted file mode 100644 index 191f372cb69..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-response-2.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "id": "tid", - "seatBid": [ - { - "bid": [ - { - "id": "bid2", - "impid": "impId02", - "price": 9.99, - "adm": "IONeptune00:00:15", - "adid": "1088", - "adomain": [ - "beachfront.io" - ], - "cid": "277", - "crid": "532", - "w": 300, - "h": 250, - "ext": { - "prebid": { - "type": "video" - } - } - } - ], - "seat": "beachfront" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-response-3.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-response-3.json deleted file mode 100644 index ad44bef6d07..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-beachfront-bid-response-3.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "crid": "crid_3", - "price": 2.942808, - "w": 300, - "h": 250, - "slot": "impId03", - "adm": "
    ", - "crid": "crid_3", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-cache-beachfront-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-cache-beachfront-response.json deleted file mode 100644 index d2d7957f841..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-cache-beachfront-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "9c33f779-d352-4d85-8ad2-8a245dc276ce" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json index 279bf8e18fd..9c1b4f2fe94 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-request.json @@ -1,86 +1,24 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "uuid", + "id": "bid_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { - "beintoo": { - "tagid": "25251" + "prebid": { + "bidder": { + "beintoo": { + "tagid": "25251" + } + } } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId", - "ua": "Android Chrome/60", - "ip" : "127.0.0.1" - }, - "site": { - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json index 8e4cb308a7e..9419a85783d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json @@ -1,41 +1,22 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "uuid", - "impid": "uuid", + "id": "bid_id", + "impid": "bid_id", "price": 2.942808, - "adm": "
    ", "adid": "94395500", "crid": "94395500", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "2.90", - "hb_cache_id_beintoo": "9a5d11a7-de5a-4ce4-8e89-d37f18a10045", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_beintoo": "{{ cache.path }}", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_beintoo": "{{ cache.host }}", - "hb_size": "300x250", - "hb_bidder": "beintoo", - "hb_cache_id": "9a5d11a7-de5a-4ce4-8e89-d37f18a10045", - "hb_bidder_beintoo": "beintoo", - "hb_size_beintoo": "300x250", - "hb_pb_beintoo": "2.90" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}9a5d11a7-de5a-4ce4-8e89-d37f18a10045", - "cacheId": "9a5d11a7-de5a-4ce4-8e89-d37f18a10045" - } - } - } + "type": "banner" + }, + "origbidcpm": 2.942808, + "origbidcur": "USD" } } ], @@ -45,127 +26,11 @@ ], "cur": "USD", "ext": { - "debug": { - "httpcalls": { - "beintoo": [ - { - "uri": "{{ beintoo.exchange_uri }}", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"BT-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", - "responsebody": "{\"id\":\"some_test_auction\",\"seatbid\":[{\"seat\":\"12356\",\"bid\":[{\"id\":\"uuid\",\"adm\":\"
    \",\"impid\":\"uuid\",\"ttl\":300,\"crid\":\"94395500\",\"w\":300,\"price\":2.942808,\"adid\":\"94395500\",\"h\":250}]}],\"cur\":\"USD\"}", - "status": 200 - } - ], - "cache": [ - { - "uri": "{{ cache.endpoint }}", - "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"uuid\",\"impid\":\"uuid\",\"price\":2.942808,\"adm\":\"
    \",\"adid\":\"94395500\",\"crid\":\"94395500\",\"w\":300,\"h\":250}}]}", - "responsebody": "{\"responses\":[{\"uuid\":\"9a5d11a7-de5a-4ce4-8e89-d37f18a10045\"}]}", - "status": 200 - } - ] - }, - "resolvedrequest": { - "id": "tid", - "imp": [ - { - "id": "uuid", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "beintoo": { - "tagid": "25251" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "Android Chrome/60", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } - } - }, "responsetimemillis": { - "cache": "{{ cache.response_time_ms }}", "beintoo": "{{ beintoo.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-request.json index 23914b4b674..473ddf1aeb7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-request.json @@ -1,10 +1,9 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "uuid", + "id": "bid_id", "banner": { - "format": [], "w": 300, "h": 250 }, @@ -13,78 +12,27 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 } }, "device": { - "ua": "Android Chrome/60", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid" : "BT-UID", - "ext" : { - "consent" : "consentValue", - "digitrust" : { - "id" : "id", - "keyv" : 123, - "pref" : 0 - } - } + "ua": "userAgent", + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-response.json index e1df1077904..f655098a411 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-beintoo-bid-response.json @@ -1,13 +1,11 @@ { - "id": "some_test_auction", + "id": "request_id", "seatbid": [ { - "seat": "12356", "bid": [ { - "id": "uuid", - "adm": "
    ", "adid": "94395500", @@ -14,28 +14,10 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "2.90", - "hb_cache_id_emx_digital": "9a5d11a7-de5a-4ce4-8e89-d37f18a10045", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_emx_digital": "{{ cache.path }}", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_emx_digital": "{{ cache.host }}", - "hb_size": "300x250", - "hb_bidder": "emx_digital", - "hb_cache_id": "9a5d11a7-de5a-4ce4-8e89-d37f18a10045", - "hb_bidder_emx_digital": "emx_digital", - "hb_size_emx_digital": "300x250", - "hb_pb_emx_digital": "2.90" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}9a5d11a7-de5a-4ce4-8e89-d37f18a10045", - "cacheId": "9a5d11a7-de5a-4ce4-8e89-d37f18a10045" - } - } - } + "type": "banner" + }, + "origbidcpm": 2.942808, + "origbidcur": "USD" } } ], @@ -45,127 +27,11 @@ ], "cur": "USD", "ext": { - "debug": { - "httpcalls": { - "emx_digital": [ - { - "uri": "{{ emx_digital.exchange_uri }}?t=1000&ts=2060541160", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"uuid\",\"banner\":{\"format\":[],\"w\":300,\"h\":250},\"tagid\":\"25251\",\"secure\":0}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"Android Chrome/60\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", - "responsebody": "{\"id\":\"some_test_auction\",\"seatbid\":[{\"seat\":\"12356\",\"bid\":[{\"id\":\"uuid\",\"adm\":\"
    \",\"impid\":\"uuid\",\"ttl\":300,\"crid\":\"94395500\",\"w\":300,\"price\":2.942808,\"adid\":\"94395500\",\"h\":250}]}],\"cur\":\"USD\"}", - "status": 200 - } - ], - "cache": [ - { - "uri": "{{ cache.endpoint }}", - "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"uuid\",\"impid\":\"uuid\",\"price\":2.942808,\"adm\":\"
    \",\"adid\":\"94395500\",\"crid\":\"94395500\",\"w\":300,\"h\":250}}]}", - "responsebody": "{\"responses\":[{\"uuid\":\"9a5d11a7-de5a-4ce4-8e89-d37f18a10045\"}]}", - "status": 200 - } - ] - }, - "resolvedrequest": { - "id": "tid", - "imp": [ - { - "id": "uuid", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "emx_digital": { - "tagid": "25251" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "Android Chrome/60", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } - } - }, "responsetimemillis": { - "cache": "{{ cache.response_time_ms }}", - "emx_digital": "{{ emx_digital.response_time_ms }}" + "emx_digital": 0 }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-cache-emxdigital-request.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-cache-emxdigital-request.json deleted file mode 100644 index e1f7a597707..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-cache-emxdigital-request.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "uuid", - "adm": "
    \"}", + "adm": "{\"assets\":[{\"id\":0,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":1,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", "adomain": [ "appnexus.com" ], diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json index b65c278477a..ccb2a71e5db 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-request.json @@ -22,35 +22,37 @@ ] }, "ext": { - "rubicon": { - "accountId": 2001, - "siteId": 3001, - "zoneId": 4001, - "inventory": { - "rating": [ - "5-star" - ], - "prodtype": [ - "tech" - ] - }, - "visitor": { - "ucat": [ - "new" - ], - "search": [ - "iphone" - ] - }, - "video": { - "size_id": 15, - "playerWidth": 780, - "playerHeight": "438", - "skip": 5, - "skipdelay": 1 - } - }, "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001, + "inventory": { + "rating": [ + "5-star" + ], + "prodtype": [ + "tech" + ] + }, + "visitor": { + "ucat": [ + "new" + ], + "search": [ + "iphone" + ] + }, + "video": { + "size_id": 15, + "playerWidth": 780, + "playerHeight": "438", + "skip": 5, + "skipdelay": 1 + } + } + }, "is_rewarded_inventory": 1 } } @@ -79,49 +81,57 @@ ] }, "ext": { - "appnexus": { - "member": "103", - "inv_code": "abc", - "reserve": 1.0, - "position": "below", - "traffic_source_code": "trafficSource", - "keywords": [ - { - "key": "foo", - "value": [ - "bar", - "baz" + "prebid": { + "bidder": { + "appnexus": { + "member": "103", + "inv_code": "abc", + "reserve": 1.0, + "position": "below", + "traffic_source_code": "trafficSource", + "keywords": [ + { + "key": "foo", + "value": [ + "bar", + "baz" + ] + } ] - } - ] - }, - "appnexusAlias": { - "member": "104", - "inv_code": "abc", - "reserve": 1.0, - "position": "above", - "traffic_source_code": "trafficSourceAlias", - "keywords": [ - { - "key": "foo", - "value": [ - "barAlias", - "bazAlias" + }, + "appnexusAlias": { + "member": "104", + "inv_code": "abc", + "reserve": 1.0, + "position": "above", + "traffic_source_code": "trafficSourceAlias", + "keywords": [ + { + "key": "foo", + "value": [ + "barAlias", + "bazAlias" + ] + } ] } - ] + } } } }, { "id": "impId131", "native": { - "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"required\":1,\"title\":{\"len\":500}},{\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"required\":0,\"data\":{\"len\":200}},{\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"required\":0,\"data\":{\"len\":40}},{\"required\":0,\"data\":{\"type\":11}}]}", + "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"required\":1,\"title\":{\"len\":500}},{\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"required\":0,\"data\":{\"type\":4,\"len\":200}},{\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"required\":0,\"data\":{\"len\":40}},{\"required\":0,\"data\":{\"type\":11}}]}", "ver": "1.1" }, "ext": { - "appnexus": { - "placement_id": 9880618 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 9880618 + } + } } } }, @@ -133,8 +143,12 @@ ] }, "ext": { - "appnexus": { - "placement_id": 10433394 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 10433394 + } + } } } }, @@ -155,31 +169,12 @@ } } } - }, - { - "id": "impStoredBidResponse", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "prebid": { - "storedbidresponse": { - "bidder": "rubicon", - "id": "test-stored-bid-response" - } - } - } } ], "device": { "pxratio": 4.2, "dnt": 2, - "ip": "80.215.195.122", + "ip": "80.215.195.0", "language": "en", "ifa": "ifaId", "ext": { @@ -276,12 +271,23 @@ } }, "user": { + "data": [ + { + "ext": { + "segtax": 4 + }, + "segment": [ + { + "id": "segmentId1" + }, + { + "id": "segmentId2" + } + ] + } + ], "ext": { - "consent": "BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA", - "digitrust": { - "id": "id", - "keyv": 123 - }, + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", "eids": [ { "source": "adserver.org", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json index f068ae0499c..c4035b145bd 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-auction-rubicon-appnexus-response.json @@ -6,16 +6,16 @@ { "id": "a121a07f-1579-4465-bc5e-5c5b02a0c421", "impid": "impStoredAuctionResponse", - "crid": "crid1", "price": 0.9, + "crid": "crid1", "ext": { "prebid": { "type": "banner", "targeting": { "hb_pb": "0.90", "hb_pb_appnexus": "0.90", - "hb_cache_path": "/cache", - "hb_cache_path_appnexus": "/cache", + "hb_cache_path": "{{ cache.path }}", + "hb_cache_path_appnexus": "{{ cache.path }}", "hb_bidder_appnexus": "appnexus", "hb_bidder": "appnexus", "hb_cache_id": "765e116a-5773-49d5-a648-0b97a9907a4e", @@ -30,10 +30,12 @@ } }, "events": { - "win": "http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs", - "imp": "http://localhost:8080/event?t=imp&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + "win": "{{ event.url }}t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "imp": "{{ event.url }}t=imp&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs" } - } + }, + "origbidcpm": 0.9, + "origbidcur": "USD" } }, { @@ -54,46 +56,46 @@ "prebid": { "type": "banner", "targeting": { - "hb_pb": "5.50", "hb_pb_appnexus": "5.50", + "hb_cache_path_appnexus": "{{ cache.path }}", + "hb_cache_id": "117431c9-807a-41e1-82a7-dcd8f8875493", + "hb_cache_host_appnexus": "{{ cache.host }}", + "hb_cache_id_appnexus": "117431c9-807a-41e1-82a7-dcd8f8875493", + "hb_pb": "5.50", + "hb_cache_path": "{{ cache.path }}", "hb_size": "300x250", "hb_bidder_appnexus": "appnexus", "hb_bidder": "appnexus", - "hb_cache_id": "117431c9-807a-41e1-82a7-dcd8f8875493", - "hb_size_appnexus": "300x250", - "hb_cache_id_appnexus": "117431c9-807a-41e1-82a7-dcd8f8875493", "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_appnexus": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_appnexus": "{{ cache.path }}" - }, - "events": { - "win": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs", - "imp": "http://localhost:8080/event?t=imp&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + "hb_size_appnexus": "300x250" }, "cache": { "bids": { "url": "{{ cache.resource_url }}117431c9-807a-41e1-82a7-dcd8f8875493", "cacheId": "117431c9-807a-41e1-82a7-dcd8f8875493" } + }, + "events": { + "win": "{{ event.url }}t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "imp": "{{ event.url }}t=imp&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs" } }, - "bidder": { - "appnexus": { - "brand_id": 1, - "auction_id": 8189378542222915032, - "bidder_id": 2, - "bid_ad_type": 0, - "ranking_price": 0.0 - } - } + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + }, + "origbidcpm": 5.5, + "origbidcur": "USD" } }, { "id": "928185755156387460", "impid": "impId131", "price": 1.0, - "adm": "{\"assets\":[{\"id\":0,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", + "adm": "{\"assets\":[{\"id\":0,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":1,\"img\":{\"type\":3,\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":2,\"data\":{\"type\":4,\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"type\":2,\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", "adid": "69595837", "adomain": [ "appnexus.com" @@ -110,136 +112,48 @@ "targeting": { "hb_pb": "1.00", "hb_pb_appnexus": "1.00", + "hb_cache_path": "{{ cache.path }}", + "hb_cache_path_appnexus": "{{ cache.path }}", "hb_bidder_appnexus": "appnexus", "hb_bidder": "appnexus", "hb_cache_id": "6cf69b42-96f5-4ba1-a984-a9b4d8ff21cf", - "hb_cache_id_appnexus": "6cf69b42-96f5-4ba1-a984-a9b4d8ff21cf", "hb_cache_host": "{{ cache.host }}", "hb_cache_host_appnexus": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_appnexus": "{{ cache.path }}" - }, - "events": { - "win": "http://localhost:8080/event?t=win&b=928185755156387460&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs", - "imp": "http://localhost:8080/event?t=imp&b=928185755156387460&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + "hb_cache_id_appnexus": "6cf69b42-96f5-4ba1-a984-a9b4d8ff21cf" }, "cache": { "bids": { "url": "{{ cache.resource_url }}6cf69b42-96f5-4ba1-a984-a9b4d8ff21cf", "cacheId": "6cf69b42-96f5-4ba1-a984-a9b4d8ff21cf" } - } - }, - "bidder": { - "appnexus": { - "brand_id": 1, - "brand_category_id": 1, - "auction_id": 5607483846416358664, - "bidder_id": 2, - "bid_ad_type": 3 - } - } - } - } - ], - "seat": "appnexus", - "group": 0 - }, - { - "bid": [ - { - "id": "7706636740145184840", - "impid": "impId3", - "price": 5.00, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": [ - "appnexus.com" - ], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "cat": [], - "w": 300, - "h": 250, - "ext": { - "prebid": { - "type": "banner", - "targeting": { - "hb_bidder_appnexusAlias": "appnexusAlias", - "hb_pb_appnexusAlias": "5.00", - "hb_size_appnexusAlias": "300x250", - "hb_cache_id_appnexusAlias": "91912e5b-dfa8-42bc-9c7e-df6ce0449c19", - "hb_cache_host_appnexusAlias": "{{ cache.host }}", - "hb_cache_path_appnexusAlias": "{{ cache.path }}" }, "events": { - "win": "http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs", - "imp": "http://localhost:8080/event?t=imp&b=7706636740145184840&a=5001&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs" - }, - "cache": { - "bids": { - "url": "{{ cache_resource_url }}91912e5b-dfa8-42bc-9c7e-df6ce0449c19", - "cacheId": "91912e5b-dfa8-42bc-9c7e-df6ce0449c19" - } + "win": "{{ event.url }}t=win&b=928185755156387460&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "imp": "{{ event.url }}t=imp&b=928185755156387460&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs" } }, - "bidder": { - "appnexus": { - "brand_id": 350, - "brand_category_id": 350, - "auction_id": 8189378542222915031, - "bidder_id": 2, - "bid_ad_type": 0, - "ranking_price": 0.0 - } - } + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 3 + }, + "origbidcpm": 1.0, + "origbidcur": "USD" } } ], - "seat": "appnexusAlias", + "seat": "appnexus", "group": 0 }, { "bid": [ - { - "id": "f227a07f-1579-4465-bc5e-5c5b02a0c181", - "impid": "impStoredBidResponse", - "crid": "crid1", - "price": 0.8, - "ext": { - "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "0.80", - "hb_pb_rubicon": "0.80", - "hb_cache_id_rubicon": "734b7948-e41d-4c14-b2c3-c31634b32376", - "hb_cache_path": "/cache", - "hb_bidder": "rubicon", - "hb_bidder_rubicon": "rubicon", - "hb_cache_id": "734b7948-e41d-4c14-b2c3-c31634b32376", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_rubicon": "{{ cache.path }}", - "hb_cache_host_rubicon": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}734b7948-e41d-4c14-b2c3-c31634b32376", - "cacheId": "734b7948-e41d-4c14-b2c3-c31634b32376" - } - }, - "events": { - "win": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c181&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs", - "imp": "http://localhost:8080/event?t=imp&b=f227a07f-1579-4465-bc5e-5c5b02a0c181&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" - } - } - } - }, { "id": "f227a07f-1579-4465-bc5e-5c5b02a0c180", "impid": "impStoredAuctionResponse", - "crid": "crid1", "price": 0.8, + "crid": "crid1", "ext": { "prebid": { "type": "banner", @@ -257,66 +171,68 @@ } }, "events": { - "win": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs", - "imp": "http://localhost:8080/event?t=imp&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + "win": "{{ event.url }}t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "imp": "{{ event.url }}t=imp&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs" } - } + }, + "origbidcpm": 0.8, + "origbidcur": "USD" } }, { "id": "880290288", "impid": "impId1", "price": 8.43, - "adm": "<\/Impression>", + "adm": "", "crid": "crid1", "w": 300, "h": 250, + "exp": 120, "ext": { "prebid": { "type": "video", "targeting": { + "hb_size_rubicon": "300x250", + "hb_cache_id": "683fe79f-6df7-4971-ac70-820e0486992d", + "hb_cache_path_rubicon": "{{ cache.path }}", + "hb_cache_host_rubicon": "{{ cache.host }}", "hb_pb": "8.40", "hb_pb_rubicon": "8.40", "hb_cache_id_rubicon": "683fe79f-6df7-4971-ac70-820e0486992d", + "hb_cache_path": "{{ cache.path }}", + "hb_uuid": "b2528f73-96ab-42ab-8f15-fbe6ed779a26", "hb_size": "300x250", - "hb_size_rubicon": "300x250", + "hb_uuid_rubicon": "b2528f73-96ab-42ab-8f15-fbe6ed779a26", "hb_bidder": "rubicon", "hb_bidder_rubicon": "rubicon", - "hb_cache_id": "683fe79f-6df7-4971-ac70-820e0486992d", - "hb_uuid_rubicon": "b2528f73-96ab-42ab-8f15-fbe6ed779a26", - "hb_uuid": "b2528f73-96ab-42ab-8f15-fbe6ed779a26", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_rubicon": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_rubicon": "{{ cache.path }}" - }, - "events": { - "win": "http://localhost:8080/event?t=win&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs", - "imp": "http://localhost:8080/event?t=imp&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + "hb_cache_host": "{{ cache.host }}" }, "cache": { - "vastXml": { - "url": "{{ cache.resource_url }}b2528f73-96ab-42ab-8f15-fbe6ed779a26", - "cacheId": "b2528f73-96ab-42ab-8f15-fbe6ed779a26" - }, "bids": { "url": "{{ cache.resource_url }}683fe79f-6df7-4971-ac70-820e0486992d", "cacheId": "683fe79f-6df7-4971-ac70-820e0486992d" + }, + "vastXml": { + "url": "{{ cache.resource_url }}b2528f73-96ab-42ab-8f15-fbe6ed779a26", + "cacheId": "b2528f73-96ab-42ab-8f15-fbe6ed779a26" } + }, + "events": { + "win": "{{ event.url }}t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "imp": "{{ event.url }}t=imp&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs" } }, - "bidder": { - "rp": { - "targeting": [ - { - "key": "rpfl_1001", - "values": [ - "2_tier0100" - ] - } - ] - } - } + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + }, + "origbidcpm": 8.43 } }, { @@ -331,53 +247,153 @@ "prebid": { "type": "banner", "targeting": { + "hb_size_rubicon": "300x600", + "hb_cache_id": "4fe59ef5-6fb4-48c5-88b6-9870257fc49e", + "hb_cache_path_rubicon": "{{ cache.path }}", + "hb_cache_host_rubicon": "{{ cache.host }}", "hb_pb": "4.20", "hb_pb_rubicon": "4.20", "hb_cache_id_rubicon": "4fe59ef5-6fb4-48c5-88b6-9870257fc49e", + "hb_cache_path": "{{ cache.path }}", "hb_size": "300x600", - "hb_size_rubicon": "300x600", "hb_bidder": "rubicon", "hb_bidder_rubicon": "rubicon", - "hb_cache_id": "4fe59ef5-6fb4-48c5-88b6-9870257fc49e", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_rubicon": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_rubicon": "{{ cache.path }}" - }, - "events": { - "win": "http://localhost:8080/event?t=win&b=466223845&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs", - "imp": "http://localhost:8080/event?t=imp&b=466223845&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + "hb_cache_host": "{{ cache.host }}" }, "cache": { "bids": { "url": "{{ cache.resource_url }}4fe59ef5-6fb4-48c5-88b6-9870257fc49e", "cacheId": "4fe59ef5-6fb4-48c5-88b6-9870257fc49e" } + }, + "events": { + "win": "{{ event.url }}t=win&b=466223845&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "imp": "{{ event.url }}t=imp&b=466223845&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs" } - } + }, + "origbidcpm": 4.26 } } ], "seat": "rubicon", "group": 0 + }, + { + "bid": [ + { + "id": "7706636740145184840", + "impid": "impId3", + "price": 5.0, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "cat": [], + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_bidder_appnexusAlias": "appnexusAlias", + "hb_pb_appnexusAlias": "5.00", + "hb_size_appnexusAlias": "300x250", + "hb_cache_path_appnexusAlias": "{{ cache.path }}", + "hb_cache_host_appnexusAlias": "{{ cache.host }}", + "hb_cache_id_appnexusAlias": "91912e5b-dfa8-42bc-9c7e-df6ce0449c19" + }, + "cache": { + "bids": { + "url": "{{ cache.resource_url }}91912e5b-dfa8-42bc-9c7e-df6ce0449c19", + "cacheId": "91912e5b-dfa8-42bc-9c7e-df6ce0449c19" + } + }, + "events": { + "win": "{{ event.url }}t=win&b=7706636740145184840&a=5001&aid=tid&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs", + "imp": "{{ event.url }}t=imp&b=7706636740145184840&a=5001&aid=tid&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs" + } + }, + "appnexus": { + "brand_id": 350, + "brand_category_id": 350, + "auction_id": 8189378542222915031, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + }, + "origbidcpm": 5.0, + "origbidcur": "USD" + } + } + ], + "seat": "appnexusAlias", + "group": 0 } ], "cur": "USD", "ext": { "debug": { "httpcalls": { + "cache": [ + { + "uri": "{{ cache.endpoint }}", + "requestheaders": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"466223845\",\"impid\":\"impId2\",\"price\":4.26,\"adm\":\"adm2\",\"crid\":\"crid2\",\"w\":300,\"h\":600,\"ext\":{\"origbidcpm\":4.26,\"prebid\":{\"type\":\"banner\",\"events\":{\"win\":\"http://localhost:8080/event?t=win&b=466223845&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs\",\"imp\":\"http://localhost:8080/event?t=imp&b=466223845&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs\"}}},\"wurl\":\"http://localhost:8080/event?t=win&b=466223845&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs\"},\"aid\":\"tid\"},{\"type\":\"json\",\"value\":{\"id\":\"7706636740145184841\",\"impid\":\"impId3\",\"price\":5.5,\"adm\":\"some-test-ad\",\"adid\":\"29681110\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"w\":300,\"h\":250,\"ext\":{\"appnexus\":{\"brand_id\":1,\"auction_id\":8189378542222915032,\"bidder_id\":2,\"bid_ad_type\":0,\"ranking_price\":0.0},\"origbidcpm\":5.5,\"origbidcur\":\"USD\",\"prebid\":{\"type\":\"banner\",\"events\":{\"win\":\"http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs\",\"imp\":\"http://localhost:8080/event?t=imp&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs\"}}},\"wurl\":\"http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs\"},\"aid\":\"tid\"},{\"type\":\"json\",\"value\":{\"id\":\"7706636740145184840\",\"impid\":\"impId3\",\"price\":5,\"adm\":\"some-test-ad\",\"adid\":\"29681110\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"cat\":[],\"w\":300,\"h\":250,\"ext\":{\"appnexus\":{\"brand_id\":350,\"brand_category_id\":350,\"auction_id\":8189378542222915031,\"bidder_id\":2,\"bid_ad_type\":0,\"ranking_price\":0.0},\"origbidcpm\":5,\"origbidcur\":\"USD\",\"prebid\":{\"type\":\"banner\",\"events\":{\"win\":\"http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&aid=tid&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs\",\"imp\":\"http://localhost:8080/event?t=imp&b=7706636740145184840&a=5001&aid=tid&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs\"}}},\"wurl\":\"http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&aid=tid&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs\"},\"aid\":\"tid\"},{\"type\":\"json\",\"value\":{\"id\":\"a121a07f-1579-4465-bc5e-5c5b02a0c421\",\"impid\":\"impStoredAuctionResponse\",\"price\":0.9,\"crid\":\"crid1\",\"ext\":{\"origbidcpm\":0.9,\"origbidcur\":\"USD\",\"prebid\":{\"type\":\"banner\",\"events\":{\"win\":\"http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs\",\"imp\":\"http://localhost:8080/event?t=imp&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs\"}}},\"wurl\":\"http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs\"},\"aid\":\"tid\"},{\"type\":\"json\",\"value\":{\"id\":\"928185755156387460\",\"impid\":\"impId131\",\"price\":1,\"adm\":\"{\\\"assets\\\":[{\\\"id\\\":0,\\\"title\\\":{\\\"text\\\":\\\"This is an example Prebid Native creative\\\"}},{\\\"id\\\":1,\\\"img\\\":{\\\"url\\\":\\\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\\\",\\\"w\\\":3000,\\\"h\\\":2250,\\\"ext\\\":{\\\"appnexus\\\":{\\\"prevent_crop\\\":0}}}},{\\\"id\\\":2,\\\"data\\\":{\\\"value\\\":\\\"Prebid.org\\\"}},{\\\"id\\\":3,\\\"data\\\":{\\\"value\\\":\\\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\\\"}}],\\\"link\\\":{\\\"url\\\":\\\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\\\"},\\\"imptrackers\\\":[\\\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\\\"],\\\"jstracker\\\":\\\"\\\"}\",\"adid\":\"69595837\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=69595837\",\"cid\":\"958\",\"crid\":\"69595837\",\"cat\":[\"IAB20-3\"],\"ext\":{\"appnexus\":{\"brand_id\":1,\"brand_category_id\":1,\"auction_id\":5607483846416358664,\"bidder_id\":2,\"bid_ad_type\":3},\"origbidcpm\":1,\"origbidcur\":\"USD\",\"prebid\":{\"type\":\"native\",\"events\":{\"win\":\"http://localhost:8080/event?t=win&b=928185755156387460&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs\",\"imp\":\"http://localhost:8080/event?t=imp&b=928185755156387460&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs\"}}},\"wurl\":\"http://localhost:8080/event?t=win&b=928185755156387460&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs\"},\"aid\":\"tid\"},{\"type\":\"json\",\"value\":{\"id\":\"f227a07f-1579-4465-bc5e-5c5b02a0c180\",\"impid\":\"impStoredAuctionResponse\",\"price\":0.8,\"crid\":\"crid1\",\"ext\":{\"origbidcpm\":0.8,\"origbidcur\":\"USD\",\"prebid\":{\"type\":\"banner\",\"events\":{\"win\":\"http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs\",\"imp\":\"http://localhost:8080/event?t=imp&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs\"}}},\"wurl\":\"http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs\"},\"aid\":\"tid\"},{\"type\":\"json\",\"value\":{\"id\":\"880290288\",\"impid\":\"impId1\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid1\",\"w\":300,\"h\":250,\"ext\":{\"rp\":{\"targeting\":[{\"key\":\"rpfl_1001\",\"values\":[\"2_tier0100\"]}]},\"origbidcpm\":8.43,\"prebid\":{\"type\":\"video\",\"events\":{\"win\":\"http://localhost:8080/event?t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs\",\"imp\":\"http://localhost:8080/event?t=imp&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs\"}}},\"wurl\":\"http://localhost:8080/event?t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs\"},\"aid\":\"tid\"},{\"type\":\"xml\",\"value\":\"\",\"ttlseconds\":120,\"aid\":\"tid\"}]}", + "responsebody": "{\"responses\":[{\"uuid\":\"4fe59ef5-6fb4-48c5-88b6-9870257fc49e\"},{\"uuid\":\"117431c9-807a-41e1-82a7-dcd8f8875493\"},{\"uuid\":\"91912e5b-dfa8-42bc-9c7e-df6ce0449c19\"},{\"uuid\":\"765e116a-5773-49d5-a648-0b97a9907a4e\"},{\"uuid\":\"6cf69b42-96f5-4ba1-a984-a9b4d8ff21cf\"},{\"uuid\":\"c75130ed-bcdd-4821-ad91-90cf835615c5\"},{\"uuid\":\"683fe79f-6df7-4971-ac70-820e0486992d\"},{\"uuid\":\"b2528f73-96ab-42ab-8f15-fbe6ed779a26\"}]}", + "status": 200 + } + ], "appnexus": [ { "uri": "{{ appnexus.exchange_uri }}?member_id=103", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId3\",\"banner\":{\"format\":[{\"w\":300,\"h\":250},{\"w\":300,\"h\":600}],\"w\":300,\"h\":250,\"pos\":3},\"tagid\":\"abc\",\"bidfloor\":1.0,\"ext\":{\"appnexus\":{\"keywords\":\"foo=bar,foo=baz\",\"traffic_source_code\":\"trafficSource\"}}},{\"id\":\"impId131\",\"native\":{\"request\":\"{\\\"ver\\\":\\\"1.1\\\",\\\"context\\\":1,\\\"contextsubtype\\\":11,\\\"plcmttype\\\":4,\\\"plcmtcnt\\\":1,\\\"assets\\\":[{\\\"id\\\":0,\\\"required\\\":1,\\\"title\\\":{\\\"len\\\":500}},{\\\"id\\\":1,\\\"required\\\":1,\\\"img\\\":{\\\"type\\\":3,\\\"wmin\\\":1,\\\"hmin\\\":1}},{\\\"id\\\":2,\\\"required\\\":0,\\\"data\\\":{\\\"len\\\":200}},{\\\"id\\\":3,\\\"required\\\":0,\\\"data\\\":{\\\"type\\\":2,\\\"len\\\":15000}},{\\\"id\\\":4,\\\"required\\\":0,\\\"data\\\":{\\\"len\\\":40}},{\\\"id\\\":5,\\\"required\\\":0,\\\"data\\\":{\\\"type\\\":11}}]}\",\"ver\":\"1.1\"},\"ext\":{\"appnexus\":{\"placement_id\":9880618}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"5001\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"80.215.195.0\",\"pxratio\":4.2,\"language\":\"en\",\"ext\":{\"prebid\":{\"interstitial\":{\"minwidthperc\":50,\"minheightperc\":60}}}},\"user\":{\"ext\":{\"consent\":\"BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\",\"ext\":{\"schain\":{\"ver\":\"1.0\"}}},\"regs\":{\"ext\":{\"us_privacy\":\"1YNN\"}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\"},\"currency\":{\"rates\":{\"EUR\":{\"USD\":1.2406},\"USD\":{\"EUR\":0.811}},\"usepbsrates\":false},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"events\":{},\"auctiontimestamp\":1000,\"integration\":\"dmbjs\",\"channel\":{\"name\":\"web\"}}}}", - "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"seat\":\"958\",\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"impId3\",\"price\":5.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"h\":250,\"w\":300,\"ext\":{\"appnexus\":{\"brand_id\":1,\"auction_id\":8189378542222915032,\"bidder_id\":2,\"bid_ad_type\":0,\"ranking_price\":0.0}}},{\"id\":\"928185755156387460\",\"impid\":\"impId131\",\"price\":1.0,\"adid\":\"69595837\",\"adm\":\"{\\\"assets\\\":[{\\\"id\\\":0,\\\"img\\\":{\\\"url\\\":\\\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\\\",\\\"w\\\":3000,\\\"h\\\":2250,\\\"ext\\\":{\\\"appnexus\\\":{\\\"prevent_crop\\\":0}}}},{\\\"id\\\":1,\\\"title\\\":{\\\"text\\\":\\\"This is an example Prebid Native creative\\\"}},{\\\"id\\\":2,\\\"data\\\":{\\\"value\\\":\\\"Prebid.org\\\"}},{\\\"id\\\":3,\\\"data\\\":{\\\"value\\\":\\\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\\\"}}],\\\"link\\\":{\\\"url\\\":\\\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\\\"},\\\"imptrackers\\\":[\\\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\\\"],\\\"jstracker\\\":\\\"\\\"}\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=69595837\",\"cid\":\"958\",\"crid\":\"69595837\",\"ext\":{\"appnexus\":{\"brand_id\":1,\"brand_category_id\":1,\"auction_id\":5607483846416358664,\"bidder_id\":2,\"bid_ad_type\":3}}}]}],\"bidid\":\"5778926625248726496\",\"cur\":\"USD\"}", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "Sec-GPC": [ + "1" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId3\",\"banner\":{\"format\":[{\"w\":300,\"h\":250},{\"w\":300,\"h\":600}],\"w\":300,\"h\":250,\"pos\":3},\"tagid\":\"abc\",\"bidfloor\":1.0,\"ext\":{\"appnexus\":{\"keywords\":\"foo=bar,foo=baz\",\"traffic_source_code\":\"trafficSource\"}}},{\"id\":\"impId131\",\"native\":{\"request\":\"{\\\"ver\\\":\\\"1.1\\\",\\\"context\\\":1,\\\"contextsubtype\\\":11,\\\"plcmttype\\\":4,\\\"plcmtcnt\\\":1,\\\"assets\\\":[{\\\"id\\\":0,\\\"required\\\":1,\\\"title\\\":{\\\"len\\\":500}},{\\\"id\\\":1,\\\"required\\\":1,\\\"img\\\":{\\\"type\\\":3,\\\"wmin\\\":1,\\\"hmin\\\":1}},{\\\"id\\\":2,\\\"required\\\":0,\\\"data\\\":{\\\"type\\\":4,\\\"len\\\":200}},{\\\"id\\\":3,\\\"required\\\":0,\\\"data\\\":{\\\"type\\\":2,\\\"len\\\":15000}},{\\\"id\\\":4,\\\"required\\\":0,\\\"data\\\":{\\\"len\\\":40}},{\\\"id\\\":5,\\\"required\\\":0,\\\"data\\\":{\\\"type\\\":11}}]}\",\"ver\":\"1.1\"},\"ext\":{\"appnexus\":{\"placement_id\":9880618}}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"5001\",\"domain\":\"example.com\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"80.215.195.0\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\",\"ext\":{\"prebid\":{\"interstitial\":{\"minwidthperc\":50,\"minheightperc\":60}}}},\"user\":{\"buyeruid\":\"12345\",\"data\":[{\"segment\":[{\"id\":\"segmentId1\"},{\"id\":\"segmentId2\"}],\"ext\":{\"segtax\":4}}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"eids\":[{\"source\":\"adserver.org\",\"uids\":[{\"id\":\"cd96870f-f53d-4986-a08e-cd1612fb13b0\",\"ext\":{\"rtiPartner\":\"TDID\"}}]},{\"source\":\"liveintent.com\",\"uids\":[{\"id\":\"efcf3a33-2eaf-4d6b-bf11-a411f134278c\"}],\"ext\":{\"segments\":[\"999\",\"888\"]}},{\"source\":\"pubcid\",\"id\":\"29cfaea8-a429-48fc-9537-8a19a8eb4f0d\"}]}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\",\"ext\":{\"schain\":{\"ver\":\"1.0\"}}},\"regs\":{\"ext\":{\"us_privacy\":\"1YNN\"}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\"},\"currency\":{\"rates\":{\"EUR\":{\"USD\":1.2406},\"USD\":{\"EUR\":0.811}},\"usepbsrates\":false},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"events\":{},\"auctiontimestamp\":1000,\"integration\":\"dmbjs\",\"channel\":{\"name\":\"web\"},\"pbs\":{\"endpoint\":\"/openrtb2/auction\"}}}}", + "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"seat\":\"958\",\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"impId3\",\"price\":5.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"h\":250,\"w\":300,\"ext\":{\"appnexus\":{\"brand_id\":1,\"auction_id\":8189378542222915032,\"bidder_id\":2,\"bid_ad_type\":0,\"ranking_price\":0.0}}},{\"id\":\"928185755156387460\",\"impid\":\"impId131\",\"price\":1.0,\"adid\":\"69595837\",\"adm\":\"{\\\"assets\\\":[{\\\"id\\\":0,\\\"title\\\":{\\\"text\\\":\\\"This is an example Prebid Native creative\\\"}},{\\\"id\\\":1,\\\"img\\\":{\\\"url\\\":\\\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\\\",\\\"w\\\":3000,\\\"h\\\":2250,\\\"ext\\\":{\\\"appnexus\\\":{\\\"prevent_crop\\\":0}}}},{\\\"id\\\":2,\\\"data\\\":{\\\"value\\\":\\\"Prebid.org\\\"}},{\\\"id\\\":3,\\\"data\\\":{\\\"value\\\":\\\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\\\"}}],\\\"link\\\":{\\\"url\\\":\\\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\\\"},\\\"imptrackers\\\":[\\\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\\\"],\\\"jstracker\\\":\\\"\\\"}\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=69595837\",\"cid\":\"958\",\"crid\":\"69595837\",\"ext\":{\"appnexus\":{\"brand_id\":1,\"brand_category_id\":1,\"auction_id\":5607483846416358664,\"bidder_id\":2,\"bid_ad_type\":3}}}]}],\"bidid\":\"5778926625248726496\",\"cur\":\"USD\"}", "status": 200 } ], "appnexusAlias": [ { "uri": "{{ appnexus.exchange_uri }}?member_id=104", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId3\",\"banner\":{\"format\":[{\"w\":300,\"h\":250},{\"w\":300,\"h\":600}],\"w\":300,\"h\":250,\"pos\":1},\"tagid\":\"abc\",\"bidfloor\":1.0,\"ext\":{\"appnexus\":{\"keywords\":\"foo=barAlias,foo=bazAlias\",\"traffic_source_code\":\"trafficSourceAlias\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"5001\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"80.215.195.0\",\"pxratio\":4.2,\"language\":\"en\",\"ext\":{\"prebid\":{\"interstitial\":{\"minwidthperc\":50,\"minheightperc\":60}}}},\"user\":{\"ext\":{\"consent\":\"BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA\"}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\",\"ext\":{\"schain\":{\"ver\":\"1.0\"}}},\"regs\":{\"ext\":{\"us_privacy\":\"1YNN\"}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\"},\"currency\":{\"rates\":{\"EUR\":{\"USD\":1.2406},\"USD\":{\"EUR\":0.811}},\"usepbsrates\":false},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"events\":{},\"auctiontimestamp\":1000,\"integration\":\"dmbjs\",\"channel\":{\"name\":\"web\"}}}}", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "Sec-GPC": [ + "1" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId3\",\"banner\":{\"format\":[{\"w\":300,\"h\":250},{\"w\":300,\"h\":600}],\"w\":300,\"h\":250,\"pos\":1},\"tagid\":\"abc\",\"bidfloor\":1.0,\"ext\":{\"appnexus\":{\"keywords\":\"foo=barAlias,foo=bazAlias\",\"traffic_source_code\":\"trafficSourceAlias\"}}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"5001\",\"domain\":\"example.com\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"80.215.195.0\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\",\"ext\":{\"prebid\":{\"interstitial\":{\"minwidthperc\":50,\"minheightperc\":60}}}},\"user\":{\"buyeruid\":\"12345\",\"data\":[{\"segment\":[{\"id\":\"segmentId1\"},{\"id\":\"segmentId2\"}],\"ext\":{\"segtax\":4}}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"eids\":[{\"source\":\"adserver.org\",\"uids\":[{\"id\":\"cd96870f-f53d-4986-a08e-cd1612fb13b0\",\"ext\":{\"rtiPartner\":\"TDID\"}}]},{\"source\":\"liveintent.com\",\"uids\":[{\"id\":\"efcf3a33-2eaf-4d6b-bf11-a411f134278c\"}],\"ext\":{\"segments\":[\"999\",\"888\"]}},{\"source\":\"pubcid\",\"id\":\"29cfaea8-a429-48fc-9537-8a19a8eb4f0d\"}]}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\",\"ext\":{\"schain\":{\"ver\":\"1.0\"}}},\"regs\":{\"ext\":{\"us_privacy\":\"1YNN\"}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\"},\"currency\":{\"rates\":{\"EUR\":{\"USD\":1.2406},\"USD\":{\"EUR\":0.811}},\"usepbsrates\":false},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"events\":{},\"auctiontimestamp\":1000,\"integration\":\"dmbjs\",\"channel\":{\"name\":\"web\"},\"pbs\":{\"endpoint\":\"/openrtb2/auction\"}}}}", "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"seat\":\"959\",\"bid\":[{\"id\":\"7706636740145184840\",\"impid\":\"impId3\",\"price\":5.0,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"h\":250,\"w\":300,\"cat\":[\"IAB20-3\"],\"ext\":{\"appnexus\":{\"brand_id\":350,\"brand_category_id\":350,\"auction_id\":8189378542222915031,\"bidder_id\":2,\"bid_ad_type\":0,\"ranking_price\":0.0}}}]}],\"bidid\":\"5778926625248726496\",\"cur\":\"USD\"}", "status": 200 } @@ -385,24 +401,50 @@ "rubicon": [ { "uri": "{{ rubicon.exchange_uri }}?tk_xint=dmbjs", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"video\":{\"mimes\":[\"mimes\"],\"minduration\":20,\"maxduration\":60,\"protocols\":[1],\"w\":300,\"h\":250,\"startdelay\":5,\"skipmin\":0,\"skipafter\":0,\"playbackmethod\":[1],\"ext\":{\"skip\":5,\"skipdelay\":1,\"rp\":{\"size_id\":15},\"videotype\":\"rewarded\"}},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"rating\":[\"5-star\"],\"prodtype\":[\"tech\"],\"page\":[\"http://www.example.com\"]},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"80.215.195.0\",\"pxratio\":4.2,\"language\":\"en\",\"ext\":{\"rp\":{\"pixelratio\":4.2}}},\"user\":{\"ext\":{\"consent\":\"BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA\",\"rp\":{\"target\":{\"ucat\":[\"new\"],\"search\":[\"iphone\"]}}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\",\"ext\":{\"schain\":{\"ver\":\"1.0\",\"complete\":1,\"nodes\":[{\"asi\":\"superads.com\",\"sid\":\"123\",\"hp\":1}]}}},\"regs\":{\"ext\":{\"us_privacy\":\"1YNN\"}}}", - "responsebody": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"bid\":[{\"id\":\"880290288\",\"impid\":\"impId1\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid1\",\"w\":300,\"h\":250,\"ext\":{\"rp\":{\"targeting\":[{\"key\":\"rpfl_1001\",\"values\":[\"2_tier0100\"]}]}}}],\"seat\":\"seatId1\",\"group\":0}]}", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Sec-GPC": [ + "1" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId1\",\"video\":{\"mimes\":[\"mimes\"],\"minduration\":20,\"maxduration\":60,\"protocols\":[1],\"w\":300,\"h\":250,\"startdelay\":5,\"skipmin\":0,\"skipafter\":0,\"playbackmethod\":[1],\"ext\":{\"skip\":5,\"skipdelay\":1,\"rp\":{\"size_id\":15},\"videotype\":\"rewarded\"}},\"ext\":{\"rp\":{\"zone_id\":4001,\"target\":{\"rating\":[\"5-star\"],\"prodtype\":[\"tech\"],\"page\":[\"http://www.example.com\"]},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":2001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":3001}}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"80.215.195.0\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\",\"ext\":{\"rp\":{\"pixelratio\":4.2}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"segment\":[{\"id\":\"segmentId1\"},{\"id\":\"segmentId2\"}],\"ext\":{\"segtax\":4}}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"eids\":[{\"source\":\"adserver.org\",\"uids\":[{\"id\":\"cd96870f-f53d-4986-a08e-cd1612fb13b0\",\"ext\":{\"rtiPartner\":\"TDID\"}}]},{\"source\":\"liveintent.com\",\"uids\":[{\"id\":\"efcf3a33-2eaf-4d6b-bf11-a411f134278c\"}],\"ext\":{\"segments\":[\"999\",\"888\"]}},{\"source\":\"pubcid\",\"id\":\"29cfaea8-a429-48fc-9537-8a19a8eb4f0d\"}],\"tpid\":[{\"source\":\"tdid\",\"uid\":\"cd96870f-f53d-4986-a08e-cd1612fb13b0\"},{\"source\":\"liveintent.com\",\"uid\":\"efcf3a33-2eaf-4d6b-bf11-a411f134278c\"}],\"rp\":{\"target\":{\"ucat\":[\"new\"],\"search\":[\"iphone\"],\"LIseg\":[\"999\",\"888\"],\"iab\":[\"segmentId1\",\"segmentId2\"]}}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\",\"ext\":{\"schain\":{\"ver\":\"1.0\",\"complete\":1,\"nodes\":[{\"asi\":\"superads.com\",\"sid\":\"123\",\"hp\":1}]}}},\"regs\":{\"ext\":{\"us_privacy\":\"1YNN\"}}}", + "responsebody": "{\"id\":\"bidResponseId1\",\"seatbid\":[{\"bid\":[{\"id\":\"880290288\",\"impid\":\"impId1\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid1\",\"w\":300,\"h\":250,\"ext\":{\"rp\":{\"targeting\":[{\"key\":\"rpfl_1001\",\"values\":[\"2_tier0100\"]}]}}}],\"seat\":\"seatId1\",\"group\":0}]}", "status": 200 }, { "uri": "{{ rubicon.exchange_uri }}?tk_xint=dmbjs", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId2\",\"banner\":{\"format\":[{\"w\":300,\"h\":600}],\"w\":300,\"h\":600,\"ext\":{\"rp\":{\"size_id\":10,\"mime\":\"text/html\"}}},\"ext\":{\"rp\":{\"zone_id\":7001,\"target\":{\"page\":[\"http://www.example.com\"]},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":5001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":6001}}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"80.215.195.0\",\"pxratio\":4.2,\"language\":\"en\",\"ext\":{\"rp\":{\"pixelratio\":4.2}}},\"user\":{\"ext\":{\"consent\":\"BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA\"}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\",\"ext\":{\"schain\":{\"ver\":\"1.0\",\"complete\":1,\"nodes\":[{\"asi\":\"superads.com\",\"sid\":\"123\",\"hp\":1}]}}},\"regs\":{\"ext\":{\"us_privacy\":\"1YNN\"}}}", + "requestheaders": { + "Accept": [ + "application/json" + ], + "x-prebid": [ + "pbs-java/1.70.0" + ], + "User-Agent": [ + "prebid-server/1.0" + ], + "Sec-GPC": [ + "1" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId2\",\"banner\":{\"format\":[{\"w\":300,\"h\":600}],\"w\":300,\"h\":600,\"ext\":{\"rp\":{\"size_id\":10,\"mime\":\"text/html\"}}},\"ext\":{\"rp\":{\"zone_id\":7001,\"target\":{\"page\":[\"http://www.example.com\"]},\"track\":{\"mint\":\"\",\"mint_version\":\"\"}},\"maxbids\":1}}],\"site\":{\"domain\":\"www.example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"ext\":{\"rp\":{\"account_id\":5001}}},\"ext\":{\"amp\":0,\"rp\":{\"site_id\":6001}}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"80.215.195.0\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\",\"ext\":{\"rp\":{\"pixelratio\":4.2}}},\"user\":{\"buyeruid\":\"J5VLCWQP-26-CWFT\",\"data\":[{\"segment\":[{\"id\":\"segmentId1\"},{\"id\":\"segmentId2\"}],\"ext\":{\"segtax\":4}}],\"ext\":{\"consent\":\"CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA\",\"eids\":[{\"source\":\"adserver.org\",\"uids\":[{\"id\":\"cd96870f-f53d-4986-a08e-cd1612fb13b0\",\"ext\":{\"rtiPartner\":\"TDID\"}}]},{\"source\":\"liveintent.com\",\"uids\":[{\"id\":\"efcf3a33-2eaf-4d6b-bf11-a411f134278c\"}],\"ext\":{\"segments\":[\"999\",\"888\"]}},{\"source\":\"pubcid\",\"id\":\"29cfaea8-a429-48fc-9537-8a19a8eb4f0d\"}],\"tpid\":[{\"source\":\"tdid\",\"uid\":\"cd96870f-f53d-4986-a08e-cd1612fb13b0\"},{\"source\":\"liveintent.com\",\"uid\":\"efcf3a33-2eaf-4d6b-bf11-a411f134278c\"}],\"rp\":{\"target\":{\"LIseg\":[\"999\",\"888\"],\"iab\":[\"segmentId1\",\"segmentId2\"]}}}},\"at\":1,\"tmax\":5000,\"source\":{\"fd\":1,\"tid\":\"tid\",\"ext\":{\"schain\":{\"ver\":\"1.0\",\"complete\":1,\"nodes\":[{\"asi\":\"superads.com\",\"sid\":\"123\",\"hp\":1}]}}},\"regs\":{\"ext\":{\"us_privacy\":\"1YNN\"}}}", "responsebody": "{\"id\":\"bidResponseId2\",\"seatbid\":[{\"bid\":[{\"id\":\"466223845\",\"impid\":\"impId2\",\"price\":4.26,\"adm\":\"adm2\",\"crid\":\"crid2\",\"w\":300,\"h\":600}],\"seat\":\"seatId2\",\"group\":0}]}", "status": 200 } - ], - "cache": [ - { - "uri": "{{ cache.endpoint }}", - "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"7706636740145184840\",\"impid\":\"impId3\",\"price\":5,\"adm\":\"some-test-ad\",\"adid\":\"29681110\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"cat\":[],\"w\":300,\"h\":250,\"ext\":{\"appnexus\":{\"brand_id\":350,\"brand_category_id\":350,\"auction_id\":8189378542222915031,\"bidder_id\":2,\"bid_ad_type\":0,\"ranking_price\":0.0}},\"wurl\":\"http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs\"}},{\"type\":\"json\",\"value\":{\"id\":\"466223845\",\"impid\":\"impId2\",\"price\":4.26,\"adm\":\"adm2\",\"crid\":\"crid2\",\"w\":300,\"h\":600,\"wurl\":\"http://localhost:8080/event?t=win&b=466223845&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs\"}},{\"type\":\"json\",\"value\":{\"id\":\"a121a07f-1579-4465-bc5e-5c5b02a0c421\",\"impid\":\"impStoredAuctionResponse\",\"price\":0.9,\"crid\":\"crid1\",\"wurl\":\"http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs\"}},{\"type\":\"json\",\"value\":{\"id\":\"880290288\",\"impid\":\"impId1\",\"price\":8.43,\"adm\":\"\",\"crid\":\"crid1\",\"w\":300,\"h\":250,\"ext\":{\"rp\":{\"targeting\":[{\"key\":\"rpfl_1001\",\"values\":[\"2_tier0100\"]}]}},\"wurl\":\"http://localhost:8080/event?t=win&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs\"}},{\"type\":\"json\",\"value\":{\"id\":\"f227a07f-1579-4465-bc5e-5c5b02a0c180\",\"impid\":\"impStoredAuctionResponse\",\"price\":0.8,\"crid\":\"crid1\",\"wurl\":\"http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs\"}},{\"type\":\"json\",\"value\":{\"id\":\"f227a07f-1579-4465-bc5e-5c5b02a0c181\",\"impid\":\"impStoredBidResponse\",\"price\":0.8,\"crid\":\"crid1\",\"wurl\":\"http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c181&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs\"}},{\"type\":\"json\",\"value\":{\"id\":\"928185755156387460\",\"impid\":\"impId131\",\"price\":1,\"adm\":\"{\\\"assets\\\":[{\\\"id\\\":0,\\\"img\\\":{\\\"url\\\":\\\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\\\",\\\"w\\\":3000,\\\"h\\\":2250,\\\"ext\\\":{\\\"appnexus\\\":{\\\"prevent_crop\\\":0}}}},{\\\"id\\\":1,\\\"title\\\":{\\\"text\\\":\\\"This is an example Prebid Native creative\\\"}},{\\\"id\\\":2,\\\"data\\\":{\\\"value\\\":\\\"Prebid.org\\\"}},{\\\"id\\\":3,\\\"data\\\":{\\\"value\\\":\\\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\\\"}}],\\\"link\\\":{\\\"url\\\":\\\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\\\"},\\\"imptrackers\\\":[\\\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\\\"],\\\"jstracker\\\":\\\"\\\"}\",\"adid\":\"69595837\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=69595837\",\"cid\":\"958\",\"crid\":\"69595837\",\"cat\":[\"IAB20-3\"],\"ext\":{\"appnexus\":{\"brand_id\":1,\"brand_category_id\":1,\"auction_id\":5607483846416358664,\"bidder_id\":2,\"bid_ad_type\":3}},\"wurl\":\"http://localhost:8080/event?t=win&b=928185755156387460&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs\"}},{\"type\":\"json\",\"value\":{\"id\":\"7706636740145184841\",\"impid\":\"impId3\",\"price\":5.5,\"adm\":\"some-test-ad\",\"adid\":\"29681110\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"w\":300,\"h\":250,\"ext\":{\"appnexus\":{\"brand_id\":1,\"auction_id\":8189378542222915032,\"bidder_id\":2,\"bid_ad_type\":0,\"ranking_price\":0.0}},\"wurl\":\"http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs\"}},{\"type\":\"xml\",\"value\":\"\",\"expiry\":120}]}", - "responsebody": "{\"responses\":[{\"uuid\":\"91912e5b-dfa8-42bc-9c7e-df6ce0449c19\"},{\"uuid\":\"765e116a-5773-49d5-a648-0b97a9907a4e\"},{\"uuid\":\"117431c9-807a-41e1-82a7-dcd8f8875493\"},{\"uuid\":\"6cf69b42-96f5-4ba1-a984-a9b4d8ff21cf\"},{\"uuid\":\"734b7948-e41d-4c14-b2c3-c31634b32376\"},{\"uuid\":\"c75130ed-bcdd-4821-ad91-90cf835615c5\"},{\"uuid\":\"683fe79f-6df7-4971-ac70-820e0486992d\"},{\"uuid\":\"4fe59ef5-6fb4-48c5-88b6-9870257fc49e\"},{\"uuid\":\"b2528f73-96ab-42ab-8f15-fbe6ed779a26\"}]}", - "status": 200 - } ] }, "resolvedrequest": { @@ -429,35 +471,37 @@ ] }, "ext": { - "rubicon": { - "accountId": 2001, - "siteId": 3001, - "zoneId": 4001, - "inventory": { - "rating": [ - "5-star" - ], - "prodtype": [ - "tech" - ] - }, - "visitor": { - "ucat": [ - "new" - ], - "search": [ - "iphone" - ] - }, - "video": { - "size_id": 15, - "playerWidth": 780, - "playerHeight": "438", - "skip": 5, - "skipdelay": 1 - } - }, "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001, + "inventory": { + "rating": [ + "5-star" + ], + "prodtype": [ + "tech" + ] + }, + "visitor": { + "ucat": [ + "new" + ], + "search": [ + "iphone" + ] + }, + "video": { + "size_id": 15, + "playerWidth": 780, + "playerHeight": "438", + "skip": 5, + "skipdelay": 1 + } + } + }, "is_rewarded_inventory": 1 } } @@ -475,14 +519,16 @@ "h": 600 }, "ext": { - "rubicon": { - "accountId": 5001, - "siteId": 6001, - "zoneId": 7001 - }, "prebid": { "storedrequest": { "id": "test-rubicon-stored-request-2" + }, + "bidder": { + "rubicon": { + "accountId": 5001, + "siteId": 6001, + "zoneId": 7001 + } } } } @@ -502,49 +548,57 @@ ] }, "ext": { - "appnexus": { - "member": "103", - "inv_code": "abc", - "reserve": 1.0, - "position": "below", - "traffic_source_code": "trafficSource", - "keywords": [ - { - "key": "foo", - "value": [ - "bar", - "baz" + "prebid": { + "bidder": { + "appnexus": { + "member": "103", + "inv_code": "abc", + "reserve": 1.0, + "position": "below", + "traffic_source_code": "trafficSource", + "keywords": [ + { + "key": "foo", + "value": [ + "bar", + "baz" + ] + } ] - } - ] - }, - "appnexusAlias": { - "member": "104", - "inv_code": "abc", - "reserve": 1.0, - "position": "above", - "traffic_source_code": "trafficSourceAlias", - "keywords": [ - { - "key": "foo", - "value": [ - "barAlias", - "bazAlias" + }, + "appnexusAlias": { + "member": "104", + "inv_code": "abc", + "reserve": 1.0, + "position": "above", + "traffic_source_code": "trafficSourceAlias", + "keywords": [ + { + "key": "foo", + "value": [ + "barAlias", + "bazAlias" + ] + } ] } - ] + } } } }, { "id": "impId131", "native": { - "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":500}},{\"id\":1,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":2,\"required\":0,\"data\":{\"len\":200}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":4,\"required\":0,\"data\":{\"len\":40}},{\"id\":5,\"required\":0,\"data\":{\"type\":11}}]}", + "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":500}},{\"id\":1,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":2,\"required\":0,\"data\":{\"type\":4,\"len\":200}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":4,\"required\":0,\"data\":{\"len\":40}},{\"id\":5,\"required\":0,\"data\":{\"type\":11}}]}", "ver": "1.1" }, "ext": { - "appnexus": { - "placement_id": 9880618 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 9880618 + } + } } } }, @@ -556,8 +610,12 @@ ] }, "ext": { - "appnexus": { - "placement_id": 10433394 + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 10433394 + } + } } } }, @@ -578,32 +636,14 @@ } } } - }, - { - "id": "impStoredBidResponse", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "prebid": { - "storedbidresponse": { - "bidder": "rubicon", - "id": "test-stored-bid-response" - } - } - } } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "5001" + "id": "5001", + "domain": "example.com" }, "ext": { "amp": 0 @@ -612,7 +652,7 @@ "device": { "ua": "userAgent", "dnt": 2, - "ip": "80.215.195.122", + "ip": "80.215.195.0", "pxratio": 4.2, "language": "en", "ifa": "ifaId", @@ -626,13 +666,23 @@ } }, "user": { + "data": [ + { + "segment": [ + { + "id": "segmentId1" + }, + { + "id": "segmentId2" + } + ], + "ext": { + "segtax": 4 + } + } + ], "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA", + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", "eids": [ { "source": "adserver.org", @@ -683,6 +733,9 @@ "ext": { "prebid": { "debug": 1, + "aliases": { + "appnexusAlias": "appnexus" + }, "currency": { "rates": { "EUR": { @@ -694,10 +747,6 @@ }, "usepbsrates": false }, - "aliases": { - "appnexusAlias": "appnexus" - }, - "events": {}, "targeting": { "pricegranularity": { "precision": 2, @@ -717,6 +766,7 @@ "ttlseconds": 120 } }, + "events": {}, "schains": [ { "bidders": [ @@ -743,15 +793,18 @@ } } ], + "auctiontimestamp": 1000, "bidders": { "rubicon": { "integration": "dmbjs" } }, "integration": "dmbjs", - "auctiontimestamp": 1000, "channel": { "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" } } } @@ -771,9 +824,9 @@ "rubicon": "{{ rubicon.response_time_ms }}", "cache": "{{ cache.response_time_ms }}" }, + "tmaxrequest": 5000, "prebid": { "auctiontimestamp": 1000 - }, - "tmaxrequest": 5000 + } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-cache-matcher-rubicon-appnexus.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-cache-matcher-rubicon-appnexus.json index 6465c55bfce..163ba9dfc10 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-cache-matcher-rubicon-appnexus.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-cache-matcher-rubicon-appnexus.json @@ -3,9 +3,8 @@ "928185755156387460@1": "6cf69b42-96f5-4ba1-a984-a9b4d8ff21cf", "7706636740145184841@5.5": "117431c9-807a-41e1-82a7-dcd8f8875493", "880290288@8.43": "683fe79f-6df7-4971-ac70-820e0486992d", - "": "b2528f73-96ab-42ab-8f15-fbe6ed779a26", + "": "b2528f73-96ab-42ab-8f15-fbe6ed779a26", "7706636740145184840@5": "91912e5b-dfa8-42bc-9c7e-df6ce0449c19", "f227a07f-1579-4465-bc5e-5c5b02a0c180@0.8": "c75130ed-bcdd-4821-ad91-90cf835615c5", - "a121a07f-1579-4465-bc5e-5c5b02a0c421@0.9": "765e116a-5773-49d5-a648-0b97a9907a4e", - "f227a07f-1579-4465-bc5e-5c5b02a0c181@0.8": "734b7948-e41d-4c14-b2c3-c31634b32376" -} \ No newline at end of file + "a121a07f-1579-4465-bc5e-5c5b02a0c421@0.9": "765e116a-5773-49d5-a648-0b97a9907a4e" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-cache-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-cache-rubicon-appnexus-request.json index aad30dbaf2f..cc3e73bc0df 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-cache-rubicon-appnexus-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-cache-rubicon-appnexus-request.json @@ -1,5 +1,29 @@ { "puts": [ + { + "type": "json", + "value": { + "id": "466223845", + "impid": "impId2", + "price": 4.26, + "adm": "adm2", + "crid": "crid2", + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "banner", + "events": { + "win": "http://localhost:8080/event?t=win&b=466223845&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "imp": "http://localhost:8080/event?t=imp&b=466223845&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + "origbidcpm": 4.26 + }, + "wurl": "http://localhost:8080/event?t=win&b=466223845&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs" + }, + "aid": "tid" + }, { "type": "json", "value": { @@ -17,8 +41,15 @@ "cat": [], "w": 300, "h": 250, - "wurl": "http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs", + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&aid=tid&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs", "ext": { + "prebid": { + "type": "banner", + "events": { + "win": "http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&aid=tid&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs", + "imp": "http://localhost:8080/event?t=imp&b=7706636740145184840&a=5001&aid=tid&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs" + } + }, "appnexus": { "brand_id": 350, "brand_category_id": 350, @@ -26,32 +57,50 @@ "bidder_id": 2, "bid_ad_type": 0, "ranking_price": 0.0 - } + }, + "origbidcpm": 5, + "origbidcur": "USD" } - } + }, + "aid": "tid" }, { "type": "json", "value": { - "id": "466223845", - "impid": "impId2", - "price": 4.26, - "adm": "adm2", - "crid": "crid2", + "id": "7706636740145184841", + "impid": "impId3", + "price": 5.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", "w": 300, - "h": 600, - "wurl": "http://localhost:8080/event?t=win&b=466223845&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" - } - }, - { - "type": "json", - "value": { - "id": "a121a07f-1579-4465-bc5e-5c5b02a0c421", - "impid": "impStoredAuctionResponse", - "crid": "crid1", - "price": 0.9, - "wurl": "http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" - } + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "ext": { + "prebid": { + "type": "banner", + "events": { + "win": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "imp": "http://localhost:8080/event?t=imp&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + }, + "origbidcpm": 5.5, + "origbidcur": "USD" + } + }, + "aid": "tid" }, { "type": "json", @@ -59,12 +108,19 @@ "id": "880290288", "impid": "impId1", "price": 8.43, - "adm": "", + "adm": "", "crid": "crid1", "w": 300, "h": 250, - "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs", "ext": { + "prebid": { + "type": "video", + "events": { + "win": "http://localhost:8080/event?t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "imp": "http://localhost:8080/event?t=imp&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, "rp": { "targeting": [ { @@ -74,29 +130,11 @@ ] } ] - } + }, + "origbidcpm": 8.43 } - } - }, - { - "type": "json", - "value": { - "id": "f227a07f-1579-4465-bc5e-5c5b02a0c180", - "impid": "impStoredAuctionResponse", - "crid": "crid1", - "price": 0.8, - "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" - } - }, - { - "type": "json", - "value": { - "id": "f227a07f-1579-4465-bc5e-5c5b02a0c181", - "impid": "impStoredBidResponse", - "crid": "crid1", - "price": 0.8, - "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c181&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" - } + }, + "aid": "tid" }, { "type": "json", @@ -104,7 +142,7 @@ "id": "928185755156387460", "impid": "impId131", "price": 1, - "adm": "{\"assets\":[{\"id\":0,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", + "adm": "{\"assets\":[{\"id\":0,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":1,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", "adid": "69595837", "adomain": [ "appnexus.com" @@ -116,49 +154,76 @@ "IAB20-3" ], "ext": { + "prebid": { + "type": "native", + "events": { + "win": "http://localhost:8080/event?t=win&b=928185755156387460&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "imp": "http://localhost:8080/event?t=imp&b=928185755156387460&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, "appnexus": { "brand_id": 1, "brand_category_id": 1, "auction_id": 5607483846416358664, "bidder_id": 2, "bid_ad_type": 3 - } + }, + "origbidcpm": 1, + "origbidcur": "USD" }, - "wurl": "http://localhost:8080/event?t=win&b=928185755156387460&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" - } + "wurl": "http://localhost:8080/event?t=win&b=928185755156387460&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs" + }, + "aid": "tid" }, { "type": "json", "value": { - "id": "7706636740145184841", - "impid": "impId3", - "price": 5.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": [ - "appnexus.com" - ], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "w": 300, - "h": 250, - "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "id": "a121a07f-1579-4465-bc5e-5c5b02a0c421", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.9, "ext": { - "appnexus": { - "brand_id": 1, - "auction_id": 8189378542222915032, - "bidder_id": 2, - "bid_ad_type": 0, - "ranking_price": 0.0 - } - } - } + "prebid": { + "type": "banner", + "events": { + "win": "http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "imp": "http://localhost:8080/event?t=imp&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + "origbidcpm": 0.9, + "origbidcur": "USD" + }, + "wurl": "http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=dmbjs" + }, + "aid": "tid" + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c180", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.8, + "ext": { + "prebid": { + "type": "banner", + "events": { + "win": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "imp": "http://localhost:8080/event?t=imp&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + "origbidcpm": 0.8, + "origbidcur": "USD" + }, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=dmbjs" + }, + "aid": "tid" }, { "type": "xml", - "value": "", - "expiry": 120 + "value": "", + "ttlseconds": 120, + "aid": "tid" } ] } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-request-1.json index 557146afda3..129f1e65bcf 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-request-1.json @@ -23,10 +23,10 @@ "ext": { "skip": 5, "skipdelay": 1, - "videotype": "rewarded", "rp": { "size_id": 15 - } + }, + "videotype": "rewarded" } }, "ext": { @@ -47,12 +47,13 @@ "mint": "", "mint_version": "" } - } + }, + "maxbids": 1 } } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { "ext": { @@ -62,10 +63,10 @@ } }, "ext": { + "amp": 0, "rp": { "site_id": 3001 - }, - "amp": 0 + } } }, "device": { @@ -74,6 +75,7 @@ "ip": "80.215.195.0", "pxratio": 4.2, "language": "en", + "ifa": "ifaId", "ext": { "rp": { "pixelratio": 4.2 @@ -81,8 +83,65 @@ } }, "user": { + "data": [ + { + "ext": { + "segtax": 4 + }, + "segment": [ + { + "id": "segmentId1" + }, + { + "id": "segmentId2" + } + ] + } + ], + "buyeruid": "J5VLCWQP-26-CWFT", "ext": { - "consent": "BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA", + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "eids": [ + { + "source": "adserver.org", + "uids": [ + { + "id": "cd96870f-f53d-4986-a08e-cd1612fb13b0", + "ext": { + "rtiPartner": "TDID" + } + } + ] + }, + { + "source": "liveintent.com", + "uids": [ + { + "id": "efcf3a33-2eaf-4d6b-bf11-a411f134278c" + } + ], + "ext": { + "segments": [ + "999", + "888" + ] + } + }, + { + "source": "pubcid", + "id": "29cfaea8-a429-48fc-9537-8a19a8eb4f0d" + } + ], + "tpid": [ + { + "source": "tdid", + "uid": "cd96870f-f53d-4986-a08e-cd1612fb13b0" + }, + { + "source": "liveintent.com", + "uid": "efcf3a33-2eaf-4d6b-bf11-a411f134278c" + } + ], "rp": { "target": { "ucat": [ @@ -90,6 +149,14 @@ ], "search": [ "iphone" + ], + "LIseg": [ + "999", + "888" + ], + "iab": [ + "segmentId1", + "segmentId2" ] } } @@ -119,4 +186,4 @@ "us_privacy": "1YNN" } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-request-2.json index 4f8585e3abd..2c63626e55a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-request-2.json @@ -31,12 +31,13 @@ "mint": "", "mint_version": "" } - } + }, + "maxbids": 1 } } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { "ext": { @@ -46,10 +47,10 @@ } }, "ext": { + "amp": 0, "rp": { "site_id": 6001 - }, - "amp": 0 + } } }, "device": { @@ -58,6 +59,7 @@ "ip": "80.215.195.0", "pxratio": 4.2, "language": "en", + "ifa": "ifaId", "ext": { "rp": { "pixelratio": 4.2 @@ -65,8 +67,77 @@ } }, "user": { + "buyeruid": "J5VLCWQP-26-CWFT", + "data": [ + { + "ext": { + "segtax": 4 + }, + "segment": [ + { + "id": "segmentId1" + }, + { + "id": "segmentId2" + } + ] + } + ], "ext": { - "consent": "BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA" + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "eids": [ + { + "source": "adserver.org", + "uids": [ + { + "id": "cd96870f-f53d-4986-a08e-cd1612fb13b0", + "ext": { + "rtiPartner": "TDID" + } + } + ] + }, + { + "source": "liveintent.com", + "uids": [ + { + "id": "efcf3a33-2eaf-4d6b-bf11-a411f134278c" + } + ], + "ext": { + "segments": [ + "999", + "888" + ] + } + }, + { + "source": "pubcid", + "id": "29cfaea8-a429-48fc-9537-8a19a8eb4f0d" + } + ], + "tpid": [ + { + "source": "tdid", + "uid": "cd96870f-f53d-4986-a08e-cd1612fb13b0" + }, + { + "source": "liveintent.com", + "uid": "efcf3a33-2eaf-4d6b-bf11-a411f134278c" + } + ], + "rp": { + "target": { + "LIseg": [ + "999", + "888" + ], + "iab": [ + "segmentId1", + "segmentId2" + ] + } + } } }, "at": 1, @@ -93,4 +164,4 @@ "us_privacy": "1YNN" } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-response-1.json index 3f7cac4c127..833bcb4bad1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus/test-rubicon-bid-response-1.json @@ -7,7 +7,7 @@ "id": "880290288", "impid": "impId1", "price": 8.43, - "adm": "", + "adm": "", "crid": "crid1", "w": 300, "h": 250, @@ -29,4 +29,4 @@ "group": 0 } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-request-1.json new file mode 100644 index 00000000000..76ba0a905c0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-request-1.json @@ -0,0 +1,139 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "video": { + "mimes": [ + "mimes" + ], + "minduration": 20, + "maxduration": 60, + "protocols": [ + 1 + ], + "w": 300, + "h": 250, + "startdelay": 5, + "skipmin": 0, + "skipafter": 0, + "playbackmethod": [ + 1 + ] + }, + "tagid": "abc", + "bidfloor": 1.0, + "ext": { + "appnexus": { + "keywords": "foo=bar,foo=baz", + "traffic_source_code": "trafficSource" + } + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "5001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "dnt": 2, + "ip": "80.215.195.0", + "pxratio": 4.2, + "language": "en", + "ifa": "ifaId", + "ext": { + "prebid": { + "interstitial": { + "minwidthperc": 50, + "minheightperc": 60 + } + } + } + }, + "user": { + "buyeruid": "12345", + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "source": { + "fd": 1, + "tid": "tid" + }, + "regs": { + "ext": { + "us_privacy": "1YNN" + } + }, + "ext": { + "prebid": { + "debug": 0, + "currency": { + "rates": { + "EUR": { + "USD": 1.2406 + }, + "USD": { + "EUR": 0.811 + } + }, + "usepbsrates": false + }, + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "cache": { + "bids": { + }, + "vastxml": { + "ttlseconds": 120 + } + }, + "events": {}, + "auctiontimestamp": 1000, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + }, + "multibid": [ + { + "bidder": "appnexus", + "maxbids": 2 + } + ], + "auctiontimestamp": 1000, + "events": {}, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-response-1.json new file mode 100644 index 00000000000..98c2116fea9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-response-1.json @@ -0,0 +1,78 @@ +{ + "id": "tid", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "impId1", + "price": 5.5, + "adid": "29681110", + "adm": "some-test-ad2", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 1, + "ranking_price": 0.000000 + } + } + }, + { + "id": "928185755156387460", + "impid": "impId1", + "price": 1.00, + "adid": "69595837", + "adm": "{\"assets\":[{\"id\":0,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 1 + } + } + }, + { + "id": "222214214214", + "impid": "impId1", + "price": 2.00, + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "adm": "some-test-ad1", + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 1 + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-request.json new file mode 100644 index 00000000000..2305a408e71 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-request.json @@ -0,0 +1,165 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "video": { + "mimes": [ + "mimes" + ], + "minduration": 20, + "maxduration": 60, + "protocols": [ + 1 + ], + "w": 300, + "h": 250, + "startdelay": 5, + "skipmin": 0, + "skipafter": 0, + "playbackmethod": [ + 1 + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001, + "inventory": { + "rating": [ + "5-star" + ], + "prodtype": [ + "tech" + ] + }, + "visitor": { + "ucat": [ + "new" + ], + "search": [ + "iphone" + ] + }, + "video": { + "size_id": 15, + "playerWidth": 780, + "playerHeight": "438", + "skip": 5, + "skipdelay": 1 + } + }, + "appnexus": { + "member": "103", + "inv_code": "abc", + "reserve": 1.0, + "position": "below", + "traffic_source_code": "trafficSource", + "keywords": [ + { + "key": "foo", + "value": [ + "bar", + "baz" + ] + } + ] + } + }, + "is_rewarded_inventory": 1 + } + } + } + ], + "device": { + "pxratio": 4.2, + "dnt": 2, + "ip": "80.215.195.122", + "language": "en", + "ifa": "ifaId", + "ext": { + "prebid": { + "interstitial": { + "minwidthperc": 50, + "minheightperc": 60 + } + } + } + }, + "site": { + "publisher": { + "id": "5001" + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "source": { + "fd": 1, + "tid": "tid" + }, + "ext": { + "prebid": { + "debug": 0, + "currency": { + "rates": { + "EUR": { + "USD": 1.2406 + }, + "USD": { + "EUR": 0.8110 + } + }, + "usepbsrates": false + }, + "events": {}, + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + } + }, + "cache": { + "bids": {}, + "vastxml": { + "ttlseconds": 120 + } + }, + "multibid": [ + { + "bidder": "rubicon", + "maxbids": 2, + "targetbiddercodeprefix": "rubN" + }, + { + "bidders": [ + "appnexus", + "someBidder" + ], + "maxbids": 2 + } + ], + "auctiontimestamp": 1000 + } + }, + "user": { + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" + } + }, + "regs": { + "ext": { + "us_privacy": "1YNN" + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-response.json new file mode 100644 index 00000000000..cdd08a9a501 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-response.json @@ -0,0 +1,232 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "21521324", + "impid": "impId1", + "price": 12.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "exp": 120, + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_size_rubicon": "300x250", + "hb_cache_id": "07a81993-a3f4-4582-89c1-44a6935b192b", + "hb_cache_path_rubicon": "{{ cache.path }}", + "hb_cache_host_rubicon": "{{ cache.host }}", + "hb_pb": "12.40", + "hb_pb_rubicon": "12.40", + "hb_cache_id_rubicon": "07a81993-a3f4-4582-89c1-44a6935b192b", + "hb_cache_path": "{{ cache.path }}", + "hb_uuid": "e15f6dcb-7b3b-4c32-be95-9b9d6f8f9320", + "hb_size": "300x250", + "hb_uuid_rubicon": "e15f6dcb-7b3b-4c32-be95-9b9d6f8f9320", + "hb_bidder": "rubicon", + "hb_bidder_rubicon": "rubicon", + "hb_cache_host": "{{ cache.host }}" + }, + "targetbiddercode": "rubicon", + "cache": { + "bids": { + "url": "{{ cache.resource_url }}07a81993-a3f4-4582-89c1-44a6935b192b", + "cacheId": "07a81993-a3f4-4582-89c1-44a6935b192b" + }, + "vastXml": { + "url": "{{ cache.resource_url }}e15f6dcb-7b3b-4c32-be95-9b9d6f8f9320", + "cacheId": "e15f6dcb-7b3b-4c32-be95-9b9d6f8f9320" + } + }, + "events": { + "win": "{{ event.url }}t=win&b=21521324&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=", + "imp": "{{ event.url }}t=imp&b=21521324&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=" + } + }, + "origbidcpm": 12.43, + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + }, + { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "exp": 120, + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_pb_rubN2": "8.40", + "hb_uuid_rubN2": "d4f001de-4cc9-4857-9fad-70feff5c0dbc", + "hb_cache_host_rubN2": "{{ cache.host }}", + "hb_bidder_rubN2": "rubN2", + "hb_cache_path_rubN2": "{{ cache.path }}", + "hb_size_rubN2": "300x250", + "hb_cache_id_rubN2": "683fe79f-6df7-4971-ac70-820e0486992d" + }, + "targetbiddercode": "rubN2", + "cache": { + "bids": { + "url": "{{ cache.resource_url }}683fe79f-6df7-4971-ac70-820e0486992d", + "cacheId": "683fe79f-6df7-4971-ac70-820e0486992d" + }, + "vastXml": { + "url": "{{ cache.resource_url }}d4f001de-4cc9-4857-9fad-70feff5c0dbc", + "cacheId": "d4f001de-4cc9-4857-9fad-70feff5c0dbc" + } + }, + "events": { + "win": "{{ event.url }}t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=", + "imp": "{{ event.url }}t=imp&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=" + } + }, + "origbidcpm": 8.43, + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + } + ], + "seat": "rubicon", + "group": 0 + }, + { + "bid": [ + { + "id": "7706636740145184841", + "impid": "impId1", + "price": 5.5, + "adm": "some-test-ad2", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "exp": 120, + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_pb_appnexus": "5.50", + "hb_uuid_appnexus": "5d7bfab8-69ea-416e-a5a1-a64db0bb218c", + "hb_cache_path_appnexus": "{{ cache.path }}", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host_appnexus": "{{ cache.host }}", + "hb_cache_id_appnexus": "9ef753e9-e7b7-4309-b46a-65df40f75909" + }, + "targetbiddercode": "appnexus", + "cache": { + "bids": { + "url": "{{ cache.resource_url }}9ef753e9-e7b7-4309-b46a-65df40f75909", + "cacheId": "9ef753e9-e7b7-4309-b46a-65df40f75909" + }, + "vastXml": { + "url": "{{ cache.resource_url }}5d7bfab8-69ea-416e-a5a1-a64db0bb218c", + "cacheId": "5d7bfab8-69ea-416e-a5a1-a64db0bb218c" + } + }, + "events": { + "win": "{{ event.url }}t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=", + "imp": "{{ event.url }}t=imp&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=" + } + }, + "origbidcpm": 5.5, + "origbidcur": "USD", + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 1, + "ranking_price": 0.0 + } + } + }, + { + "id": "222214214214", + "impid": "impId1", + "price": 2.0, + "adm": "some-test-ad1", + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "cat": [ + "IAB20-3" + ], + "exp": 120, + "ext": { + "prebid": { + "type": "video", + "cache": { + "bids": { + "url": "{{ cache.resource_url }}c2da8624-f28a-4d01-bbd7-f348478c1185", + "cacheId": "c2da8624-f28a-4d01-bbd7-f348478c1185" + }, + "vastXml": { + "url": "{{ cache.resource_url }}d71b6640-811b-480b-9f34-e6858ec80e40", + "cacheId": "d71b6640-811b-480b-9f34-e6858ec80e40" + } + }, + "events": { + "win": "{{ event.url }}t=win&b=222214214214&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=", + "imp": "{{ event.url }}t=imp&b=222214214214&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=" + } + }, + "origbidcpm": 2.0, + "origbidcur": "USD", + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 1 + } + } + } + ], + "seat": "appnexus", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "appnexus": "{{ appnexus.response_time_ms }}", + "rubicon": "{{ rubicon.response_time_ms }}", + "cache": "{{ cache.response_time_ms }}" + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-matcher-rubicon-appnexus.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-matcher-rubicon-appnexus.json new file mode 100644 index 00000000000..510caf2c1ca --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-matcher-rubicon-appnexus.json @@ -0,0 +1,10 @@ +{ + "880290288@8.43": "683fe79f-6df7-4971-ac70-820e0486992d", + "21521324@12.43": "07a81993-a3f4-4582-89c1-44a6935b192b", + "7706636740145184841@5.5": "9ef753e9-e7b7-4309-b46a-65df40f75909", + "222214214214@2": "c2da8624-f28a-4d01-bbd7-f348478c1185", + "": "e15f6dcb-7b3b-4c32-be95-9b9d6f8f9320", + "": "d4f001de-4cc9-4857-9fad-70feff5c0dbc", + "some-test-ad1": "d71b6640-811b-480b-9f34-e6858ec80e40", + "some-test-ad2": "5d7bfab8-69ea-416e-a5a1-a64db0bb218c" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-rubicon-appnexus-request.json new file mode 100644 index 00000000000..4be24dcc12b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-rubicon-appnexus-request.json @@ -0,0 +1,171 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "21521324", + "impid": "impId1", + "price": 12.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "video", + "events": { + "win": "http://localhost:8080/event?t=win&b=21521324&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=", + "imp": "http://localhost:8080/event?t=imp&b=21521324&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=" + } + }, + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + }, + "origbidcpm": 12.43 + }, + "wurl": "http://localhost:8080/event?t=win&b=21521324&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=" + }, + "aid": "tid" + }, + { + "type": "json", + "value": { + "id": "7706636740145184841", + "impid": "impId1", + "price": 5.5, + "adm": "some-test-ad2", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "ext": { + "prebid": { + "type": "video", + "events": { + "win": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=", + "imp": "http://localhost:8080/event?t=imp&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=" + } + }, + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 1, + "ranking_price": 0.0 + }, + "origbidcpm": 5.5, + "origbidcur": "USD" + }, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=" + }, + "aid": "tid" + }, + { + "type": "json", + "value": { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "video", + "events": { + "win": "http://localhost:8080/event?t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=", + "imp": "http://localhost:8080/event?t=imp&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=" + } + }, + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + }, + "origbidcpm": 8.43 + }, + "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=i&int=" + }, + "aid": "tid" + }, + { + "type": "json", + "value": { + "id": "222214214214", + "impid": "impId1", + "price": 2, + "adm": "some-test-ad1", + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "cat": [ + "IAB20-3" + ], + "ext": { + "prebid": { + "type": "video", + "events": { + "win": "http://localhost:8080/event?t=win&b=222214214214&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=", + "imp": "http://localhost:8080/event?t=imp&b=222214214214&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=" + } + }, + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 1 + }, + "origbidcpm": 2, + "origbidcur": "USD" + }, + "wurl": "http://localhost:8080/event?t=win&b=222214214214&a=5001&aid=tid&ts=1000&bidder=appnexus&f=i&int=" + }, + "aid": "tid" + }, + { + "type": "xml", + "value": "some-test-ad1", + "ttlseconds": 120, + "aid": "tid" + }, + { + "type": "xml", + "value": "", + "ttlseconds": 120, + "aid": "tid" + }, + { + "type": "xml", + "value": "some-test-ad2", + "ttlseconds": 120, + "aid": "tid" + }, + { + "type": "xml", + "value": "", + "ttlseconds": 120, + "aid": "tid" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-request-1.json new file mode 100644 index 00000000000..db331af2373 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-request-1.json @@ -0,0 +1,112 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "video": { + "mimes": [ + "mimes" + ], + "minduration": 20, + "maxduration": 60, + "protocols": [ + 1 + ], + "w": 300, + "h": 250, + "startdelay": 5, + "skipmin": 0, + "skipafter": 0, + "playbackmethod": [ + 1 + ], + "ext": { + "skip": 5, + "skipdelay": 1, + "rp": { + "size_id": 15 + }, + "videotype": "rewarded" + } + }, + "ext": { + "rp": { + "zone_id": 4001, + "target": { + "rating": [ + "5-star" + ], + "prodtype": [ + "tech" + ], + "page": [ + "http://www.example.com" + ] + }, + "track": { + "mint": "", + "mint_version": "" + } + }, + "maxbids": 2 + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "ext": { + "rp": { + "account_id": 2001 + } + } + }, + "ext": { + "amp": 0, + "rp": { + "site_id": 3001 + } + } + }, + "device": { + "ua": "userAgent", + "dnt": 2, + "ip": "80.215.195.0", + "pxratio": 4.2, + "language": "en", + "ifa": "ifaId", + "ext": { + "rp": { + "pixelratio": 4.2 + } + } + }, + "user": { + "buyeruid": "J5VLCWQP-26-CWFT", + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA", + "rp": { + "target": { + "ucat": [ + "new" + ], + "search": [ + "iphone" + ] + } + } + } + }, + "at": 1, + "tmax": 5000, + "source": { + "fd": 1, + "tid": "tid" + }, + "regs": { + "ext": { + "us_privacy": "1YNN" + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-response-1.json new file mode 100644 index 00000000000..497f717ea49 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-response-1.json @@ -0,0 +1,74 @@ +{ + "id": "bidResponseId1", + "seatbid": [ + { + "bid": [ + { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + }, + { + "id": "21521324", + "impid": "impId1", + "price": 12.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + }, + { + "id": "992342532", + "impid": "impId1", + "price": 2.00, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + } + ], + "seat": "seatId1", + "group": 0 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-auction-rubicon-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-auction-rubicon-request.json new file mode 100644 index 00000000000..99fa981f1f5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-auction-rubicon-request.json @@ -0,0 +1,57 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId001", + "video": { + "mimes": [ + "mimes" + ], + "w": 300, + "h": 250 + }, + "ext": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + } + } + ], + "device": { + "ua": "testUa", + "ip": "193.168.244.1" + }, + "ext": { + "prebid": { + "bidadjustmentfactors": { + "mediatypes": { + "video": { + "rubicon": 0.9 + } + } + }, + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + } + }, + "cache": { + "bids": {} + }, + "auctiontimestamp": 0 + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-auction-rubicon-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-auction-rubicon-response.json new file mode 100644 index 00000000000..97fd4676974 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-auction-rubicon-response.json @@ -0,0 +1,59 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bid001", + "impid": "impId001", + "price": 2.997, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_pb": "2.90", + "hb_size_rubicon": "300x250", + "hb_bidder_rubicon": "rubicon", + "hb_cache_path": "{{ cache.path }}", + "hb_size": "300x250", + "hb_cache_host_rubicon": "{{ cache.host }}", + "hb_cache_path_rubicon": "{{ cache.path }}", + "hb_cache_id_rubicon": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", + "hb_bidder": "rubicon", + "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", + "hb_pb_rubicon": "2.90", + "hb_cache_host": "{{ cache.host }}" + }, + "cache": { + "bids": { + "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb", + "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" + } + } + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "rubicon": "{{ rubicon.response_time_ms }}", + "cache": "{{ cache.response_time_ms }}" + }, + "tmaxrequest": 2000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-cache-rubicon-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-cache-rubicon-request.json new file mode 100644 index 00000000000..396c885a724 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-cache-rubicon-request.json @@ -0,0 +1,25 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "bid001", + "impid": "impId001", + "price": 2.997, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 3.33 + } + }, + "aid": "tid" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-cache-adgeneration-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-cache-rubicon-response.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-cache-adgeneration-response.json rename to src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-cache-rubicon-response.json diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-rubicon-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-rubicon-bid-request.json new file mode 100644 index 00000000000..95c16146e0e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-rubicon-bid-request.json @@ -0,0 +1,65 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId001", + "video": { + "mimes": [ + "mimes" + ], + "w": 300, + "h": 250 + }, + "ext": { + "rp": { + "zone_id": 4001, + "target": { + "page": [ + "http://www.example.com" + ] + }, + "track": { + "mint": "", + "mint_version": "" + } + }, + "maxbids": 1 + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "ext": { + "rp": { + "account_id": 2001 + } + } + }, + "ext": { + "amp": 0, + "rp": { + "site_id": 3001 + } + } + }, + "device": { + "ua": "testUa", + "ip": "193.168.244.1", + "ext": { + "rp": { + } + } + }, + "user": { + "buyeruid": "RUB-UID" + }, + "at": 1, + "tmax": 2000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-rubicon-bid-response.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response-1.json rename to src/test/resources/org/prebid/server/it/openrtb2/rubicon_core_functionality/test-rubicon-bid-response.json diff --git a/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-auction-salunamedia-request.json b/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-auction-salunamedia-request.json new file mode 100644 index 00000000000..019b570ea3e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-auction-salunamedia-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "sa_lunamedia": { + "key": "someKey" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-auction-salunamedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-auction-salunamedia-response.json new file mode 100644 index 00000000000..d3bd2828fc5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-auction-salunamedia-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "mediaType": "banner", + "origbidcpm": 3.33 + } + } + ], + "seat": "sa_lunamedia", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "sa_lunamedia": "{{ sa_lunamedia.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-salunamedia-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-salunamedia-bid-request.json new file mode 100644 index 00000000000..3ca2278b8b8 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-salunamedia-bid-request.json @@ -0,0 +1,41 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "key": "someKey" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-salunamedia-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-salunamedia-bid-response.json new file mode 100644 index 00000000000..c8f5fab1829 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-salunamedia-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300, + "ext": { + "mediaType": "banner" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json index f85ecebe767..f9288ee5846 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-request.json @@ -1,110 +1,33 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "bid", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { - "sharethrough": { - "pkey": "abc123", - "iframe": true, - "iframeSize": [ - 50, - 50 - ], - "bidfloor": 3.5 + "prebid": { + "bidder": { + "sharethrough": { + "pkey": "abc123", + "iframe": true, + "iframeSize": [ + 50, + 50 + ], + "bidfloor": 3.5 + } + } } } } ], - "badv": [ - "testblocked" - ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId", - "ua": "Android Chrome/60", - "ip": "127.0.0.1" - }, - "site": { - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "test": 1, - "at": 1, - "tmax": 3000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "eids": [ - { - "source": "adserver.org", - "uids": [ - { - "id": "id" - } - ] - } - ], - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, + "tmax": 5000, "regs": { "ext": { - "gdpr": 0, - "us_privacy": "1NYN" + "gdpr": 0 } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json index 0237cfc3df6..75fb6227604 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json @@ -1,13 +1,13 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid", - "impid": "bid", + "id": "bid_id", + "impid": "imp_id", "price": 10, - "adm": "\n\t\t
    \n\t\t\n\t\t\t\n", + "adm": "", "adid": "arid", "cid": "cmpKey", "crid": "creaKey", @@ -15,28 +15,10 @@ "h": 50, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_bidder_sharethrough": "sharethrough", - "hb_cache_id": "a03927f1-86e1-48fb-9efc-da9104a44e39", - "hb_cache_id_sharethrough": "a03927f1-86e1-48fb-9efc-da9104a44e39", - "hb_cache_path_sharethrough": "{{ cache.path }}", - "hb_pb": "10.00", - "hb_size_sharethrough": "50x50", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "50x50", - "hb_bidder": "sharethrough", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_sharethrough": "{{ cache.host }}", - "hb_pb_sharethrough": "10.00" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}a03927f1-86e1-48fb-9efc-da9104a44e39", - "cacheId": "a03927f1-86e1-48fb-9efc-da9104a44e39" - } - } - } + "type": "banner" + }, + "origbidcpm": 10, + "origbidcur": "USD" } } ], @@ -46,149 +28,12 @@ ], "cur": "USD", "ext": { - "debug": { - "httpcalls": { - "sharethrough": [ - { - "uri": "{{ sharethrough.exchange_uri }}?placement_key=abc123&bidId=bid&consent_required=false&consent_string=consentValue&us_privacy=1NYN&instant_play_capable=true&stayInIframe=true&height=50&width=50&adRequestAt={{ TEST_FORMATTED_TIME }}&supplyId=FGMrCMMc&strVersion=8&ttduid=id&stxuid=STR-UID", - "requestbody": "{\"badv\":[\"testblocked\"],\"tmax\":3000,\"deadline\":\"{{ DEADLINE_FORMATTED_TIME }}\",\"bidfloor\":3.5}", - "responsebody": "{\"adserverRequestId\":\"arid\",\"bidId\":\"bid\",\"creatives\":[{\"cpm\":10,\"creative\":{\"campaign_key\":\"cmpKey\",\"creative_key\":\"creaKey\"},\"version\":0}]}", - "status": 200 - } - ], - "cache": [ - { - "uri": "{{ cache.endpoint }}", - "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"bid\",\"impid\":\"bid\",\"price\":10,\"adm\":\"\\n\\t\\t
    \\n\\t\\t\\n\\t\\t\\t\\n\",\"adid\":\"arid\",\"cid\":\"cmpKey\",\"crid\":\"creaKey\",\"w\":50,\"h\":50}}]}", - "responsebody": "{\"responses\":[{\"uuid\":\"a03927f1-86e1-48fb-9efc-da9104a44e39\"}]}", - "status": 200 - } - ] - }, - "resolvedrequest": { - "id": "tid", - "imp": [ - { - "id": "bid", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "sharethrough": { - "pkey": "abc123", - "iframe": true, - "iframeSize": [ - 50, - 50 - ], - "bidfloor": 3.5 - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "Android Chrome/60", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "ext": { - "eids": [ - { - "source": "adserver.org", - "uids": [ - { - "id": "id" - } - ] - } - ], - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "test": 1, - "at": 1, - "tmax": 3000, - "cur": [ - "USD" - ], - "badv": [ - "testblocked" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0, - "us_privacy": "1NYN" - } - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } - } - }, "responsetimemillis": { - "sharethrough": "{{ sharethrough.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "sharethrough": 0 }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, - "tmaxrequest": 3000 + "tmaxrequest": 5000 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-cache-sharethrough-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-cache-sharethrough-request.json deleted file mode 100644 index 73dc1156b71..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-cache-sharethrough-request.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid", - "impid": "bid", - "price": 10, - "adid": "arid", - "adm": "\n\t\t
    \n\t\t\n\t\t\t\n", - "cid": "cmpKey", - "crid": "creaKey", - "w": 50, - "h": 50 - } - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-cache-sharethrough-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-cache-sharethrough-response.json deleted file mode 100644 index 069ae678069..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-cache-sharethrough-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "a03927f1-86e1-48fb-9efc-da9104a44e39" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-response-1.json deleted file mode 100644 index 14e15e52548..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-response-1.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "adserverRequestId": "arid", - "bidId": "bid", - "creatives": [ - { - "cpm": 10, - "creative": { - "campaign_key": "cmpKey", - "creative_key": "creaKey" - }, - "version": 0 - } - ] -} - diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-response.json new file mode 100644 index 00000000000..f564c17f06c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-response.json @@ -0,0 +1,15 @@ +{ + "adserverRequestId": "arid", + "bidId": "bid_id", + "creatives": [ + { + "cpm": 10, + "creative": { + "campaign_key": "cmpKey", + "creative_key": "creaKey" + }, + "version": 0 + } + ] +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-request.json index be1de2d96e2..b51c3667078 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-request.json @@ -1,8 +1,5 @@ { - "badv": [ - "testblocked" - ], - "tmax": 3000, - "deadline": "{{ DEADLINE_FORMATTED_TIME }}", + "tmax" : 5000, + "deadline": "${json-unit.any-string}", "bidfloor": 3.5 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-request.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-request.json new file mode 100644 index 00000000000..933b6c2c003 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "silvermob": { + "zoneid": "testZoneId", + "host": "testHostValue" + } + }, + "tagid": "tag_id" + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json new file mode 100644 index 00000000000..a3dca51a91e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json @@ -0,0 +1,35 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 0.01, + "adid": "adid", + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "silvermob", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "silvermob": "{{ silvermob.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-request.json new file mode 100644 index 00000000000..d7de6ff4d89 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-request.json @@ -0,0 +1,43 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "tagid": "tag_id", + "ext": { + "bidder": { + "zoneid": "testZoneId", + "host": "testHostValue" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-response.json new file mode 100644 index 00000000000..96ad624d30d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-silvermob-bid-response.json @@ -0,0 +1,18 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "crid": "crid", + "adid": "adid", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "cid" + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-request.json index b3d71ceb604..a6264d12e6e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-request.json @@ -1,16 +1,12 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", - "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", + "id": "imp_id", + "tagid": "tag_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "smaato": { @@ -20,78 +16,10 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "ip": "193.168.244.1", - "language": "en", - "ifa": "ifaId", - "ua": "userAgent" - }, - "site": { - "publisher": { - "id": "1100042525" - }, - "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", - "ext": { - "data": { - "keywords": "power tools", - "search": "drill", - "content": { - "userrating": 4 - } - } - } - }, - "at": 1, - "tmax": 3000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "user": { - "ext": { - "consent": "gdprConsentString", - "data": { - "keywords": "a,b", - "gender": "M", - "yob": 1984, - "geo": { - "country": "ca" - } - } - } - }, + "tmax": 5000, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "includebidderkeys": true, - "includewinners": true, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-response.json index e8bf61f24ac..886a9d8a04d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-response.json @@ -1,41 +1,21 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { "adm": "
    \"\"\"\"
    ", - "cid": "test_cid", - "crid": "test_banner_crid", + "cid": "cid", + "crid": "crid", + "id": "bid_id", + "impid": "imp_id", + "price": 0.01, "ext": { - "bidder": { - "format": "BANNER" - }, "prebid": { - "type": "banner", - "targeting": { - "hb_bidder": "smaato", - "hb_bidder_smaato": "smaato", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_smaato": "{{ cache.host }}", - "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_id_smaato": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_smaato": "{{ cache.path }}", - "hb_pb": "0.00", - "hb_pb_smaato": "0.00" - }, - "cache": { - "bids": { - "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - } - } - }, - "id": "1", - "impid": "impId001", - "price": 0.01 + "type": "banner" + }, + "origbidcpm": 0.01 + } } ], "group": 0, @@ -45,12 +25,11 @@ "cur": "USD", "ext": { "responsetimemillis": { - "cache": "{{ cache.response_time_ms }}", "smaato": "{{ smaato.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, - "tmaxrequest": 3000 + "tmaxrequest": 5000 } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-cache-smaato-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-cache-smaato-request.json deleted file mode 100644 index a8fced07cb3..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-cache-smaato-request.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "1", - "impid": "impId001", - "price": 0.01, - "adm": "
    \"\"\"\"
    ", - "cid": "test_cid", - "crid": "test_banner_crid", - "ext": { - "format": "BANNER" - } - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-cache-smaato-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-cache-smaato-response.json deleted file mode 100644 index 93d0b8de2cd..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-cache-smaato-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json index 994b0c9a1aa..e69898c274e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json @@ -1,61 +1,35 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { "w": 300, - "h": 250, - "format": [ - { - "w": 300, - "h": 250 - } - ] + "h": 250 }, "tagid": "130563103" } ], "site": { - "domain": "example.com", - "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", "publisher": { "id": "11000" - }, - "keywords": "power tools" + } }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "SM-UID", - "yob": 1984, - "gender": "M", - "keywords": "a,b", - "ext": { - "consent": "gdprConsentString" - } + "ip": "193.168.244.1" }, "at": 1, - "tmax": 3000, + "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } }, "ext": { - "client": "prebid_server_0.1" + "client": "prebid_server_0.4" } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-response.json index f54dd0840c2..cb5b6b21d08 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-response.json @@ -1,14 +1,14 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { "adm": "{\"image\":{\"img\":{\"url\":\"//prebid-test.smaatolabs.net/img/320x50.jpg\",\"w\":350,\"h\":50,\"ctaurl\":\"//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}", - "crid": "test_banner_crid", - "cid": "test_cid", - "impid": "impId001", - "id": "1", + "crid": "crid", + "cid": "cid", + "impid": "imp_id", + "id": "bid_id", "price": 0.01, "ext": { "format": "BANNER" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json index 7de3994683c..bd93b91baf0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-request.json @@ -1,85 +1,27 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "test-imp-banner-id", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 500, - "h": 400 + "w": 300, + "h": 250 }, "ext": { - "smartadserver": { - "siteId": 1, - "pageId": 2, - "formatId": 3, - "networkId": 73 + "prebid": { + "bidder": { + "smartadserver": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 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 00a73c254e1..fde0fbaa115 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 @@ -1,45 +1,26 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "7706636740145184841", - "impid": "test-imp-banner-id", + "id": "bid_id", + "impid": "imp_id", "price": 0.5, "adm": "some-test-ad", - "adid": "29681110", + "adid": "adid", "adomain": [ "advertsite.com" ], - "cid": "772", - "crid": "29681110", + "cid": "cid", + "crid": "crid", "w": 1024, "h": 576, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "0.50", - "hb_bidder_smartadserver": "smartadserver", - "hb_cache_path_smartadserver": "{{ cache.path }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_host_smartadserver": "{{ cache.host }}", - "hb_bidder": "smartadserver", - "hb_cache_id": "78f9a6dd-d08c-4b80-ba0f-0159b9add9bf", - "hb_cache_host": "{{ cache.host }}", - "hb_pb_smartadserver": "0.50", - "hb_cache_id_smartadserver": "78f9a6dd-d08c-4b80-ba0f-0159b9add9bf", - "hb_size": "1024x576", - "hb_size_smartadserver": "1024x576" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}78f9a6dd-d08c-4b80-ba0f-0159b9add9bf", - "cacheId": "78f9a6dd-d08c-4b80-ba0f-0159b9add9bf" - } - } - } + "type": "banner" + }, + "origbidcpm": 0.5 } } ], @@ -49,132 +30,11 @@ ], "cur": "USD", "ext": { - "debug": { - "httpcalls": { - "cache": [ - { - "uri": "{{ cache.endpoint }}", - "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-banner-id\",\"price\":0.5,\"adm\":\"some-test-ad\",\"adid\":\"29681110\",\"adomain\":[\"advertsite.com\"],\"cid\":\"772\",\"crid\":\"29681110\",\"w\":1024,\"h\":576}}]}", - "responsebody": "{\"responses\":[{\"uuid\":\"78f9a6dd-d08c-4b80-ba0f-0159b9add9bf\"}]}", - "status": 200 - } - ], - "smartadserver": [ - { - "uri": "{{ smartadserver.exchange_uri }}?callerId=5", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-banner-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}],\"w\":500,\"h\":400},\"ext\":{\"bidder\":{\"siteId\":1,\"pageId\":2,\"formatId\":3,\"networkId\":73}}}],\"site\":{\"publisher\":{\"id\":\"73\"}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"SA-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", - "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-banner-id\",\"price\":0.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"advertsite.com\"],\"cid\":\"772\",\"crid\":\"29681110\",\"h\":576,\"w\":1024}]}]}", - "status": 200 - } - ] - }, - "resolvedrequest": { - "id": "tid", - "imp": [ - { - "id": "test-imp-banner-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 500, - "h": 400 - }, - "ext": { - "smartadserver": { - "siteId": 1, - "pageId": 2, - "formatId": 3, - "networkId": 73 - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } - } - }, "responsetimemillis": { - "cache": "{{ cache.response_time_ms }}", "smartadserver": "{{ smartadserver.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-cache-smartadserver-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-cache-smartadserver-request.json deleted file mode 100644 index a97a73381ff..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-cache-smartadserver-request.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "7706636740145184841", - "impid": "test-imp-banner-id", - "price": 0.500000, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": ["advertsite.com"], - "cid": "772", - "crid": "29681110", - "h": 576, - "w": 1024 - } - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-cache-smartadserver-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-cache-smartadserver-response.json deleted file mode 100644 index 0b2fbde6c47..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-cache-smartadserver-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "78f9a6dd-d08c-4b80-ba0f-0159b9add9bf" - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request-1.json deleted file mode 100644 index 115e58fa4b0..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request-1.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "test-imp-banner-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 500, - "h": 400 - }, - "ext": { - "bidder": { - "siteId": 1, - "pageId": 2, - "formatId": 3, - "networkId": 73 - } - } - } - ], - "site": { - "publisher": { - "id": "73" - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid" : "SA-UID", - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request.json new file mode 100644 index 00000000000..3747042c8ea --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-request.json @@ -0,0 +1,45 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-response-1.json deleted file mode 100644 index 92517b36952..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-response-1.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "7706636740145184841", - "impid": "test-imp-banner-id", - "price": 0.500000, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": ["advertsite.com"], - "cid": "772", - "crid": "29681110", - "h": 576, - "w": 1024 - } - ] - } - ] -} 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 new file mode 100644 index 00000000000..cee26a03931 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-smartadserver-bid-response.json @@ -0,0 +1,23 @@ +{ + "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 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-request.json index f6caca91bf3..03cfada6597 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-request.json @@ -1,16 +1,12 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", - "tagid": "N4zTDq3PPEHBIODv7cXK", + "id": "imp_id", + "tagid": "tag_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "smartrtb": { @@ -19,93 +15,9 @@ "force_bid": true } } - }, - { - "id": "impId002", - "tagid": "N4zTDq3PPEHBIODv7cXK", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "skipafter": 0, - "skipmin": 0, - "w": 1024, - "h": 576 - }, - "ext": { - "smartrtb": { - "pub_id": "12345", - "zone_id": "N4zTDq3PPEHBIODv7cXK", - "force_bid": true - } - } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "ip": "193.168.244.1", - "language": "en", - "ifa": "ifaId", - "ua": "userAgent" - }, - "site": { - "domain": "example.com", - "ext": { - "amp": 0 - }, - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "includebidderkeys": true, - "includewinners": true, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-response.json index e8dcb65eb06..fbe5d5a69a2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-response.json @@ -1,38 +1,21 @@ { "cur": "USD", - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { "adm": "hi", - "cid": "test_cid", - "crid": "test_banner_crid", + "cid": "cid", + "crid": "crid", "ext": { "prebid": { - "cache": { - "bids": { - "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "url": "http://localhost:8090/cache?uuid=f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - }, - "targeting": { - "hb_bidder": "smartrtb", - "hb_bidder_smartrtb": "smartrtb", - "hb_cache_host": "localhost:8090", - "hb_cache_host_smartrtb": "localhost:8090", - "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_id_smartrtb": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_path": "/cache", - "hb_cache_path_smartrtb": "/cache", - "hb_pb": "0.00", - "hb_pb_smartrtb": "0.00" - }, "type": "banner" - } + }, + "origbidcpm": 0.01 }, - "id": "1", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 0.01 } ], @@ -42,11 +25,10 @@ ], "ext": { "responsetimemillis": { - "cache": "{{ cache.response_time_ms }}", "smartrtb": "{{ smartrtb.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-cache-smartrtb-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-cache-smartrtb-request.json deleted file mode 100644 index 00cfdec611c..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-cache-smartrtb-request.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "adm": "hi", - "cid": "test_cid", - "crid": "test_banner_crid", - "id": "1", - "impid": "impId001", - "price": 0.01 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-cache-smartrtb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-cache-smartrtb-response.json deleted file mode 100644 index 93d0b8de2cd..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-cache-smartrtb-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-request.json index 841e7745379..c7f2f287217 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-request.json @@ -4,51 +4,15 @@ "USD" ], "device": { - "dnt": 2, - "ifa": "ifaId", "ip": "193.168.244.1", - "language": "en", - "pxratio": 4.2, "ua": "userAgent" }, - "ext": { - "prebid": { - "cache": { - "bids": { - }, - "vastxml": { - "ttlseconds": 120 - } - }, - "targeting": { - "includebidderkeys": true, - "includewinners": true, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "increment": 0.1, - "max": 20 - } - ] - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - }, - "id": "tid", + "id": "request_id", "imp": [ { "banner": { - "format": [ - { - "h": 250, - "w": 300 - } - ] + "h": 250, + "w": 300 }, "ext": { "bidder": { @@ -57,32 +21,8 @@ "zone_id": "N4zTDq3PPEHBIODv7cXK" } }, - "id": "impId001", + "id": "imp_id", "tagid": "N4zTDq3PPEHBIODv7cXK" - }, - { - "ext": { - "bidder": { - "force_bid": true, - "pub_id": "12345", - "zone_id": "N4zTDq3PPEHBIODv7cXK" - } - }, - "id": "impId002", - "tagid": "N4zTDq3PPEHBIODv7cXK", - "video": { - "h": 576, - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "skipafter": 0, - "skipmin": 0, - "w": 1024 - } } ], "regs": { @@ -91,28 +31,14 @@ } }, "site": { - "domain": "example.com", + "domain": "www.example.com", "ext": { "amp": 0 }, "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" } }, - "source": { - "fd": 1, - "tid": "tid" - }, - "tmax": 5000, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - } + "tmax": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-response.json index 455a146256a..2e1b7e91e69 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-smartrtb-bid-response.json @@ -1,14 +1,14 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { "adm": "hi", - "crid": "test_banner_crid", - "cid": "test_cid", - "impid": "impId001", - "id": "1", + "crid": "crid", + "cid": "cid", + "impid": "imp_id", + "id": "bid_id", "price": 0.01, "ext": { "format": "BANNER" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-auction-smartyads-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-auction-smartyads-request.json new file mode 100644 index 00000000000..e42e5d40df9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-auction-smartyads-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "smartyads": { + "accountid": "testAccountid", + "sourceid": "testSourceid", + "host": "testHost" + } + }, + "tagid": "tag_id" + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-auction-smartyads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-auction-smartyads-response.json new file mode 100644 index 00000000000..5d5a737a256 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-auction-smartyads-response.json @@ -0,0 +1,35 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 0.01, + "adid": "adid", + "cid": "cid", + "crid": "crid", + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "smartyads", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smartyads": "{{ smartyads.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-smartyads-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-smartyads-bid-request.json new file mode 100644 index 00000000000..2accbe26272 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-smartyads-bid-request.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "tagid": "tag_id" + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-smartyads-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-smartyads-bid-response.json new file mode 100644 index 00000000000..96ad624d30d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-smartyads-bid-response.json @@ -0,0 +1,18 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "crid": "crid", + "adid": "adid", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "cid" + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-auction-smilewanted-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-auction-smilewanted-request.json new file mode 100644 index 00000000000..0049fb36725 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-auction-smilewanted-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "smilewanted": { + "zoneId": "someZoneId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-auction-smilewanted-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-auction-smilewanted-response.json new file mode 100644 index 00000000000..b882d1c8f28 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-auction-smilewanted-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid", + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "smilewanted", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smilewanted": "{{ smilewanted.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-smilewanted-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-smilewanted-bid-request.json new file mode 100644 index 00000000000..1a662427c32 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-smilewanted-bid-request.json @@ -0,0 +1,41 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "zoneId": "someZoneId" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-smilewanted-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-smilewanted-bid-response.json new file mode 100644 index 00000000000..1698b5a2b57 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-smilewanted-bid-response.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid", + "crid": "crid", + "cid": "cid", + "adm": "adm001", + "h": 250, + "w": 300, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-request.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-request.json index a7cdb7df279..107aba6589a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId16", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "somoaudience": { @@ -17,113 +13,9 @@ "bid_floor": 1.5 } } - }, - { - "id": "impId17", - "banner": { - "format": [ - { - "w": 360, - "h": 240 - } - ] - }, - "ext": { - "somoaudience": { - "placement_hash": "placementId02", - "bid_floor": 1.33 - } - } - }, - { - "id": "impId18", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "w": 1024, - "h": 576 - }, - "ext": { - "somoaudience": { - "placement_hash": "placementId03", - "bid_floor": 2.1 - } - } - }, - { - "id": "impId19", - "native": { - "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"required\":1,\"title\":{\"len\":500}}]}", - "ver": "1.1" - }, - "ext": { - "somoaudience": { - "placement_hash": "placementId04", - "bid_floor": 1.9 - } - } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-response.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-response.json index 577cf9ef6ab..80fd006b653 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-auction-somoaudience-response.json @@ -1,11 +1,11 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bidId01", - "impid": "impId16", + "id": "bid_id", + "impid": "imp_id", "price": 8.43, "adm": "adm16", "crid": "crid16", @@ -13,141 +13,9 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_cache_host_somoaudience": "{{ cache.host }}", - "hb_size_somoaudience": "300x250", - "hb_cache_id": "6da10965-53bf-453e-a890-ea94eeb00765", - "hb_pb_somoaudience": "8.40", - "hb_pb": "8.40", - "hb_bidder_somoaudience": "somoaudience", - "hb_cache_id_somoaudience": "6da10965-53bf-453e-a890-ea94eeb00765", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_bidder": "somoaudience", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_somoaudience": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}6da10965-53bf-453e-a890-ea94eeb00765", - "cacheId": "6da10965-53bf-453e-a890-ea94eeb00765" - } - } - } - } - }, - { - "id": "bidId02", - "impid": "impId17", - "price": 7.99, - "adm": "adm17", - "crid": "crid17", - "w": 360, - "h": 240, - "ext": { - "prebid": { - "type": "banner", - "targeting": { - "hb_cache_host_somoaudience": "{{ cache.host }}", - "hb_size_somoaudience": "360x240", - "hb_cache_id": "9ce56613-4f34-4b28-8a1e-69683ad26b99", - "hb_pb_somoaudience": "7.90", - "hb_pb": "7.90", - "hb_bidder_somoaudience": "somoaudience", - "hb_cache_id_somoaudience": "9ce56613-4f34-4b28-8a1e-69683ad26b99", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "360x240", - "hb_bidder": "somoaudience", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_somoaudience": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}9ce56613-4f34-4b28-8a1e-69683ad26b99", - "cacheId": "9ce56613-4f34-4b28-8a1e-69683ad26b99" - } - } - } - } - }, - { - "id": "bidId03", - "impid": "impId18", - "price": 9.99, - "adm": "adm18", - "crid": "crid18", - "w": 1024, - "h": 576, - "ext": { - "prebid": { - "type": "video", - "targeting": { - "hb_cache_host_somoaudience": "{{ cache.host }}", - "hb_size_somoaudience": "1024x576", - "hb_cache_id": "0bd56608-42fe-47f6-b2ec-bca5f3905dc5", - "hb_pb_somoaudience": "9.90", - "hb_pb": "9.90", - "hb_bidder_somoaudience": "somoaudience", - "hb_cache_id_somoaudience": "0bd56608-42fe-47f6-b2ec-bca5f3905dc5", - "hb_cache_path": "{{ cache.path }}", - "hb_uuid": "8aa68c39-680d-46f5-ba83-fcd761d436fe", - "hb_size": "1024x576", - "hb_bidder": "somoaudience", - "hb_uuid_somoaudience": "8aa68c39-680d-46f5-ba83-fcd761d436fe", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_somoaudience": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}0bd56608-42fe-47f6-b2ec-bca5f3905dc5", - "cacheId": "0bd56608-42fe-47f6-b2ec-bca5f3905dc5" - }, - "vastXml": { - "url": "{{ cache.resource_url }}8aa68c39-680d-46f5-ba83-fcd761d436fe", - "cacheId": "8aa68c39-680d-46f5-ba83-fcd761d436fe" - } - } - } - } - }, - { - "id": "bidId04", - "impid": "impId19", - "price": 10.0, - "adm": "{\"assets\":[{\"id\": 0,\"title\":{\"text\":\"This is an example Prebid Native creative\"}}],\"jstracker\":\"\"}", - "adid": "adid19", - "adomain": [ - "appnexus.com" - ], - "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", - "cid": "cid19", - "crid": "crid19", - "cat": [ - "IAB3-1" - ], - "ext": { - "prebid": { - "type": "native", - "targeting": { - "hb_cache_host_somoaudience": "{{ cache.host }}", - "hb_cache_id": "2bf317a1-d300-42df-97d5-8ae4a3fef9e3", - "hb_pb_somoaudience": "10.00", - "hb_pb": "10.00", - "hb_bidder_somoaudience": "somoaudience", - "hb_cache_id_somoaudience": "2bf317a1-d300-42df-97d5-8ae4a3fef9e3", - "hb_cache_path": "{{ cache.path }}", - "hb_bidder": "somoaudience", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_somoaudience": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}2bf317a1-d300-42df-97d5-8ae4a3fef9e3", - "cacheId": "2bf317a1-d300-42df-97d5-8ae4a3fef9e3" - } - } - } + "type": "banner" + }, + "origbidcpm": 8.43 } } ], @@ -158,11 +26,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "somoaudience": "{{ somoaudience.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "somoaudience": "{{ somoaudience.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-cache-matcher-somoaudience.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-cache-matcher-somoaudience.json deleted file mode 100644 index 87f651fbebb..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-cache-matcher-somoaudience.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "bidId01@8.43" : "6da10965-53bf-453e-a890-ea94eeb00765", - "bidId04@10" : "2bf317a1-d300-42df-97d5-8ae4a3fef9e3", - "bidId03@9.99" : "0bd56608-42fe-47f6-b2ec-bca5f3905dc5", - "bidId02@7.99" : "9ce56613-4f34-4b28-8a1e-69683ad26b99", - "adm18" : "8aa68c39-680d-46f5-ba83-fcd761d436fe" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-cache-somoaudience-request.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-cache-somoaudience-request.json deleted file mode 100644 index f7f2bbb35a8..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-cache-somoaudience-request.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bidId01", - "impid": "impId16", - "price": 8.43, - "adm": "adm16", - "crid": "crid16", - "w": 300, - "h": 250 - } - }, - { - "type": "json", - "value": { - "id": "bidId02", - "impid": "impId17", - "price": 7.99, - "adm": "adm17", - "crid": "crid17", - "w": 360, - "h": 240 - } - }, - { - "type": "json", - "value": { - "id": "bidId03", - "impid": "impId18", - "price": 9.99, - "adm": "adm18", - "crid": "crid18", - "w": 1024, - "h": 576 - } - }, - { - "type": "json", - "value": { - "id": "bidId04", - "impid": "impId19", - "price": 10, - "adm": "{\"assets\":[{\"id\": 0,\"title\":{\"text\":\"This is an example Prebid Native creative\"}}],\"jstracker\":\"\"}", - "adid": "adid19", - "adomain": [ - "appnexus.com" - ], - "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", - "cid": "cid19", - "crid": "crid19", - "cat": [ - "IAB3-1" - ] - } - }, - { - "type": "xml", - "value": "adm18", - "expiry": 120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-1.json deleted file mode 100644 index 077c83ddce5..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-1.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId16", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "bidfloor": 1.5 - }, - { - "id": "impId17", - "banner": { - "format": [ - { - "w": 360, - "h": 240 - } - ] - }, - "bidfloor": 1.33 - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "SM-UID", - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": "hb_pbs_1.0.0" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-2.json deleted file mode 100644 index d23c70e4ebf..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-2.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId18", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "w": 1024, - "h": 576 - }, - "bidfloor": 2.1 - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "SM-UID", - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": "hb_pbs_1.0.0" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-3.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-3.json deleted file mode 100644 index bca7fa68205..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request-3.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId19", - "native": { - "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":500}}]}", - "ver": "1.1" - }, - "bidfloor": 1.9 - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "SM-UID", - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": "hb_pbs_1.0.0" - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request.json new file mode 100644 index 00000000000..621f6a97f67 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-request.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "bidfloor": 1.5 + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": "hb_pbs_1.0.0" + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response-1.json deleted file mode 100644 index e9d6a552bda..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response-1.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bidId01", - "impid": "impId16", - "price": 8.43, - "adm": "adm16", - "crid": "crid16", - "w": 300, - "h": 250 - }, - { - "id": "bidId02", - "impid": "impId17", - "price": 7.99, - "adm": "adm17", - "crid": "crid17", - "w": 360, - "h": 240 - } - ], - "seat": "seatId01" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response-2.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response-2.json deleted file mode 100644 index e3ba5ef4f26..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response-2.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bidId03", - "impid": "impId18", - "price": 9.99, - "adm": "adm18", - "crid": "crid18", - "w": 1024, - "h": 576 - } - ], - "seat": "seatId02" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response-3.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response-3.json deleted file mode 100644 index 27ab45f5c56..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response-3.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bidId04", - "impid": "impId19", - "price": 10.000000, - "adid": "adid19", - "adm": "{\"assets\":[{\"id\": 0,\"title\":{\"text\":\"This is an example Prebid Native creative\"}}],\"jstracker\":\"\"}", - "adomain": [ - "appnexus.com" - ], - "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", - "cid": "cid19", - "crid": "crid19", - "cat": [ - "IAB3-1" - ] - } - ], - "seat": "seatId03" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response.json new file mode 100644 index 00000000000..88aed6b5934 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/somoaudience/test-somoaudience-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 8.43, + "adm": "adm16", + "crid": "crid16", + "w": 300, + "h": 250 + } + ], + "seat": "seatId01" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-request.json index 4f8dddb44df..0f94d462601 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-request.json @@ -1,90 +1,20 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "sonobi": { "TagID": "first-tagid" } } - }, - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "w": 640, - "h": 480 - }, - "ext": { - "sonobi": { - "TagID": "second-tagid" - } - } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json index 4c5f14ab944..2bf9c126798 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json @@ -1,88 +1,21 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 1.25, "adm": "adm001", - "crid": "crid001", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "1.20", - "hb_cache_id_sonobi": "9092799c-93b0-4e11-a232-2c0151d5d275", - "hb_cache_path_sonobi": "{{ cache.path }}", - "hb_cache_path": "{{ cache.path }}", - "hb_pb_sonobi": "1.20", - "hb_size": "300x250", - "hb_bidder_sonobi": "sonobi", - "hb_size_sonobi": "300x250", - "hb_bidder": "sonobi", - "hb_cache_id": "9092799c-93b0-4e11-a232-2c0151d5d275", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_sonobi": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}9092799c-93b0-4e11-a232-2c0151d5d275", - "cacheId": "9092799c-93b0-4e11-a232-2c0151d5d275" - } - } - } - } - }, - { - "id": "bid01", - "impid": "impId002", - "price": 2.25, - "adm": "adm002", - "adid": "29681110", - "adomain": [ - "video-example.com" - ], - "cid": "1001", - "crid": "crid002", - "cat": [ - "IAB2" - ], - "w": 640, - "h": 480, - "ext": { - "prebid": { - "type": "video", - "targeting": { - "hb_cache_id_sonobi": "83cdc325-c816-4d2e-bf2c-9213a70671dd", - "hb_cache_path_sonobi": "{{ cache.path }}", - "hb_size_sonobi": "640x480", - "hb_cache_id": "83cdc325-c816-4d2e-bf2c-9213a70671dd", - "hb_uuid_sonobi": "99dc3357-34ac-4819-9f68-0820039a542f", - "hb_pb": "2.20", - "hb_cache_path": "{{ cache.path }}", - "hb_uuid": "99dc3357-34ac-4819-9f68-0820039a542f", - "hb_pb_sonobi": "2.20", - "hb_size": "640x480", - "hb_bidder_sonobi": "sonobi", - "hb_bidder": "sonobi", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_sonobi": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}83cdc325-c816-4d2e-bf2c-9213a70671dd", - "cacheId": "83cdc325-c816-4d2e-bf2c-9213a70671dd" - }, - "vastXml": { - "url": "{{ cache.resource_url }}99dc3357-34ac-4819-9f68-0820039a542f", - "cacheId": "99dc3357-34ac-4819-9f68-0820039a542f" - } - } - } + "type": "banner" + }, + "origbidcpm": 1.25 } } ], @@ -93,11 +26,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "sonobi": "{{ sonobi.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "sonobi": "{{ sonobi.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-cache-sonobi-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-cache-sonobi-request.json deleted file mode 100644 index 58471d39985..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-cache-sonobi-request.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "adm": "adm001", - "crid": "crid001", - "w": 300, - "h": 250 - } - }, - { - "type": "json", - "value": { - "id": "bid01", - "impid": "impId002", - "price": 2.25, - "adm": "adm002", - "adid": "29681110", - "adomain": [ - "video-example.com" - ], - "cid": "1001", - "crid": "crid002", - "cat": [ - "IAB2" - ], - "w": 640, - "h": 480 - } - }, - { - "type": "xml", - "value": "adm002", - "expiry": 120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-cache-sonobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-cache-sonobi-response.json deleted file mode 100644 index c6317eb6c8c..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-cache-sonobi-response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "responses": [ - { - "uuid": "9092799c-93b0-4e11-a232-2c0151d5d275" - }, - { - "uuid": "83cdc325-c816-4d2e-bf2c-9213a70671dd" - }, - { - "uuid": "99dc3357-34ac-4819-9f68-0820039a542f" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-1.json deleted file mode 100644 index 51ed94815c8..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-1.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId001", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "tagid": "first-tagid", - "ext": { - "bidder": { - "TagID": "first-tagid" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "SB-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-2.json deleted file mode 100644 index bbb3df7405e..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request-2.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "w": 640, - "h": 480 - }, - "tagid": "second-tagid", - "ext": { - "bidder": { - "TagID": "second-tagid" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "SB-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request.json new file mode 100644 index 00000000000..3637896d9be --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "first-tagid", + "ext": { + "bidder": { + "TagID": "first-tagid" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-response-1.json deleted file mode 100644 index 89ec3dc2248..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-response-1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "crid": "crid001", - "adm": "adm001", - "h": 250, - "w": 300 - } - ] - } - ], - "bidid": "bid001" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-response-2.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-response-2.json deleted file mode 100644 index 966ec782f17..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-response-2.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid01", - "impid": "impId002", - "price": 2.25, - "cid": "1001", - "crid": "crid002", - "adid": "29681110", - "adm": "adm002", - "cat": [ - "IAB2" - ], - "adomain": [ - "video-example.com" - ], - "h": 480, - "w": 640 - } - ] - } - ], - "bidid": "bid01" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-response.json new file mode 100644 index 00000000000..260fde09efd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-sonobi-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ], + "bidid": "bid_id" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-request.json index 77b496bb41e..e19095ff01f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-request.json @@ -1,77 +1,20 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId13", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], "w": 300, "h": 250 }, "ext": { "sovrn": { - "tagid": "tagId1" + "tagid": "tag_id" } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-response.json index f6fe198ca04..39760b80408 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-response.json @@ -1,40 +1,21 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "600527793", - "impid": "impId13", + "id": "bid_id", + "impid": "imp_id", "price": 5.78, "adm": "adm 13", - "crid": "crid13", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "5.70", - "hb_size": "300x250", - "hb_bidder_sovrn": "sovrn", - "hb_pb_sovrn": "5.70", - "hb_bidder": "sovrn", - "hb_cache_id": "1e6fb739-d0e7-4b7c-9b00-21aa40dc3301", - "hb_size_sovrn": "300x250", - "hb_cache_id_sovrn": "1e6fb739-d0e7-4b7c-9b00-21aa40dc3301", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_sovrn": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_sovrn": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}1e6fb739-d0e7-4b7c-9b00-21aa40dc3301", - "cacheId": "1e6fb739-d0e7-4b7c-9b00-21aa40dc3301" - } - } - } + "type": "banner" + }, + "origbidcpm": 5.78 } } ], @@ -45,11 +26,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "sovrn": "{{ sovrn.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "sovrn": "{{ sovrn.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-cache-sovrn-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-cache-sovrn-request.json deleted file mode 100644 index aba1be88992..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-cache-sovrn-request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "600527793", - "impid": "impId13", - "price": 5.78, - "adm": "adm 13", - "crid": "crid13", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-cache-sovrn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-cache-sovrn-response.json deleted file mode 100644 index 88c1a7aad43..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-cache-sovrn-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "1e6fb739-d0e7-4b7c-9b00-21aa40dc3301" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-request-1.json deleted file mode 100644 index 3e333fb2f90..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-request-1.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId13", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 300, - "h": 250 - }, - "tagid": "tagId1", - "ext": { - "bidder": { - "tagid": "tagId1" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "990011", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-request.json new file mode 100644 index 00000000000..de55f328d98 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "tag_id", + "ext": { + "bidder": { + "tagid": "tag_id" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-response-1.json deleted file mode 100644 index 9e9ca934bbe..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-response-1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "bidResponseId13", - "seatbid": [ - { - "bid": [ - { - "id": "600527793", - "impid": "impId13", - "price": 5.78, - "adm": "adm+13", - "crid": "crid13", - "w": 300, - "h": 250 - } - ], - "seat": "seatId13", - "group": 0 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-response.json new file mode 100644 index 00000000000..c9537c8a2b2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-sovrn-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 5.78, + "adm": "adm+13", + "crid": "crid", + "w": 300, + "h": 250 + } + ], + "seat": "seatId13", + "group": 0 + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-auction-request.json b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-auction-request.json new file mode 100644 index 00000000000..797adeaf08a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-auction-request.json @@ -0,0 +1,82 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impStoredBidResponse", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + } + }, + "storedbidresponse": { + "bidder": "rubicon", + "id": "test-stored-bid-response" + } + } + } + } + ], + "device": { + "pxratio": 4.2, + "dnt": 2, + "language": "en", + "ifa": "ifaId" + }, + "site": { + "page": "http://www.example.com", + "publisher": { + "id": "publisherId" + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "source": { + "fd": 1, + "tid": "tid" + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + } + }, + "cache": { + "bids": {}, + "vastxml": { + "ttlseconds": 120 + } + }, + "auctiontimestamp": 0 + } + }, + "user": { + "buyeruid": "revcontent-UID" + }, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-auction-response.json new file mode 100644 index 00000000000..7bd4aefd12e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-auction-response.json @@ -0,0 +1,57 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "466223845", + "impid": "impStoredBidResponse", + "price": 0.8, + "adm": "adm1", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_size_rubicon": "300x250", + "hb_cache_id": "474175c2-815f-4bde-90ad-935d2f6e1aa0", + "hb_cache_path_rubicon": "{{ cache.path }}", + "hb_cache_host_rubicon": "{{ cache.host }}", + "hb_pb": "0.80", + "hb_pb_rubicon": "0.80", + "hb_cache_id_rubicon": "474175c2-815f-4bde-90ad-935d2f6e1aa0", + "hb_cache_path": "{{ cache.path }}", + "hb_size": "300x250", + "hb_bidder": "rubicon", + "hb_bidder_rubicon": "rubicon", + "hb_cache_host": "{{ cache.host }}" + }, + "cache": { + "bids": { + "url": "{{ cache.resource_url }}474175c2-815f-4bde-90ad-935d2f6e1aa0", + "cacheId": "474175c2-815f-4bde-90ad-935d2f6e1aa0" + } + } + }, + "origbidcpm": 0.8 + } + } + ], + "seat": "rubicon", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "cache": "{{ cache.response_time_ms }}", + "rubicon": "{{ rubicon.response_time_ms }}" + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-cache-request.json b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-cache-request.json new file mode 100644 index 00000000000..65c5bb22030 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-cache-request.json @@ -0,0 +1,23 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "466223845", + "impid": "impStoredBidResponse", + "price": 0.8, + "adm": "adm1", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 0.8 + } + }, + "aid": "tid" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-cache-response.json b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-cache-response.json new file mode 100644 index 00000000000..ef1b89a3205 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-cache-response.json @@ -0,0 +1,7 @@ +{ + "responses": [ + { + "uuid": "474175c2-815f-4bde-90ad-935d2f6e1aa0" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-request.json b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-request.json index 839add5a448..34ec6de658f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-request.json @@ -1,96 +1,24 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "synacormedia": { - "seatId": "228", - "tagId": "demo1" - } - } - }, - { - "id": "impId002", - "video": { "w": 300, - "h": 250, - "pos": 1, - "mimes": [ - "video/mp4" - ] + "h": 250 }, "ext": { - "synacormedia":{ - "seatId":"1987", - "tagId": "demo1" + "synacormedia": { + "seatId": "seat_id", + "tagId": "tag_id" } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-response.json index 9268e249a56..1f7ce402a5d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-auction-synacormedia-response.json @@ -1,85 +1,23 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 7.77, "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", + "adid": "adid", + "cid": "cid", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "7.70", - "hb_size_synacormedia": "300x250", - "hb_cache_id_synacormedia": "c1662cf6-f00a-4066-b71a-46d97abccc35", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_bidder_synacormedia": "synacormedia", - "hb_cache_host_synacormedia": "{{ cache.host }}", - "hb_bidder": "synacormedia", - "hb_cache_id": "c1662cf6-f00a-4066-b71a-46d97abccc35", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_synacormedia": "{{ cache.path }}", - "hb_pb_synacormedia": "7.70" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}c1662cf6-f00a-4066-b71a-46d97abccc35", - "cacheId": "c1662cf6-f00a-4066-b71a-46d97abccc35" - } - } - } - } - }, - { - "id": "bid002", - "impid": "impId002", - "price": 9.99, - "adomain": [ - "psacentral.org" - ], - "cid": "cid002", - "crid": "crid002", - "w": 300, - "h": 250, - "ext": { - "prebid": { - "type": "video", - "targeting": { - "hb_size_synacormedia": "300x250", - "hb_cache_host_synacormedia": "{{ cache.host }}", - "hb_cache_id": "dbaa191c-5a56-4655-85eb-da079f94e09f", - "hb_pb_synacormedia": "9.90", - "hb_pb": "9.90", - "hb_cache_id_synacormedia": "dbaa191c-5a56-4655-85eb-da079f94e09f", - "hb_cache_path": "{{ cache.path }}", - "hb_uuid": "62019cff-d657-42fc-8366-16c34e1fd28c", - "hb_size": "300x250", - "hb_bidder_synacormedia": "synacormedia", - "hb_bidder": "synacormedia", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_synacormedia": "{{ cache.path }}", - "hb_uuid_synacormedia": "62019cff-d657-42fc-8366-16c34e1fd28c" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}dbaa191c-5a56-4655-85eb-da079f94e09f", - "cacheId": "dbaa191c-5a56-4655-85eb-da079f94e09f" - }, - "vastXml": { - "url": "{{ cache.resource_url }}62019cff-d657-42fc-8366-16c34e1fd28c", - "cacheId": "62019cff-d657-42fc-8366-16c34e1fd28c" - } - } - } + "type": "banner" + }, + "origbidcpm": 7.77 } } ], @@ -90,11 +28,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "synacormedia": "{{ synacormedia.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "synacormedia": "{{ synacormedia.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-cache-synacormedia-request.json b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-cache-synacormedia-request.json deleted file mode 100644 index c76c7256d07..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-cache-synacormedia-request.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 7.77, - "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", - "w": 300, - "h": 250 - } - }, - { - "type": "json", - "value": { - "id": "bid002", - "impid": "impId002", - "price": 9.99, - "adomain": [ - "psacentral.org" - ], - "cid": "cid002", - "crid": "crid002", - "w": 300, - "h": 250 - } - }, - { - "type": "xml", - "value": "prebid.org wrapper", - "expiry": 120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-cache-synacormedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-cache-synacormedia-response.json deleted file mode 100644 index babbed58d14..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-cache-synacormedia-response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "responses": [ - { - "uuid": "c1662cf6-f00a-4066-b71a-46d97abccc35" - }, - { - "uuid": "dbaa191c-5a56-4655-85eb-da079f94e09f" - }, - { - "uuid": "62019cff-d657-42fc-8366-16c34e1fd28c" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-request.json index 836ad96bce4..ea5527c0a73 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-request.json @@ -1,48 +1,26 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "tagid": "demo1", - "ext": { - "bidder": { - "seatId": "228", - "tagId": "demo1" - } - } - }, - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], "w": 300, - "h": 250, - "pos": 1 + "h": 250 }, - "tagid": "demo1", + "tagid": "tag_id", "ext": { "bidder": { - "seatId": "1987", - "tagId": "demo1" + "seatId": "seat_id", + "tagId": "tag_id" } } } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -50,39 +28,19 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "SCM-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } }, "ext": { - "seatId": "228", - "tagId": "demo1" + "seatId": "seat_id" } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-response.json index f1db668198e..d9ae07cf3a9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/synacormedia/test-synacormedia-bid-response.json @@ -1,33 +1,21 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 7.77, - "adid": "adid001", - "crid": "crid001", - "cid": "cid001", + "adid": "adid", + "crid": "crid", + "cid": "cid", "adm": "adm001", "h": 250, "w": 300 - }, - { - "id": "bid002", - "impid": "impId002", - "price": 9.99, - "crid": "crid002", - "cid": "cid002", - "adomain": [ - "psacentral.org" - ], - "h": 250, - "w": 300 } ], "seat": "synacormedia" } ] -} \ No newline at end of file +} 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 47492a49d71..e4c871ea24e 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 @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId12", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "tappx": { @@ -21,58 +17,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-response.json index b8c423172e3..98bd6f901a7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-response.json @@ -1,19 +1,19 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "wehM-93KGr0_0_0", - "impid": "impId12", + "id": "bid_id", + "impid": "imp_id", "price": 0.5, "adm": "", - "adid": "19005", + "adid": "adid", "adomain": [ "test.com" ], - "cid": "3706", - "crid": "19005", + "cid": "cid", + "crid": "crid", "cat": [ "IAB2" ], @@ -21,28 +21,9 @@ "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "0.50", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_host_tappx": "{{ cache.host }}", - "hb_size": "300x250", - "hb_cache_path_tappx": "{{ cache.path }}", - "hb_bidder_tappx": "tappx", - "hb_bidder": "tappx", - "hb_cache_id_tappx": "9c0c4f2f-686f-4673-a00a-d8cae7e7a05d", - "hb_size_tappx": "300x250", - "hb_cache_id": "9c0c4f2f-686f-4673-a00a-d8cae7e7a05d", - "hb_cache_host": "{{ cache.host }}", - "hb_pb_tappx": "0.50" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}9c0c4f2f-686f-4673-a00a-d8cae7e7a05d", - "cacheId": "9c0c4f2f-686f-4673-a00a-d8cae7e7a05d" - } - } - } + "type": "banner" + }, + "origbidcpm": 0.5 } } ], @@ -53,11 +34,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "tappx": "{{ tappx.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "tappx": "{{ tappx.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-cache-tappx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-cache-tappx-request.json deleted file mode 100644 index 68da84fe2ec..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-cache-tappx-request.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "wehM-93KGr0_0_0", - "impid": "impId12", - "price": 0.5, - "cid": "3706", - "crid": "19005", - "adid": "19005", - "adm": "", - "cat": ["IAB2"], - "adomain": ["test.com"], - "h": 250, - "w": 300 - } - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-cache-tappx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-cache-tappx-response.json deleted file mode 100644 index 2680566622c..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-cache-tappx-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "9c0c4f2f-686f-4673-a00a-d8cae7e7a05d" - } - ] -} 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 89ee69285d3..fad771082b3 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 @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId12", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "bidfloor": 1.5, "ext": { @@ -23,10 +19,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -34,62 +30,21 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "TX-UID", - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } }, "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } + "bidder": { + "tappxkey": "pub-12345-android-9876" } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-response.json index 6da4905c67c..0096be158d1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-response.json @@ -1,22 +1,28 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "seat": "rtbhouse", - "bid": [{ - "id": "wehM-93KGr0_0_0", - "impid": "impId12", - "price": 0.5, - "cid": "3706", - "crid": "19005", - "adid": "19005", - "adm": "", - "cat": ["IAB2"], - "adomain": ["test.com"], - "h": 250, - "w": 300 - }] + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 0.5, + "cid": "cid", + "crid": "crid", + "adid": "adid", + "adm": "", + "cat": [ + "IAB2" + ], + "adomain": [ + "test.com" + ], + "h": 250, + "w": 300 + } + ] } ], - "bidid": "bid01" + "bidid": "bid_id" } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-request.json b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-request.json index f98e514064f..6ff55fbb954 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-request.json @@ -1,9 +1,9 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId002", - "tagid": "N4zTDq3PPEHBIODv7cXK", + "id": "imp_id", + "tagid": "tag_id", "video": { "mimes": [ "video/mp4" @@ -28,77 +28,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "ip": "193.168.244.1", - "language": "en", - "ifa": "ifaId", - "ua": "userAgent" - }, - "site": { - "domain": "example.com", - "ext": { - "amp": 0 - }, - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "includebidderkeys": true, - "includewinners": true, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-response.json b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-response.json index b8fdc317be6..f0d9731620a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-auction-telaria-response.json @@ -1,47 +1,21 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "1", - "impid": "impId002", + "id": "bid_id", + "impid": "imp_id", "price": 0.01, "adm": "hi", - "cid": "test_cid", - "crid": "test_video_crid", + "cid": "cid", + "crid": "crid", "ext": { "prebid": { - "type": "video", - "targeting": { - "hb_pb": "0.00", - "hb_cache_id_telaria": "9c0c4f2f-686f-4673-a00a-d8cae7e7a05d", - "hb_uuid_telaria": "0553c675-4afc-431a-b22c-0ea9cc01977e", - "hb_cache_path": "/cache", - "hb_uuid": "0553c675-4afc-431a-b22c-0ea9cc01977e", - "hb_pb_telaria": "0.00", - "hb_bidder": "telaria", - "hb_cache_id": "9c0c4f2f-686f-4673-a00a-d8cae7e7a05d", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_telaria": "/cache", - "hb_pb_telaria": "0.00", - "hb_cache_host_telaria": "{{ cache.host }}", - "hb_bidder_telaria": "telaria" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}9c0c4f2f-686f-4673-a00a-d8cae7e7a05d", - "cacheId": "9c0c4f2f-686f-4673-a00a-d8cae7e7a05d" - }, - "vastXml": { - "url": "{{ cache.resource_url }}0553c675-4afc-431a-b22c-0ea9cc01977e", - "cacheId": "0553c675-4afc-431a-b22c-0ea9cc01977e" - } - } + "type": "video" }, - "bidder": { - "format": "VIDEO" - } + "origbidcpm": 0.01, + "format": "VIDEO" } } ], @@ -52,11 +26,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "telaria": "{{ telaria.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "telaria": "{{ telaria.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-cache-telaria-request.json b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-cache-telaria-request.json deleted file mode 100644 index 94bed967e0c..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-cache-telaria-request.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "1", - "impid": "impId002", - "price": 0.01, - "adm": "hi", - "cid": "test_cid", - "crid": "test_video_crid", - "ext": { - "format": "VIDEO" - } - } - }, - { - "type": "xml", - "value": "hi", - "expiry": 120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-cache-telaria-response.json b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-cache-telaria-response.json deleted file mode 100644 index 1ec0c1e0b5a..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-cache-telaria-response.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "responses": [ - { - "uuid": "9c0c4f2f-686f-4673-a00a-d8cae7e7a05d" - }, - { - "uuid": "0553c675-4afc-431a-b22c-0ea9cc01977e" - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-request-1.json deleted file mode 100644 index 3fb7054b871..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-request-1.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "w": 1024, - "h": 576, - "skipmin": 0, - "skipafter": 0 - }, - "tagid": "my-adcode", - "ext": { - "originalTagid": "N4zTDq3PPEHBIODv7cXK", - "originalPublisherid": "publisherId" - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "my-seatcode" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "TL-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "extra": { - "custom": "1234" - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-request.json new file mode 100644 index 00000000000..a3b7b7ab0b9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-request.json @@ -0,0 +1,55 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576, + "skipmin": 0, + "skipafter": 0 + }, + "tagid": "my-adcode", + "ext": { + "originalTagid": "tag_id" + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "my-seatcode", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "extra": { + "custom": "1234" + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-response-1.json deleted file mode 100644 index be57f5bf39c..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-response-1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "adm": "hi", - "crid": "test_video_crid", - "cid": "test_cid", - "impid": "impId002", - "id": "1", - "price": 0.01, - "ext": { - "format": "VIDEO" - } - } - ] - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-response.json new file mode 100644 index 00000000000..b902e03219d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/telaria/test-telaria-bid-response.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "adm": "hi", + "crid": "crid", + "cid": "cid", + "impid": "imp_id", + "exp": 120, + "id": "bid_id", + "price": 0.01, + "ext": { + "format": "VIDEO" + } + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json index 8a2deca46c7..e6bfa8cab8c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId1", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "triplelift": { @@ -18,58 +14,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-response.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-response.json index c06053eea40..e57fe1c9cbf 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-response.json @@ -1,50 +1,30 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "7706636740145184841", - "impid": "impId1", + "id": "bid_id", + "impid": "imp_id", "price": 0.5, "adm": "some-test-ad", - "adid": "29681110", + "adid": "adid", "adomain": [ "triplelift.com" ], "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", + "cid": "cid", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "0.50", - "hb_cache_path_triplelift": "{{ cache.path }}", - "hb_cache_path": "{{ cache.path }}", - "hb_size_triplelift": "300x250", - "hb_bidder_triplelift": "triplelift", - "hb_size": "300x250", - "hb_bidder": "triplelift", - "hb_cache_id": "5b9f507f-2ae9-4111-a697-796f3174df10", - "hb_cache_host": "{{ cache.host }}", - "hb_pb_triplelift": "0.50", - "hb_cache_id_triplelift": "5b9f507f-2ae9-4111-a697-796f3174df10", - "hb_cache_host_triplelift": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "http://localhost:8090/cache?uuid=5b9f507f-2ae9-4111-a697-796f3174df10", - "cacheId": "5b9f507f-2ae9-4111-a697-796f3174df10" - } - } + "type": "banner" }, - "bidder": { - "triplelift_pb": { - "format": 2 - } + "origbidcpm": 0.5, + "origbidcur": "USD", + "triplelift_pb": { + "format": 2 } } } @@ -56,11 +36,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "triplelift": "{{ triplelift.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "triplelift": "{{ triplelift.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-cache-triplelift-request.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-cache-triplelift-request.json deleted file mode 100644 index 23689d131c7..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-cache-triplelift-request.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "7706636740145184841", - "impid": "impId1", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": [ - "triplelift.com" - ], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "w": 300, - "h": 250, - "ext": { - "triplelift_pb": { - "format": 2 - } - } - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-cache-triplelift-response.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-cache-triplelift-response.json deleted file mode 100644 index 955f18dcb33..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-cache-triplelift-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "5b9f507f-2ae9-4111-a697-796f3174df10" - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json index 5eb728e4cd9..07e21eb169b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId1", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "tagid": "foo", "ext": { @@ -20,10 +16,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -31,62 +27,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "TL-UID", - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-response.json index 23c8eaacd7f..af4eea052a8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-response.json @@ -1,21 +1,21 @@ { - "id": "test-request-id", + "id": "request_id", "seatbid": [ { "seat": "958", "bid": [ { - "id": "7706636740145184841", - "impid": "impId1", + "id": "bid_id", + "impid": "imp_id", "price": 0.5, - "adid": "29681110", + "adid": "adid", "adm": "some-test-ad", "adomain": [ "triplelift.com" ], "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", + "cid": "cid", + "crid": "crid", "h": 250, "w": 300, "ext": { @@ -27,6 +27,6 @@ ] } ], - "bidid": "5778926625248726496", + "bidid": "bid_id", "cur": "USD" } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json index 7653b419c44..64a8c1dc3b5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json @@ -1,75 +1,28 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "test-imp-id", + "id": "imp_id", "native": { "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"required\":1,\"title\":{\"len\":500}}]}" }, "ext": { - "triplelift_native": { - "inventoryCode": "foo" + "prebid": { + "bidder": { + "triplelift_native": { + "inventoryCode": "foo" + } + } } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, "site": { "publisher": { "id": "test" } }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json index 00a6d4ec1c0..0c61803d3c3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json @@ -1,46 +1,28 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "7706636740145184841", - "impid": "test-imp-id", + "id": "bid_id", + "impid": "imp_id", "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", + "adm": "{\"assets\":[{\"id\":0,\"title\":{\"text\":\"This is an example Prebid Native creative\"}}]}", + "adid": "adid", "adomain": [ "triplelift.com" ], "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", + "cid": "cid", + "crid": "crid", "h": 250, "w": 300, "ext": { "prebid": { - "type": "native", - "targeting": { - "hb_pb": "0.50", - "hb_cache_path_triplelift_native": "{{ cache.path }}", - "hb_cache_path": "{{ cache.path }}", - "hb_size_triplelift_native": "300x250", - "hb_bidder_triplelift_native": "triplelift_native", - "hb_size": "300x250", - "hb_bidder": "triplelift_native", - "hb_cache_id": "029e95ca-1a14-4e45-9669-8ad8d667de50", - "hb_cache_host": "{{ cache.host }}", - "hb_pb_triplelift_native": "0.50", - "hb_cache_id_triplelift_native": "029e95ca-1a14-4e45-9669-8ad8d667de50", - "hb_cache_host_triplelift_native": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "http://localhost:8090/cache?uuid=029e95ca-1a14-4e45-9669-8ad8d667de50", - "cacheId": "029e95ca-1a14-4e45-9669-8ad8d667de50" - } - } - } + "type": "native" + }, + "origbidcpm": 0.5, + "origbidcur": "USD" } } ], @@ -50,122 +32,11 @@ ], "cur": "USD", "ext": { - "debug": { - "httpcalls": { - "cache": [ - { - "uri": "{{ cache.endpoint }}", - "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-id\",\"price\":0.5,\"adm\":\"some-test-ad\",\"adid\":\"29681110\",\"adomain\":[\"triplelift.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"w\":300,\"h\":250}}]}", - "responsebody": "{\"responses\":[{\"uuid\":\"029e95ca-1a14-4e45-9669-8ad8d667de50\"}]}", - "status": 200 - } - ], - "triplelift_native": [ - { - "uri": "{{ triplelift_native.exchange_uri }}", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"test-imp-id\",\"native\":{\"request\":\"{\\\"ver\\\":\\\"1.1\\\",\\\"context\\\":1,\\\"contextsubtype\\\":11,\\\"plcmttype\\\":4,\\\"plcmtcnt\\\":1,\\\"assets\\\":[{\\\"id\\\":0,\\\"required\\\":1,\\\"title\\\":{\\\"len\\\":500}}]}\"},\"tagid\":\"foo\",\"ext\":{\"bidder\":{\"inventoryCode\":\"foo\"}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"test\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"T\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"aliases\":{\"appnexusAlias\":\"appnexus\",\"conversantAlias\":\"conversant\"},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", - "responsebody": "{\"id\":\"test-request-id\",\"seatbid\":[{\"seat\":\"958\",\"bid\":[{\"id\":\"7706636740145184841\",\"impid\":\"test-imp-id\",\"price\":0.5,\"adid\":\"29681110\",\"adm\":\"some-test-ad\",\"adomain\":[\"triplelift.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"h\":250,\"w\":300}]}],\"bidid\":\"5778926625248726496\",\"cur\":\"USD\"}", - "status": 200 - } - ] - }, - "resolvedrequest": { - "id": "tid", - "imp": [ - { - "id": "test-imp-id", - "native": { - "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":500}}]}" - }, - "ext": { - "triplelift_native": { - "inventoryCode": "foo" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "test" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } - } - }, "responsetimemillis": { - "triplelift_native": "{{ triplelift_native.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "triplelift_native": "{{ triplelift_native.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-cache-triplelift-native-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-cache-triplelift-native-request.json deleted file mode 100644 index d0168e8c2b9..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-cache-triplelift-native-request.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.5, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": [ - "triplelift.com" - ], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "h": 250, - "w": 300 - } - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-cache-triplelift-native-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-cache-triplelift-native-response.json deleted file mode 100644 index c227f5cb24a..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-cache-triplelift-native-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "029e95ca-1a14-4e45-9669-8ad8d667de50" - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json index 439b6474237..232e2aed675 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json @@ -1,8 +1,8 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "test-imp-id", + "id": "imp_id", "native": { "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":500}}]}" }, @@ -15,10 +15,11 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "test" + "id": "test", + "domain": "example.com" }, "ext": { "amp": 0 @@ -26,67 +27,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "T", - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "debug": 1, - "aliases": { - "appnexusAlias": "appnexus", - "conversantAlias": "conversant" - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-response.json index d445d58033c..eb95150819f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-response.json @@ -1,27 +1,27 @@ { - "id": "test-request-id", + "id": "request_id", "seatbid": [ { "seat": "958", "bid": [ { - "id": "7706636740145184841", - "impid": "test-imp-id", + "id": "bid_id", + "impid": "imp_id", "price": 0.5, - "adid": "29681110", - "adm": "some-test-ad", + "adid": "adid", + "adm": "{\"assets\":[{\"id\":0,\"title\":{\"text\":\"This is an example Prebid Native creative\"}}]}", "adomain": [ "triplelift.com" ], "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", + "cid": "cid", + "crid": "crid", "h": 250, "w": 300 } ] } ], - "bidid": "5778926625248726496", + "bidid": "bid_id", "cur": "USD" } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json index 0e6d21179aa..dd257a94191 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "ttx": { @@ -20,61 +16,10 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-response.json index ebd9b8f21ff..50c30722255 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-response.json @@ -1,40 +1,21 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 1.25, "adm": "adm001", - "crid": "crid001", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_cache_path_ttx": "{{ cache.path }}", - "hb_cache_host_ttx": "{{ cache.host }}", - "hb_bidder_ttx": "ttx", - "hb_cache_id": "ab634778-1ad8-4851-8f8d-b8588885ec78", - "hb_cache_id_ttx": "ab634778-1ad8-4851-8f8d-b8588885ec78", - "hb_pb_ttx": "1.20", - "hb_pb": "1.20", - "hb_size_ttx": "300x250", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_bidder": "ttx", - "hb_cache_host": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}ab634778-1ad8-4851-8f8d-b8588885ec78", - "cacheId": "ab634778-1ad8-4851-8f8d-b8588885ec78" - } - } - } + "type": "banner" + }, + "origbidcpm": 1.25 } } ], @@ -45,12 +26,11 @@ "cur": "USD", "ext": { "responsetimemillis": { - "ttx": "{{ ttx.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "ttx": "{{ ttx.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-cache-ttx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-cache-ttx-request.json deleted file mode 100644 index 14a96320420..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-cache-ttx-request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "adm": "adm001", - "crid": "crid001", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-cache-ttx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-cache-ttx-response.json deleted file mode 100644 index 3e0324beabe..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-cache-ttx-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "ab634778-1ad8-4851-8f8d-b8588885ec78" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json deleted file mode 100644 index 95ee04bedbb..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request-1.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId001", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "ttx": { - "prod": "inview", - "zoneid": "zone-id" - } - } - } - ], - "site": { - "id": "site-id", - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "TTX-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request.json new file mode 100644 index 00000000000..0104d8914d3 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "ttx": { + "prod": "inview", + "zoneid": "zone-id" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-response-1.json deleted file mode 100644 index 89ec3dc2248..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-response-1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "crid": "crid001", - "adm": "adm001", - "h": 250, - "w": 300 - } - ] - } - ], - "bidid": "bid001" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-response.json new file mode 100644 index 00000000000..1dc4ed293ae --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-ttx-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ], + "bidid": "bid_id" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-request.json index 8317dba4939..1ea792dc710 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "ucfunnel": { @@ -19,68 +15,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-response.json index b05053ea656..cb78783eafb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-response.json @@ -1,42 +1,23 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 3.33, "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", + "adid": "adid", + "cid": "cid", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "3.30", - "hb_size_ucfunnel": "300x250", - "hb_bidder_ucfunnel": "ucfunnel", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_cache_host_ucfunnel": "{{ cache.host }}", - "hb_cache_path_ucfunnel": "{{ cache.path }}", - "hb_cache_id_ucfunnel": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_bidder": "ucfunnel", - "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_pb_ucfunnel": "3.30", - "hb_cache_host": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - } - } + "type": "banner" + }, + "origbidcpm": 3.33 } } ], @@ -47,11 +28,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "ucfunnel": "{{ ucfunnel.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "ucfunnel": "{{ ucfunnel.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-cache-ucfunnel-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-cache-ucfunnel-request.json deleted file mode 100644 index 60845624d51..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-cache-ucfunnel-request.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 3.33, - "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-cache-ucfunnel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-cache-ucfunnel-response.json deleted file mode 100644 index 93d0b8de2cd..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-cache-ucfunnel-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-request.json index 6eb2dec557c..7d0fb71f1dc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "bidder": { @@ -20,10 +16,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -31,72 +27,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "UF-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-response.json index 95a93284e04..8901429253e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-ucfunnel-bid-response.json @@ -1,15 +1,15 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 3.33, - "adid": "adid001", - "crid": "crid001", - "cid": "cid001", + "adid": "adid", + "crid": "crid", + "cid": "cid", "adm": "adm001", "h": 250, "w": 300 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-auction-unicorn-request.json b/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-auction-unicorn-request.json new file mode 100644 index 00000000000..6e67e1171df --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-auction-unicorn-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "unicorn": { + "placementId": "placementId", + "publisherId": 123, + "mediaId": "mediaTestId", + "accountId": 456 + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-auction-unicorn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-auction-unicorn-response.json new file mode 100644 index 00000000000..abe21f3cada --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-auction-unicorn-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid", + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "unicorn", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "unicorn": "{{ unicorn.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-unicorn-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-unicorn-bid-request.json new file mode 100644 index 00000000000..fec5225f4e5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-unicorn-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "placementId", + "secure": 1, + "ext": { + "bidder": { + "placementId": "placementId", + "publisherId": 123, + "mediaId": "mediaTestId", + "accountId": 456 + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "source": { + "ext": { + "bidder": "unicorn", + "stype": "prebid_server_uncn" + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "accountId": 456 + } +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-unicorn-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-unicorn-bid-response.json new file mode 100644 index 00000000000..8901429253e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-unicorn-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid", + "crid": "crid", + "cid": "cid", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json index 0805dd611c1..66c0070a37a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json @@ -1,8 +1,8 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "video": { "mimes": [ "video/mp4" @@ -16,76 +16,9 @@ "siteid": "site_id_1" } } - }, - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "w": 640, - "h": 480 - }, - "ext": { - "unruly": { - "uuid": "uu_id_2", - "siteid": "site_id_2" - } - } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-response.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-response.json index 4ae45e5168a..22ac518e357 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-response.json @@ -1,86 +1,21 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 1.25, "adm": "adm001", - "crid": "crid001", + "crid": "crid", "w": 800, "h": 600, "ext": { "prebid": { - "type": "video", - "targeting": { - "hb_pb_unruly": "1.20", - "hb_cache_id_unruly": "d7ec26d4-4336-4661-988a-79e96040e281", - "hb_cache_id": "d7ec26d4-4336-4661-988a-79e96040e281", - "hb_uuid_unruly": "54a3b0a5-e145-43cf-a1cc-1beaa8b29018", - "hb_pb": "1.20", - "hb_cache_path": "{{ cache.path }}", - "hb_uuid": "54a3b0a5-e145-43cf-a1cc-1beaa8b29018", - "hb_bidder_unruly": "unruly", - "hb_size": "800x600", - "hb_bidder": "unruly", - "hb_size_unruly": "800x600", - "hb_cache_host_unruly": "{{ cache.host }}", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_unruly": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}d7ec26d4-4336-4661-988a-79e96040e281", - "cacheId": "d7ec26d4-4336-4661-988a-79e96040e281" - }, - "vastXml": { - "url": "{{ cache.resource_url }}54a3b0a5-e145-43cf-a1cc-1beaa8b29018", - "cacheId": "54a3b0a5-e145-43cf-a1cc-1beaa8b29018" - } - } - } - } - }, - { - "id": "bid002", - "impid": "impId002", - "price": 2.25, - "adm": "adm002", - "crid": "crid002", - "w": 640, - "h": 480, - "ext": { - "prebid": { - "type": "video", - "targeting": { - "hb_pb_unruly": "2.20", - "hb_cache_id_unruly": "51c3e63e-5ece-4d16-b781-4314ce9e3ea8", - "hb_cache_id": "51c3e63e-5ece-4d16-b781-4314ce9e3ea8", - "hb_uuid_unruly": "41c08fce-546f-4a57-a657-1158fd62af3d", - "hb_pb": "2.20", - "hb_cache_path": "{{ cache.path }}", - "hb_uuid": "41c08fce-546f-4a57-a657-1158fd62af3d", - "hb_bidder_unruly": "unruly", - "hb_size": "640x480", - "hb_bidder": "unruly", - "hb_size_unruly": "640x480", - "hb_cache_host_unruly": "{{ cache.host }}", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_unruly": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}51c3e63e-5ece-4d16-b781-4314ce9e3ea8", - "cacheId": "51c3e63e-5ece-4d16-b781-4314ce9e3ea8" - }, - "vastXml": { - "url": "{{ cache.resource_url }}41c08fce-546f-4a57-a657-1158fd62af3d", - "cacheId": "41c08fce-546f-4a57-a657-1158fd62af3d" - } - } - } + "type": "video" + }, + "origbidcpm": 1.25 } } ], @@ -91,11 +26,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "unruly": "{{ unruly.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "unruly": "{{ unruly.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-cache-unruly-request.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-cache-unruly-request.json deleted file mode 100644 index 3bb021b79d7..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-cache-unruly-request.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "adm": "adm001", - "crid": "crid001", - "w": 800, - "h": 600 - } - }, - { - "type": "json", - "value": { - "id": "bid002", - "impid": "impId002", - "price": 2.25, - "adm": "adm002", - "crid": "crid002", - "w": 640, - "h": 480 - } - }, - { - "type": "xml", - "value": "adm001", - "expiry": 120 - }, - { - "type": "xml", - "value": "adm002", - "expiry": 120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-cache-unruly-response.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-cache-unruly-response.json deleted file mode 100644 index 67a7f6b9984..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-cache-unruly-response.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "responses": [ - { - "uuid": "d7ec26d4-4336-4661-988a-79e96040e281" - }, - { - "uuid": "51c3e63e-5ece-4d16-b781-4314ce9e3ea8" - }, - { - "uuid": "54a3b0a5-e145-43cf-a1cc-1beaa8b29018" - }, - { - "uuid": "41c08fce-546f-4a57-a657-1158fd62af3d" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-1.json deleted file mode 100644 index f7a7377760f..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-1.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId001", - "video": { - "mimes": [ - "video/mp4" - ], - "w": 800, - "h": 600 - }, - "ext": { - "unruly": { - "uuid": "uu_id_1", - "siteid": "site_id_1" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "UR-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-2.json deleted file mode 100644 index 974bf822a1e..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request-2.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "w": 640, - "h": 480 - }, - "ext": { - "unruly": { - "uuid": "uu_id_2", - "siteid": "site_id_2" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "UR-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json new file mode 100644 index 00000000000..f334cef507c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json @@ -0,0 +1,45 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "unruly": { + "uuid": "uu_id_1", + "siteid": "site_id_1" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-response-1.json deleted file mode 100644 index 66500cc9040..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-response-1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "crid": "crid001", - "adm": "adm001", - "h": 600, - "w": 800 - } - ] - } - ], - "bidid": "bid001" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-response-2.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-response-2.json deleted file mode 100644 index 060c1c94664..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-response-2.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid002", - "impid": "impId002", - "price": 2.25, - "crid": "crid002", - "adm": "adm002", - "h": 480, - "w": 640 - } - ] - } - ], - "bidid": "bid002" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-response.json new file mode 100644 index 00000000000..6c59f41f84e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800 + } + ] + } + ], + "bidid": "bid001" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-request.json b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-request.json index c8226ede61c..0710f50bde5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-request.json @@ -1,34 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "valueimpression": { - "siteId": "123" - } - } - }, - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "w": 1024, - "h": 576 + "w": 300, + "h": 250 }, "ext": { "valueimpression": { @@ -37,69 +14,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "buyeruid": "VI-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-response.json b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-response.json index df2c09aaf36..248c2cfced2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-auction-valueimpression-response.json @@ -1,84 +1,23 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 3.33, "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", + "adid": "adid", + "cid": "cid", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "3.30", - "hb_size_valueimpression": "300x250", - "hb_bidder_valueimpression": "valueimpression", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_cache_host_valueimpression": "{{ cache.host }}", - "hb_cache_path_valueimpression": "{{ cache.path }}", - "hb_cache_id_valueimpression": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_bidder": "valueimpression", - "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_pb_valueimpression": "3.30", - "hb_cache_host": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - } - } - } - }, - { - "id": "bid002", - "impid": "impId002", - "price": 5.55, - "adm": "adm002", - "adid": "adid002", - "cid": "cid002", - "crid": "crid002", - "w": 1024, - "h": 576, - "ext": { - "prebid": { - "type": "video", - "targeting": { - "hb_cache_host_valueimpression": "{{ cache.host }}", - "hb_cache_path_valueimpression": "{{ cache.path }}", - "hb_cache_id": "e7965b2e-0aa3-4252-a22c-580ed010e619", - "hb_pb_valueimpression": "5.50", - "hb_pb": "5.50", - "hb_size_valueimpression": "1024x576", - "hb_bidder_valueimpression": "valueimpression", - "hb_cache_path": "{{ cache.path }}", - "hb_uuid": "44a52b06-b29f-4819-a05f-db36b9e7b8fc", - "hb_size": "1024x576", - "hb_cache_id_valueimpression": "e7965b2e-0aa3-4252-a22c-580ed010e619", - "hb_bidder": "valueimpression", - "hb_cache_host": "{{ cache.host }}", - "hb_uuid_valueimpression": "44a52b06-b29f-4819-a05f-db36b9e7b8fc" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}e7965b2e-0aa3-4252-a22c-580ed010e619", - "cacheId": "e7965b2e-0aa3-4252-a22c-580ed010e619" - }, - "vastXml": { - "url": "{{ cache.resource_url }}44a52b06-b29f-4819-a05f-db36b9e7b8fc", - "cacheId": "44a52b06-b29f-4819-a05f-db36b9e7b8fc" - } - } - } + "type": "banner" + }, + "origbidcpm": 3.33 } } ], @@ -89,11 +28,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "valueimpression": "{{ valueimpression.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "valueimpression": "{{ valueimpression.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-cache-valueimpression-request.json b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-cache-valueimpression-request.json deleted file mode 100644 index 883ffec2c36..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-cache-valueimpression-request.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 3.33, - "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", - "w": 300, - "h": 250 - } - }, - { - "type": "json", - "value": { - "id": "bid002", - "impid": "impId002", - "price": 5.55, - "adm": "adm002", - "adid": "adid002", - "cid": "cid002", - "crid": "crid002", - "w": 1024, - "h": 576 - } - }, - { - "type": "xml", - "value": "adm002", - "expiry": 120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-cache-valueimpression-response.json b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-cache-valueimpression-response.json deleted file mode 100644 index 7d2718f9414..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-cache-valueimpression-response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "responses": [ - { - "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - }, - { - "uuid": "e7965b2e-0aa3-4252-a22c-580ed010e619" - }, - { - "uuid": "44a52b06-b29f-4819-a05f-db36b9e7b8fc" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-request-1.json deleted file mode 100644 index ec42d77106c..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-request-1.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId001", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "bidder": { - "siteId": "123" - } - } - }, - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2, - 5 - ], - "w": 1024, - "h": 576 - }, - "ext": { - "bidder": { - "siteId": "123" - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "VI-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.811 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-request.json new file mode 100644 index 00000000000..8afd18f36b6 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-request.json @@ -0,0 +1,41 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-response-1.json deleted file mode 100644 index 7450ff1cebd..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-response-1.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid001", - "impid": "impId001", - "price": 3.33, - "adid": "adid001", - "crid": "crid001", - "cid": "cid001", - "adm": "adm001", - "h": 250, - "w": 300 - } - ] - }, - { - "bid": [ - { - "id": "bid002", - "impid": "impId002", - "price": 5.55, - "adid": "adid002", - "crid": "crid002", - "cid": "cid002", - "adm": "adm002", - "h": 576, - "w": 1024 - } - ] - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-response.json new file mode 100644 index 00000000000..5c64c43e045 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/valueimpression/test-valueimpression-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid", + "crid": "crid", + "cid": "cid", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-request.json b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-request.json index af571db17df..76979ec2344 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "verizonmedia": { @@ -19,58 +15,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-response.json index 920e42dc4e9..19948abcb4a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-auction-verizonmedia-response.json @@ -1,40 +1,21 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 1.25, "adm": "adm001", - "crid": "crid001", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "1.20", - "hb_pb_verizonmedia": "1.20", - "hb_size_verizonmedia": "300x250", - "hb_cache_id_verizonmedia": "3bdd45bf-b80e-4943-8445-32692ae5ff51", - "hb_cache_path": "{{ cache.path }}", - "hb_bidder_verizonmedia": "verizonmedia", - "hb_size": "300x250", - "hb_cache_path_verizonmedia": "{{ cache.path }}", - "hb_cache_host_verizonmedia": "{{ cache.host }}", - "hb_bidder": "verizonmedia", - "hb_cache_id": "3bdd45bf-b80e-4943-8445-32692ae5ff51", - "hb_cache_host": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}3bdd45bf-b80e-4943-8445-32692ae5ff51", - "cacheId": "3bdd45bf-b80e-4943-8445-32692ae5ff51" - } - } - } + "type": "banner" + }, + "origbidcpm": 1.25 } } ], @@ -45,11 +26,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "verizonmedia": "{{ verizonmedia.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "verizonmedia": "{{ verizonmedia.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-cache-verizonmedia-request.json b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-cache-verizonmedia-request.json deleted file mode 100644 index 14a96320420..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-cache-verizonmedia-request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "adm": "adm001", - "crid": "crid001", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-cache-verizonmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-cache-verizonmedia-response.json deleted file mode 100644 index 1dcd0cb72c7..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-cache-verizonmedia-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "3bdd45bf-b80e-4943-8445-32692ae5ff51" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-request-1.json deleted file mode 100644 index ce7df815bbc..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-request-1.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId001", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 300, - "h": 250 - }, - "tagid": "pos", - "ext": { - "bidder": { - "dcn": "dcn", - "pos": "pos" - } - } - } - ], - "site": { - "id": "dcn", - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-request.json new file mode 100644 index 00000000000..510e3ec0252 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-request.json @@ -0,0 +1,44 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "pos", + "ext": { + "bidder": { + "dcn": "dcn", + "pos": "pos" + } + } + } + ], + "site": { + "id": "dcn", + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-response-1.json deleted file mode 100644 index 89ec3dc2248..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-response-1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "crid": "crid001", - "adm": "adm001", - "h": 250, - "w": 300 - } - ] - } - ], - "bidid": "bid001" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-response.json new file mode 100644 index 00000000000..260fde09efd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/verizonmedia/test-verizonmedia-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ], + "bidid": "bid_id" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json index af712452338..8c9fec28d83 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-1.json @@ -233,8 +233,11 @@ } ], "site": { - "domain": "example.com", - "page": "prebid.com", + "domain": "prebid.com", + "page": "https://prebid.com", + "publisher": { + "domain": "prebid.com" + }, "content": { "episode": 6, "title": "episodeName", @@ -248,17 +251,28 @@ } }, "device": { - "ua": "userAgent", - "ip": "193.168.244.0" + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "dnt": 33, + "lmt": 44, + "ip": "123.145.167.0", + "devicetype": 1, + "os": "mac os", + "h": 480, + "w": 640, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "didsha1": "didsha1", + "didmd5": "didmd5", + "dpidsha1": "dpidsha1", + "dpidmd5": "dpidmd5", + "macsha1": "macsha1", + "macmd5": "macmd5" }, "user": { "yob": 1991, "gender": "F", - "keywords": "Hotels, Travelling" - }, - "regs": { + "keywords": "Hotels, Travelling", "ext": { - "us_privacy": "1YNN" + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" } }, "at": 1, @@ -266,6 +280,12 @@ "cur": [ "USD" ], + "regs": { + "ext": { + "gdpr": 1, + "us_privacy": "1YNN" + } + }, "ext": { "prebid": { "targeting": { @@ -279,11 +299,26 @@ "precision": 2 }, "includewinners": true, - "includebidderkeys": true + "includebidderkeys": true, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "with_category": true + } }, "cache": { "vastxml": {} + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/video" } + }, + "appnexus": { + "include_brand_category": true, + "brand_category_uniqueness": true } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json index 991c4ddf098..67eae45f9ee 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-request-2.json @@ -26,8 +26,11 @@ } ], "site": { - "domain": "example.com", - "page": "prebid.com", + "domain": "prebid.com", + "page": "https://prebid.com", + "publisher": { + "domain": "prebid.com" + }, "content": { "episode": 6, "title": "episodeName", @@ -41,17 +44,28 @@ } }, "device": { - "ua": "userAgent", - "ip": "193.168.244.0" + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "dnt": 33, + "lmt": 44, + "ip": "123.145.167.0", + "devicetype": 1, + "os": "mac os", + "h": 480, + "w": 640, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "didsha1": "didsha1", + "didmd5": "didmd5", + "dpidsha1": "dpidsha1", + "dpidmd5": "dpidmd5", + "macsha1": "macsha1", + "macmd5": "macmd5" }, "user": { "yob": 1991, "gender": "F", - "keywords": "Hotels, Travelling" - }, - "regs": { + "keywords": "Hotels, Travelling", "ext": { - "us_privacy": "1YNN" + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" } }, "at": 1, @@ -59,6 +73,12 @@ "cur": [ "USD" ], + "regs": { + "ext": { + "gdpr": 1, + "us_privacy": "1YNN" + } + }, "ext": { "prebid": { "targeting": { @@ -72,11 +92,26 @@ "precision": 2 }, "includewinners": true, - "includebidderkeys": true + "includebidderkeys": true, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "with_category": true + } }, "cache": { "vastxml": {} + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/video" } + }, + "appnexus": { + "include_brand_category": true, + "brand_category_uniqueness": true } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-response-1.json index 32918264dbe..ba91140a988 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-response-1.json @@ -21,7 +21,7 @@ "brand_id": 1, "auction_id": 8189378542222915032, "bidder_id": 2, - "bid_ad_type": 0, + "bid_ad_type": 1, "ranking_price": 0.000000 } } @@ -43,7 +43,7 @@ "brand_id": 1, "auction_id": 8189378542222915032, "bidder_id": 2, - "bid_ad_type": 0, + "bid_ad_type": 1, "ranking_price": 0.000000 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-response-2.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-response-2.json index dca41a64f02..0133a69bdfa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-response-2.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-bid-response-2.json @@ -21,7 +21,7 @@ "brand_id": 1, "auction_id": 8189378542222915032, "bidder_id": 2, - "bid_ad_type": 0, + "bid_ad_type": 1, "ranking_price": 0.000000 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json index 8c7abb10755..6dd7ce977f0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-request.json @@ -20,16 +20,11 @@ ] }, "site": { - "page": "prebid.com" + "page": "https://prebid.com" }, "user": { - "buyeruids": { - "appnexus": "unique_id_an", - "rubicon": "unique_id_rubi" - }, - "gdpr": { - "consentrequired": false, - "consentstring": "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA" + "ext": { + "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA" }, "yob": 1991, "gender": "F", @@ -37,11 +32,12 @@ }, "regs": { "ext": { + "gdpr": 1, "us_privacy": "1YNN" } }, "tmax": 5000, - "device11": { + "device": { "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", "ip": "123.145.167.10", "devicetype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-response-empty.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-response-empty.json deleted file mode 100644 index b773ce30155..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-appnexus-response-empty.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "adPods": [] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-cache-request.json b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-cache-request.json index 43d4dd4080b..dd17797a98c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-cache-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/video/test-video-cache-request.json @@ -2,15 +2,18 @@ "puts": [ { "type": "xml", - "value": "some-test-ad-3" + "value": "some-test-ad-3", + "aid": "bid_id" }, { "type": "xml", - "value": "some-test-ad" + "value": "some-test-ad", + "aid": "bid_id" }, { "type": "xml", - "value": "some-test-ad-2" + "value": "some-test-ad-2", + "aid": "bid_id" } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-request.json index a39a7602e77..1aaa81eb1bb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "test-imp-id", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "secure": 1, "ext": { @@ -26,58 +22,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-response.json index 4cca135f21c..1364e8443c0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-response.json @@ -1,43 +1,25 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "tid", - "impid": "test-imp-id", + "id": "request_id", + "impid": "imp_id", "price": 0.5, "adm": "some-test-ad", "adomain": [ "goodadvertiser.com" ], - "crid": "11_222222", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "0.50", - "hb_cache_path_visx": "{{ cache.path }}", - "hb_cache_path": "{{ cache.path }}", - "hb_size_visx": "300x250", - "hb_bidder_visx": "visx", - "hb_size": "300x250", - "hb_bidder": "visx", - "hb_cache_id": "a5d3a873-d06e-4f2f-8556-120e05d62b28", - "hb_cache_host": "{{ cache.host }}", - "hb_pb_visx": "0.50", - "hb_cache_id_visx": "a5d3a873-d06e-4f2f-8556-120e05d62b28", - "hb_cache_host_visx": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}a5d3a873-d06e-4f2f-8556-120e05d62b28", - "cacheId": "a5d3a873-d06e-4f2f-8556-120e05d62b28" - } - } - } + "type": "banner" + }, + "origbidcpm": 0.5, + "origbidcur": "USD" } } ], @@ -48,11 +30,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "cache": "{{ cache.response_time_ms }}", "visx": "{{ visx.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-cache-visx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-cache-visx-request.json deleted file mode 100644 index 8ee5c82733f..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-cache-visx-request.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "tid", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "adomain": [ - "goodadvertiser.com" - ], - "crid": "11_222222", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-cache-visx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-cache-visx-response.json deleted file mode 100644 index 773491fa9b0..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-cache-visx-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "a5d3a873-d06e-4f2f-8556-120e05d62b28" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-request.json index fcee8a54f73..a339d89e82a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "test-imp-id", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "secure": 1, "ext": { @@ -27,10 +23,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -38,62 +34,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "VISX-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-response.json index 7d395fcf7e9..8c365d4d88a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-visx-bid-response.json @@ -1,19 +1,19 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { "price": 0.500000, "adm": "some-test-ad", - "impid": "test-imp-id", + "impid": "imp_id", "auid": 46, - "id": "1", + "id": "bid_id", "h": 250, "adomain": [ "goodadvertiser.com" ], - "crid": "11_222222", + "crid": "crid", "w": 300 } ], diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-request.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-request.json index c29f63cbb11..f3296494258 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "vrtcal": { @@ -18,55 +14,9 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, "app": { }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-response.json index 36b4155213f..6ea16fdd69e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-response.json @@ -1,42 +1,21 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 1.25, "adm": "adm001", - "crid": "crid001", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_env": "mobile-app", - "hb_env_vrtcal": "mobile-app", - "hb_cache_path_vrtcal": "{{ cache.path }}", - "hb_size_vrtcal": "300x250", - "hb_cache_id_vrtcal": "6e2bfba0-8ff2-4ed8-8a2c-ce1a7ca4f599", - "hb_cache_id": "6e2bfba0-8ff2-4ed8-8a2c-ce1a7ca4f599", - "hb_pb": "1.20", - "hb_cache_path": "{{ cache.path }}", - "hb_bidder_vrtcal": "vrtcal", - "hb_size": "300x250", - "hb_bidder": "vrtcal", - "hb_cache_host_vrtcal": "{{ cache.host }}", - "hb_pb_vrtcal": "1.20", - "hb_cache_host": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}6e2bfba0-8ff2-4ed8-8a2c-ce1a7ca4f599", - "cacheId": "6e2bfba0-8ff2-4ed8-8a2c-ce1a7ca4f599" - } - } - } + "type": "banner" + }, + "origbidcpm": 1.25 } } ], @@ -47,11 +26,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "vrtcal": "{{ vrtcal.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "vrtcal": "{{ vrtcal.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-cache-vrtcal-request.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-cache-vrtcal-request.json deleted file mode 100644 index 14a96320420..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-cache-vrtcal-request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "adm": "adm001", - "crid": "crid001", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-cache-vrtcal-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-cache-vrtcal-response.json deleted file mode 100644 index 43141301133..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-cache-vrtcal-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "6e2bfba0-8ff2-4ed8-8a2c-ce1a7ca4f599" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-request-1.json deleted file mode 100644 index 4437d138db1..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-request-1.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId001", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "bidder": { - "Just_an_unused_vrtcal_param": "unused_data" - } - } - } - ], - "app": {}, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "VR-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "app" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-request.json new file mode 100644 index 00000000000..ee34878c297 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-request.json @@ -0,0 +1,32 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "Just_an_unused_vrtcal_param": "unused_data" + } + } + } + ], + "app": {}, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-response-1.json deleted file mode 100644 index 89ec3dc2248..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-response-1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid001", - "impid": "impId001", - "price": 1.25, - "crid": "crid001", - "adm": "adm001", - "h": 250, - "w": 300 - } - ] - } - ], - "bidid": "bid001" -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-response.json new file mode 100644 index 00000000000..260fde09efd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-vrtcal-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ], + "bidid": "bid_id" +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-request.json index ee088e56217..746b631505e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "native": { "request": "{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":2,\"required\":1,\"title\":{\"len\":90}},{\"id\":6,\"required\":1,\"img\":{\"type\":3,\"wmin\":128,\"hmin\":128,\"mimes\":[\"image/jpg\",\"image/jpeg\",\"image/png\"]}},{\"id\":7,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", @@ -24,80 +20,10 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "ip": "193.168.244.1", - "language": "en", - "ifa": "ifaId", - "ua": "userAgent" - }, - "site": { - "domain": "example.com", - "ext": { - "amp": 0 - }, - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - } - }, - "at": 1, - "tmax": 3000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, + "tmax": 5000, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "includebidderkeys": true, - "includewinners": true, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-response.json index b97993ef7d9..e7a58f7264d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-response.json @@ -1,40 +1,21 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "1", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 0.01, "adm": "hi", - "cid": "test_cid", - "crid": "test_banner_crid", + "cid": "cid", + "crid": "crid", "ext": { - "bidder": { - "format": "BANNER" - }, + "format": "BANNER", "prebid": { - "type": "banner", - "targeting": { - "hb_bidder": "yeahmobi", - "hb_bidder_yeahmobi": "yeahmobi", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_yeahmobi": "{{ cache.host }}", - "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_id_yeahmobi": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_yeahmobi": "{{ cache.path }}", - "hb_pb": "0.00", - "hb_pb_yeahmobi": "0.00" - }, - "cache": { - "bids": { - "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - } - } + "type": "banner" + }, + "origbidcpm": 0.01 } } ], @@ -45,12 +26,11 @@ "cur": "USD", "ext": { "responsetimemillis": { - "cache": "{{ cache.response_time_ms }}", "yeahmobi": "{{ yeahmobi.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, - "tmaxrequest": 3000 + "tmaxrequest": 5000 } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-cache-yeahmobi-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-cache-yeahmobi-request.json deleted file mode 100644 index c6692479e4a..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-cache-yeahmobi-request.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "1", - "impid": "impId001", - "price": 0.01, - "adm": "hi", - "cid": "test_cid", - "crid": "test_banner_crid", - "ext": { - "format": "BANNER" - } - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-cache-yeahmobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-cache-yeahmobi-response.json deleted file mode 100644 index 93d0b8de2cd..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-cache-yeahmobi-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-request.json index 4ff2144a2bc..ab6e8b9d61f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "native": { "request": "{\"native\":{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":2,\"required\":1,\"title\":{\"len\":90}},{\"id\":6,\"required\":1,\"img\":{\"type\":3,\"wmin\":128,\"hmin\":128,\"mimes\":[\"image/jpg\",\"image/jpeg\",\"image/png\"]}},{\"id\":7,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}}" @@ -24,10 +20,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -35,72 +31,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "YM-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, - "tmax": 3000, + "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-response.json index 455a146256a..2e1b7e91e69 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-yeahmobi-bid-response.json @@ -1,14 +1,14 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { "adm": "hi", - "crid": "test_banner_crid", - "cid": "test_cid", - "impid": "impId001", - "id": "1", + "crid": "crid", + "cid": "cid", + "impid": "imp_id", + "id": "bid_id", "price": 0.01, "ext": { "format": "BANNER" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json index 1b1f1059fed..a8c959b1b91 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-request.json @@ -1,98 +1,34 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { - "yieldlab": { - "adslotId": "12345", - "supplyId": "123456789", - "adSize": "400x300", - "targeting": { - "key1": "value1", - "key2": "value2" - }, - "extId": "abc" + "prebid": { + "bidder": { + "yieldlab": { + "adslotId": "12345", + "supplyId": "123456789", + "adSize": "400x300", + "targeting": { + "key1": "value1", + "key2": "value2" + }, + "extId": "abc" + } + } } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId", - "devicetype": 4, - "connectiontype": 6, - "geo": { - "lat": 51.499488, - "lon": -0.128953 - }, - "ua": "userAgent", - "ip": "193.168.244.1", - "h": 1098, - "w": 814 - }, - "site": { - "id": "siteId", - "publisher": { - "id": "publisherId" - }, - "page": "http://localhost:9090/gdpr.html" - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "debug": 1, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - }, - "consent": "consentValue" - } - }, "regs": { "ext": { "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json index 4b56af12471..4c7d6e6623e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json @@ -1,42 +1,22 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { "id": "12345", - "impid": "impId001", + "impid": "imp_id", "price": 2.29, - "adm": "", - "crid": "12345123435", + "crid": "", "dealid": "1234", "w": 400, "h": 300, + "adm": "", "ext": { + "origbidcpm": 2.01, + "origbidcur": "EUR", "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "2.20", - "hb_size_yieldlab": "400x300", - "hb_size": "400x300", - "hb_bidder": "yieldlab", - "hb_cache_id_yieldlab": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce", - "hb_cache_id": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce", - "hb_bidder_yieldlab": "yieldlab", - "hb_pb_yieldlab": "2.20", - "hb_deal": "1234", - "hb_deal_yieldlab": "1234", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_host_yieldlab": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_cache_path_yieldlab": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}ca2a4dd3-f974-4eff-be5c-986bf4e083ce", - "cacheId": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce" - } - } + "type": "banner" } } } @@ -47,139 +27,11 @@ ], "cur": "USD", "ext": { - "debug": { - "httpcalls": { - "cache": [ - { - "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"12345\",\"impid\":\"impId001\",\"price\":2.29,\"adm\":\"\",\"crid\":\"12345123435\",\"dealid\":\"1234\",\"w\":400,\"h\":300}}]}", - "responsebody": "{\"responses\":[{\"uuid\":\"ca2a4dd3-f974-4eff-be5c-986bf4e083ce\"}]}", - "status": 200, - "uri": "{{ cache.endpoint }}" - } - ], - "yieldlab": [ - { - "responsebody": "[{\"id\":12345,\"price\":201,\"advertiser\":\"yieldlab\",\"adsize\":\"400x300\",\"pid\":1234,\"did\":5678,\"pvid\":\"40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5\"}]", - "status": 200, - "uri": "{{ yieldlab.exchange_uri }}/12345?content=json&pvid=true&ts=200000&t=key1%3Dvalue1%26key2%3Dvalue2&ids=YL-UID&yl_rtb_ifa=ifaId&yl_rtb_devicetype=4&yl_rtb_connectiontype=6&lat=51.49949&lon=-0.128953&gdpr=0&consent=consentValue" - } - ] - }, - "resolvedrequest": { - "at": 1, - "cur": [ - "USD" - ], - "device": { - "connectiontype": 6, - "devicetype": 4, - "dnt": 2, - "geo": { - "lat": 51.49949, - "lon": -0.128953 - }, - "h": 1098, - "ifa": "ifaId", - "ip": "193.168.244.1", - "language": "en", - "pxratio": 4.2, - "ua": "userAgent", - "w": 814 - }, - "ext": { - "prebid": { - "auctiontimestamp": 1000, - "channel": { - "name": "web" - }, - "cache": { - "bids": { - }, - "vastxml": { - "ttlseconds": 120 - } - }, - "debug": 1, - "targeting": { - "includebidderkeys": true, - "includewinners": true, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "increment": 0.1, - "max": 20 - } - ] - } - } - } - }, - "id": "tid", - "imp": [ - { - "banner": { - "format": [ - { - "h": 250, - "w": 300 - } - ] - }, - "ext": { - "yieldlab": { - "adSize": "400x300", - "adslotId": "12345", - "extId": "abc", - "supplyId": "123456789", - "targeting": { - "key1": "value1", - "key2": "value2" - } - } - }, - "id": "impId001" - } - ], - "regs": { - "ext": { - "gdpr": 0 - } - }, - "site": { - "domain": "example.com", - "ext": { - "amp": 0 - }, - "id": "siteId", - "page": "http://localhost:9090/gdpr.html", - "publisher": { - "id": "publisherId" - } - }, - "source": { - "fd": 1, - "tid": "tid" - }, - "tmax": 5000, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - } - } - }, "responsetimemillis": { - "yieldlab": "{{ yieldlab.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "yieldlab": "{{ yieldlab.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-cache-yieldlab-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-cache-yieldlab-request.json deleted file mode 100644 index aeb6eb7cbc2..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-cache-yieldlab-request.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "12345", - "impid": "impId001", - "price": 2.29, - "adm": "", - "crid": "12345123435", - "dealid": "1234", - "w": 400, - "h": 300 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-cache-yieldlab-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-cache-yieldlab-response.json deleted file mode 100644 index a1adc7415cc..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-cache-yieldlab-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "ca2a4dd3-f974-4eff-be5c-986bf4e083ce" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-request.json index 25cfb2c200d..bae3f4c266c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "yieldmo": { @@ -18,58 +14,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-response.json index 36386f57789..1a0e67d6d93 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-response.json @@ -1,42 +1,23 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 3.33, "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", + "adid": "adid", + "cid": "cid", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_bidder_yieldmo": "yieldmo", - "hb_size_yieldmo": "300x250", - "hb_cache_id_yieldmo": "a5d3a873-d06e-4f2f-8556-120e05d62b28", - "hb_cache_id": "a5d3a873-d06e-4f2f-8556-120e05d62b28", - "hb_pb": "3.30", - "hb_cache_host_yieldmo": "{{ cache.host }}", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_bidder": "yieldmo", - "hb_pb_yieldmo": "3.30", - "hb_cache_host": "{{ cache.host }}", - "hb_cache_path_yieldmo": "{{ cache.path }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}a5d3a873-d06e-4f2f-8556-120e05d62b28", - "cacheId": "a5d3a873-d06e-4f2f-8556-120e05d62b28" - } - } - } + "type": "banner" + }, + "origbidcpm": 3.33 } } ], @@ -47,11 +28,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "cache": "{{ cache.response_time_ms }}", "yieldmo": "{{ yieldmo.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-cache-yieldmo-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-cache-yieldmo-request.json deleted file mode 100644 index 60845624d51..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-cache-yieldmo-request.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 3.33, - "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", - "w": 300, - "h": 250 - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-cache-yieldmo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-cache-yieldmo-response.json deleted file mode 100644 index 773491fa9b0..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-cache-yieldmo-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "a5d3a873-d06e-4f2f-8556-120e05d62b28" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request-1.json deleted file mode 100644 index 2592271d1c0..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request-1.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId001", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "placement_id": "123" - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "YM-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request.json new file mode 100644 index 00000000000..562e34cea6c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "placement_id": "123" + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-response-1.json deleted file mode 100644 index 95a93284e04..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-response-1.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid001", - "impid": "impId001", - "price": 3.33, - "adid": "adid001", - "crid": "crid001", - "cid": "cid001", - "adm": "adm001", - "h": 250, - "w": 300 - } - ] - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-response.json new file mode 100644 index 00000000000..8901429253e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid", + "crid": "crid", + "cid": "cid", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-request.json index ce0f887b9df..338133d0595 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-request.json @@ -1,15 +1,11 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] + "w": 300, + "h": 250 }, "ext": { "yieldone": { @@ -18,58 +14,7 @@ } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-response.json index 0eebe0f049e..791bfa7a4de 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-response.json @@ -1,42 +1,23 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 3.33, "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", + "adid": "adid", + "cid": "cid", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "3.30", - "hb_size_yieldone": "300x250", - "hb_bidder_yieldone": "yieldone", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_cache_host_yieldone": "{{ cache.host }}", - "hb_cache_path_yieldone": "{{ cache.path }}", - "hb_cache_id_yieldone": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_bidder": "yieldone", - "hb_cache_id": "f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "hb_pb_yieldone": "3.30", - "hb_cache_host": "{{ cache.host }}" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}f0ab9105-cb21-4e59-b433-70f5ad6671cb", - "cacheId": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - } - } + "type": "banner" + }, + "origbidcpm": 3.33 } } ], @@ -47,11 +28,10 @@ "cur": "USD", "ext": { "responsetimemillis": { - "yieldone": "{{ yieldone.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "yieldone": "{{ yieldone.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-cache-yieldone-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-cache-yieldone-request.json deleted file mode 100644 index 23ddbec96e5..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-cache-yieldone-request.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 3.33, - "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", - "w": 300, - "h": 250 - } - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-cache-yieldone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-cache-yieldone-response.json deleted file mode 100644 index 1112a7b5a6a..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-cache-yieldone-response.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "responses": [ - { - "uuid": "f0ab9105-cb21-4e59-b433-70f5ad6671cb" - } - ] -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-request.json index 97ac27767a4..54b83d07c7f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-request.json @@ -1,15 +1,9 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], "w": 300, "h": 250 }, @@ -21,10 +15,10 @@ } ], "site": { - "domain": "example.com", + "domain": "www.example.com", "page": "http://www.example.com", "publisher": { - "id": "publisherId" + "domain": "example.com" }, "ext": { "amp": 0 @@ -32,62 +26,16 @@ }, "device": { "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "YD-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } + "ip": "193.168.244.1" }, "at": 1, "tmax": 5000, "cur": [ "USD" ], - "source": { - "fd": 1, - "tid": "tid" - }, "regs": { "ext": { "gdpr": 0 } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } } } \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-response.json index 95a93284e04..8901429253e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-yieldone-bid-response.json @@ -1,15 +1,15 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 3.33, - "adid": "adid001", - "crid": "crid001", - "cid": "cid001", + "adid": "adid", + "crid": "crid", + "cid": "cid", "adm": "adm001", "h": 250, "w": 300 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json index 0f1f2f26a0c..bf17764c588 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-request.json @@ -1,107 +1,28 @@ { - "id": "tid", + "id": "request_id", "imp": [ { - "id": "impId001", + "id": "imp_id", "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "zeroclickfraud": { - "host": "localhost:8090", - "sourceId": 1 - } - } - }, - { - "id": "impId002", - "video": { "w": 300, - "h": 250, - "pos": 1, - "mimes": [ - "video/mp4" - ] + "h": 250 }, "ext": { - "zeroclickfraud": { - "host": "localhost:8090", - "sourceId": 2 + "prebid": { + "bidder": { + "zeroclickfraud": { + "host": "localhost:8090", + "sourceId": 1 + } + } } } } ], - "device": { - "pxratio": 4.2, - "dnt": 2, - "language": "en", - "ifa": "ifaId" - }, - "site": { - "publisher": { - "id": "publisherId" - } - }, - "at": 1, "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "ext": { - "prebid": { - "debug": 1, - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - } - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000 - } - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, "regs": { "ext": { "gdpr": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json index 7152fb90f16..5fea726a00a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json @@ -1,85 +1,23 @@ { - "id": "tid", + "id": "request_id", "seatbid": [ { "bid": [ { - "id": "bid002", - "impid": "impId002", - "price": 9.99, - "adomain": [ - "psacentral.org" - ], - "cid": "cid002", - "crid": "crid002", - "w": 300, - "h": 250, - "ext": { - "prebid": { - "type": "video", - "targeting": { - "hb_cache_id_zeroclickfraud": "dbaa191c-5a56-4655-85eb-da079f94e09f", - "hb_bidder_zeroclickfraud": "zeroclickfraud", - "hb_cache_id": "dbaa191c-5a56-4655-85eb-da079f94e09f", - "hb_pb": "9.90", - "hb_cache_path_zeroclickfraud": "{{ cache.path }}", - "hb_cache_path": "{{ cache.path }}", - "hb_uuid": "62019cff-d657-42fc-8366-16c34e1fd28c", - "hb_size": "300x250", - "hb_cache_host_zeroclickfraud": "{{ cache.host }}", - "hb_size_zeroclickfraud": "300x250", - "hb_bidder": "zeroclickfraud", - "hb_uuid_zeroclickfraud": "62019cff-d657-42fc-8366-16c34e1fd28c", - "hb_cache_host": "{{ cache.host }}", - "hb_pb_zeroclickfraud": "9.90" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}dbaa191c-5a56-4655-85eb-da079f94e09f", - "cacheId": "dbaa191c-5a56-4655-85eb-da079f94e09f" - }, - "vastXml": { - "url": "{{ cache.resource_url }}62019cff-d657-42fc-8366-16c34e1fd28c", - "cacheId": "62019cff-d657-42fc-8366-16c34e1fd28c" - } - } - } - } - }, - { - "id": "bid001", - "impid": "impId001", + "id": "bid_id", + "impid": "imp_id", "price": 7.77, "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", + "adid": "adid", + "cid": "cid", + "crid": "crid", "w": 300, "h": 250, "ext": { "prebid": { - "type": "banner", - "targeting": { - "hb_pb": "7.70", - "hb_cache_path_zeroclickfraud": "{{ cache.path }}", - "hb_cache_id_zeroclickfraud": "c1662cf6-f00a-4066-b71a-46d97abccc35", - "hb_cache_path": "{{ cache.path }}", - "hb_size": "300x250", - "hb_cache_host_zeroclickfraud": "{{ cache.host }}", - "hb_size_zeroclickfraud": "300x250", - "hb_bidder_zeroclickfraud": "zeroclickfraud", - "hb_bidder": "zeroclickfraud", - "hb_cache_id": "c1662cf6-f00a-4066-b71a-46d97abccc35", - "hb_cache_host": "{{ cache.host }}", - "hb_pb_zeroclickfraud": "7.70" - }, - "cache": { - "bids": { - "url": "{{ cache.resource_url }}c1662cf6-f00a-4066-b71a-46d97abccc35", - "cacheId": "c1662cf6-f00a-4066-b71a-46d97abccc35" - } - } - } + "type": "banner" + }, + "origbidcpm": 7.77 } } ], @@ -89,157 +27,11 @@ ], "cur": "USD", "ext": { - "debug": { - "httpcalls": { - "cache": [ - { - "uri": "{{ cache.endpoint }}", - "requestbody": "{\"puts\":[{\"type\":\"json\",\"value\":{\"id\":\"bid001\",\"impid\":\"impId001\",\"price\":7.77,\"adm\":\"adm001\",\"adid\":\"adid001\",\"cid\":\"cid001\",\"crid\":\"crid001\",\"w\":300,\"h\":250}},{\"type\":\"json\",\"value\":{\"id\":\"bid002\",\"impid\":\"impId002\",\"price\":9.99,\"adomain\":[\"psacentral.org\"],\"cid\":\"cid002\",\"crid\":\"crid002\",\"w\":300,\"h\":250}},{\"type\":\"xml\",\"value\":\"prebid.org wrapper\",\"expiry\":120}]}", - "responsebody": "{\"responses\":[{\"uuid\":\"c1662cf6-f00a-4066-b71a-46d97abccc35\"},{\"uuid\":\"dbaa191c-5a56-4655-85eb-da079f94e09f\"},{\"uuid\":\"62019cff-d657-42fc-8366-16c34e1fd28c\"}]}", - "status": 200 - } - ], - "zeroclickfraud": [ - { - "uri": "{{ zeroclickfraud.exchange_uri }}?sid=2", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId002\",\"video\":{\"mimes\":[\"video/mp4\"],\"w\":300,\"h\":250,\"pos\":1},\"ext\":{\"bidder\":{\"host\":\"localhost:8090\",\"sourceId\":2}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"ZF-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"currency\":{\"rates\":{\"EUR\":{\"USD\":1.2406},\"USD\":{\"EUR\":0.811}}},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", - "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"bid002\",\"impid\":\"impId002\",\"price\":9.99,\"crid\":\"crid002\",\"cid\":\"cid002\",\"adomain\":[\"psacentral.org\"],\"h\":250,\"w\":300}],\"seat\":\"zeroclickfraud\"}]}", - "status": 200 - }, - { - "uri": "{{ zeroclickfraud.exchange_uri }}?sid=1", - "requestbody": "{\"id\":\"tid\",\"imp\":[{\"id\":\"impId001\",\"banner\":{\"format\":[{\"w\":300,\"h\":250}]},\"ext\":{\"bidder\":{\"host\":\"localhost:8090\",\"sourceId\":1}}}],\"site\":{\"domain\":\"example.com\",\"page\":\"http://www.example.com\",\"publisher\":{\"id\":\"publisherId\"},\"ext\":{\"amp\":0}},\"device\":{\"ua\":\"userAgent\",\"dnt\":2,\"ip\":\"193.168.244.1\",\"pxratio\":4.2,\"language\":\"en\",\"ifa\":\"ifaId\"},\"user\":{\"buyeruid\":\"ZF-UID\",\"ext\":{\"consent\":\"consentValue\",\"digitrust\":{\"id\":\"id\",\"keyv\":123,\"pref\":0}}},\"at\":1,\"tmax\":5000,\"cur\":[\"USD\"],\"source\":{\"fd\":1,\"tid\":\"tid\"},\"regs\":{\"ext\":{\"gdpr\":0}},\"ext\":{\"prebid\":{\"debug\":1,\"currency\":{\"rates\":{\"EUR\":{\"USD\":1.2406},\"USD\":{\"EUR\":0.811}}},\"targeting\":{\"pricegranularity\":{\"precision\":2,\"ranges\":[{\"max\":20,\"increment\":0.1}]},\"includewinners\":true,\"includebidderkeys\":true},\"cache\":{\"bids\":{},\"vastxml\":{\"ttlseconds\":120}},\"auctiontimestamp\":1000,\"channel\":{\"name\":\"web\"}}}}", - "responsebody": "{\"id\":\"tid\",\"seatbid\":[{\"bid\":[{\"id\":\"bid001\",\"impid\":\"impId001\",\"price\":7.77,\"adid\":\"adid001\",\"crid\":\"crid001\",\"cid\":\"cid001\",\"adm\":\"adm001\",\"h\":250,\"w\":300}],\"seat\":\"zeroclickfraud\"}]}", - "status": 200 - } - ] - }, - "resolvedrequest": { - "id": "tid", - "imp": [ - { - "id": "impId001", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "zeroclickfraud": { - "host": "localhost:8090", - "sourceId": 1 - } - } - }, - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "w": 300, - "h": 250, - "pos": 1 - }, - "ext": { - "zeroclickfraud": { - "host": "localhost:8090", - "sourceId": 2 - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "debug": 1, - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.811 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } - } - }, "responsetimemillis": { - "zeroclickfraud": "{{ zeroclickfraud.response_time_ms }}", - "cache": "{{ cache.response_time_ms }}" + "zeroclickfraud": "{{ zeroclickfraud.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 1000 + "auctiontimestamp": 0 }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-cache-zeroclickfraud-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-cache-zeroclickfraud-request.json deleted file mode 100644 index c76c7256d07..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-cache-zeroclickfraud-request.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "puts": [ - { - "type": "json", - "value": { - "id": "bid001", - "impid": "impId001", - "price": 7.77, - "adm": "adm001", - "adid": "adid001", - "cid": "cid001", - "crid": "crid001", - "w": 300, - "h": 250 - } - }, - { - "type": "json", - "value": { - "id": "bid002", - "impid": "impId002", - "price": 9.99, - "adomain": [ - "psacentral.org" - ], - "cid": "cid002", - "crid": "crid002", - "w": 300, - "h": 250 - } - }, - { - "type": "xml", - "value": "prebid.org wrapper", - "expiry": 120 - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-cache-zeroclickfraud-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-cache-zeroclickfraud-response.json deleted file mode 100644 index babbed58d14..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-cache-zeroclickfraud-response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "responses": [ - { - "uuid": "c1662cf6-f00a-4066-b71a-46d97abccc35" - }, - { - "uuid": "dbaa191c-5a56-4655-85eb-da079f94e09f" - }, - { - "uuid": "62019cff-d657-42fc-8366-16c34e1fd28c" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-1.json deleted file mode 100644 index e60f0063394..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-1.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId001", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "bidder": { - "host": "localhost:8090", - "sourceId": 1 - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "ZF-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "debug": 1, - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-2.json deleted file mode 100644 index 3129406631f..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request-2.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "id": "tid", - "imp": [ - { - "id": "impId002", - "video": { - "mimes": [ - "video/mp4" - ], - "w": 300, - "h": 250, - "pos": 1 - }, - "ext": { - "bidder": { - "host": "localhost:8090", - "sourceId": 2 - } - } - } - ], - "site": { - "domain": "example.com", - "page": "http://www.example.com", - "publisher": { - "id": "publisherId" - }, - "ext": { - "amp": 0 - } - }, - "device": { - "ua": "userAgent", - "dnt": 2, - "ip": "193.168.244.1", - "pxratio": 4.2, - "language": "en", - "ifa": "ifaId" - }, - "user": { - "buyeruid": "ZF-UID", - "ext": { - "consent": "consentValue", - "digitrust": { - "id": "id", - "keyv": 123, - "pref": 0 - } - } - }, - "at": 1, - "tmax": 5000, - "cur": [ - "USD" - ], - "source": { - "fd": 1, - "tid": "tid" - }, - "regs": { - "ext": { - "gdpr": 0 - } - }, - "ext": { - "prebid": { - "debug": 1, - "currency": { - "rates": { - "EUR": { - "USD": 1.2406 - }, - "USD": { - "EUR": 0.8110 - } - } - }, - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true - }, - "cache": { - "bids": {}, - "vastxml": { - "ttlseconds": 120 - } - }, - "auctiontimestamp": 1000, - "channel": { - "name": "web" - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request.json new file mode 100644 index 00000000000..51b2d3ef867 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "host": "localhost:8090", + "sourceId": 1 + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response-1.json deleted file mode 100644 index 2939a729da3..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response-1.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid001", - "impid": "impId001", - "price": 7.77, - "adid": "adid001", - "crid": "crid001", - "cid": "cid001", - "adm": "adm001", - "h": 250, - "w": 300 - } - ], - "seat": "zeroclickfraud" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response-2.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response-2.json deleted file mode 100644 index f6b7a00684a..00000000000 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response-2.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "id": "tid", - "seatbid": [ - { - "bid": [ - { - "id": "bid002", - "impid": "impId002", - "price": 9.99, - "crid": "crid002", - "cid": "cid002", - "adomain": [ - "psacentral.org" - ], - "h": 250, - "w": 300 - } - ], - "seat": "zeroclickfraud" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response.json new file mode 100644 index 00000000000..c957fc72f91 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-zeroclickfraud-bid-response.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 7.77, + "adid": "adid", + "crid": "crid", + "cid": "cid", + "adm": "adm001", + "h": 250, + "w": 300 + } + ], + "seat": "zeroclickfraud" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/storedimps/test-rubicon-stored-video-cache-update.json b/src/test/resources/org/prebid/server/it/storedimps/test-rubicon-stored-video-cache-update.json index 2c07db85a8c..39002f2ebfc 100644 --- a/src/test/resources/org/prebid/server/it/storedimps/test-rubicon-stored-video-cache-update.json +++ b/src/test/resources/org/prebid/server/it/storedimps/test-rubicon-stored-video-cache-update.json @@ -13,7 +13,9 @@ "skipafter": 0, "skipmin": 0, "startdelay": 0, - "playbackmethod": [1] + "playbackmethod": [ + 1 + ] }, "ext": { "rubicon": { diff --git a/src/test/resources/org/prebid/server/it/storedimps/test-video-stored-imp-request-1.json b/src/test/resources/org/prebid/server/it/storedimps/test-video-stored-imp-request-1.json index ed4c508df98..3fd2300c22e 100644 --- a/src/test/resources/org/prebid/server/it/storedimps/test-video-stored-imp-request-1.json +++ b/src/test/resources/org/prebid/server/it/storedimps/test-video-stored-imp-request-1.json @@ -1 +1,7 @@ -{"ext": {"appnexus": {"placementId": 14997137}}} +{ + "ext": { + "appnexus": { + "placementId": 14997137 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/storedimps/test-video-stored-imp-request-2.json b/src/test/resources/org/prebid/server/it/storedimps/test-video-stored-imp-request-2.json index 920c894bca1..47ef883f74f 100644 --- a/src/test/resources/org/prebid/server/it/storedimps/test-video-stored-imp-request-2.json +++ b/src/test/resources/org/prebid/server/it/storedimps/test-video-stored-imp-request-2.json @@ -1 +1,7 @@ -{"ext": {"appnexus": {"placementId": 15016213}}} +{ + "ext": { + "appnexus": { + "placementId": 15016213 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/storedrequests/test-amp-stored-request.json b/src/test/resources/org/prebid/server/it/storedrequests/test-amp-stored-request.json index 890d960a32a..3d99416d3bf 100644 --- a/src/test/resources/org/prebid/server/it/storedrequests/test-amp-stored-request.json +++ b/src/test/resources/org/prebid/server/it/storedrequests/test-amp-stored-request.json @@ -64,7 +64,7 @@ "cache": { "bids": {} }, - "auctiontimestamp": 1000, + "auctiontimestamp": 0, "adservertargeting": [ { "key": "static_keyword1", diff --git a/src/test/resources/org/prebid/server/it/storedresponses/test-stored-bid-response.json b/src/test/resources/org/prebid/server/it/storedresponses/test-stored-bid-response.json index 621fb4a632f..3a3e7f5ba1a 100644 --- a/src/test/resources/org/prebid/server/it/storedresponses/test-stored-bid-response.json +++ b/src/test/resources/org/prebid/server/it/storedresponses/test-stored-bid-response.json @@ -1,14 +1,20 @@ -[ - { - "bid": [ - { - "id": "f227a07f-1579-4465-bc5e-5c5b02a0c181", - "impid": "impStoredBidResponse", - "crid": "crid1", - "price": 0.8 - } - ], - "seat": "rubicon", - "group": 0 - } -] +{ + "id": "storedResponseId", + "seatbid": [ + { + "bid": [ + { + "id": "466223845", + "impid": "impStoredBidResponse", + "price": 0.8, + "adm": "adm1", + "crid": "crid1", + "w": 300, + "h": 250 + } + ], + "seat": "rubicon", + "group": 0 + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/test-app-settings.yaml b/src/test/resources/org/prebid/server/it/test-app-settings.yaml index 80005f45c2f..afdaf7e7902 100644 --- a/src/test/resources/org/prebid/server/it/test-app-settings.yaml +++ b/src/test/resources/org/prebid/server/it/test-app-settings.yaml @@ -1,27 +1,123 @@ --- accounts: - id: 14062 - bannerCacheTtl: 100 - videoCacheTtl: 100 - eventsEnabled: true - analyticsSamplingFactor: 1 + auction: + banner-cache-ttl: 100 + video-cache-ttl: 100 + events: + enabled: true - id: 1001 + - id: 2001 + auction: + events: + enabled: true - id: 2763 - id: 5001 - eventsEnabled: true -configs: - - id: 14062 - config: > - [{"bid_id": "bidId2", - "bidder": "rubicon", - "params": { - "accountId": 5001, - "siteId": 6001, - "zoneId": 7001 - } - }] - - id: 1001 - - id: 2763 + auction: + events: + enabled: true + - id: 6001 + hooks: + execution-plan: + endpoints: + /openrtb2/auction: + stages: + raw-auction-request: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: raw-auction-request + processed-auction-request: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: processed-auction-request + bidder-request: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: bidder-request + raw-bidder-response: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: raw-bidder-response + processed-bidder-response: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: processed-bidder-response + auction-response: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: auction-response + - id: 7001 + hooks: + execution-plan: + endpoints: + /openrtb2/auction: + stages: + raw-auction-request: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: rejecting-raw-auction-request + - id: 8001 + hooks: + execution-plan: + endpoints: + /openrtb2/auction: + stages: + processed-auction-request: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: rejecting-processed-auction-request + - id: 9001 + hooks: + execution-plan: + endpoints: + /openrtb2/auction: + stages: + bidder-request: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: rejecting-bidder-request + - id: 10001 + hooks: + execution-plan: + endpoints: + /openrtb2/auction: + stages: + raw-bidder-response: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: rejecting-raw-bidder-response + - id: 11001 + hooks: + execution-plan: + endpoints: + /openrtb2/auction: + stages: + processed-bidder-response: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: rejecting-processed-bidder-response domains: - rubiconproject.com - www.rubiconproject.com diff --git a/src/test/resources/org/prebid/server/it/test-application-hooks.properties b/src/test/resources/org/prebid/server/it/test-application-hooks.properties new file mode 100644 index 00000000000..bad3897609d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/test-application-hooks.properties @@ -0,0 +1,21 @@ +hooks.host-execution-plan={ \ + "endpoints": { \ + "/openrtb2/auction": { \ + "stages": { \ + "entrypoint": { \ + "groups": [ \ + { \ + "timeout": 5000, \ + "hook-sequence": [ \ + { \ + "module-code": "sample-it-module", \ + "hook-impl-code": "entrypoint" \ + } \ + ] \ + } \ + ] \ + } \ + } \ + } \ + } \ +} \ No newline at end of file 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 b1d01d1af2f..2a363f1ebe8 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -1,327 +1,256 @@ +adapters.acuityads.enabled=true +adapters.acuityads.endpoint=http://localhost:8090/acuityads-exchange +adapters.adf.enabled=true +adapters.adf.endpoint=http://localhost:8090/adf-exchange adapters.adform.enabled=true adapters.adform.endpoint=http://localhost:8090/adform-exchange -adapters.adform.pbs-enforces-gdpr=true -adapters.adform.usersync.url=//adform-usersync adapters.adgeneration.enabled=true adapters.adgeneration.endpoint=http://localhost:8090/adgeneration-exchange -adapters.adgeneration.pbs-enforces-gdpr=true -adapters.adgeneration.usersync.url=//adgeneration-usersync adapters.adhese.enabled=true adapters.adhese.endpoint=http://localhost:8090/adhese-exchange -adapters.adhese.pbs-enforces-gdpr=true -adapters.adhese.usersync.url=//adhese-usersync adapters.adkerneladn.enabled=true -adapters.adkerneladn.endpoint=http://localhost:8090/adkernelAdn-exchange?account= -adapters.adkerneladn.pbs-enforces-gdpr=true -adapters.adkerneladn.usersync.url=//adkernelAdn-usersync +adapters.adkerneladn.endpoint=http://localhost:8090/adkernelAdn.{{Host}}?account={{PublisherID}} adapters.adkernel.enabled=true adapters.adkernel.endpoint=http://%s/adkernel-exchange?zone=%s -adapters.adkernel.pbs-enforces-gdpr=true -adapters.adkernel.usersync.url=//adkernel-usersync adapters.adman.enabled=true adapters.adman.endpoint=http://localhost:8090/adman-exchange -adapters.adman.pbs-enforces-gdpr=true -adapters.adman.usersync.url=//adman-usersync adapters.admixer.enabled=true adapters.admixer.endpoint=http://localhost:8090/admixer-exchange -adapters.admixer.pbs-enforces-gdpr=true -adapters.admixer.usersync.url=//admixer-usersync adapters.adocean.enabled=true adapters.adocean.endpoint=http://localhost:8090/adocean-exchange -adapters.adocean.pbs-enforces-gdpr=true -adapters.adocean.usersync.url=//adocean-usersync adapters.adoppler.enabled=true adapters.adoppler.endpoint=http://localhost:8090/adoppler-exchange -adapters.adoppler.pbs-enforces-gdpr=true -adapters.adoppler.usersync.url=//adoppler-usersync adapters.adpone.enabled=true adapters.adpone.endpoint=http://localhost:8090/adpone-exchange -adapters.adpone.pbs-enforces-gdpr=true -adapters.adpone.usersync.url=//adpone-usersync +adapters.adot.enabled=true +adapters.adot.endpoint=http://localhost:8090/adot-exchange +adapters.adprime.enabled=true +adapters.adprime.endpoint=http://localhost:8090/adprime-exchange adapters.adtarget.enabled=true adapters.adtarget.endpoint=http://localhost:8090/adtarget-exchange -adapters.adtarget.pbs-enforces-gdpr=true -adapters.adtarget.usersync.url=//adtarget-usersync adapters.adtelligent.enabled=true adapters.adtelligent.endpoint=http://localhost:8090/adtelligent-exchange -adapters.adtelligent.pbs-enforces-gdpr=true -adapters.adtelligent.usersync.url=//adtelligent-usersync +adapters.adtelligent.aliases.mediafuse.enabled=true +adapters.adtelligent.aliases.viewdoes.enabled=true adapters.advangelists.enabled=true -adapters.advangelists.endpoint=http://localhost:8090/advangelists-exchange?pubid= -adapters.advangelists.pbs-enforces-gdpr=true -adapters.advangelists.usersync.url=//advangelists-usersync +adapters.advangelists.endpoint=http://localhost:8090/advangelists-exchange?pubid={{PublisherID}} +adapters.adxcg.enabled=true +adapters.adxcg.endpoint=http://localhost:8090/adxcg-exchange +adapters.adyoulike.enabled=true +adapters.adyoulike.endpoint=http://localhost:8090/adyoulike-exchange adapters.aja.enabled=true adapters.aja.endpoint=http://localhost:8090/aja -adapters.aja.pbs-enforces-gdpr=true -adapters.aja.usersync.url=//v1/sync/ssp?ssp= +adapters.algorix.enabled=true +adapters.algorix.endpoint=http://localhost:8090/algorix-exchange +adapters.amx.enabled=true +adapters.amx.endpoint=http://localhost:8090/amx-exchange adapters.appnexus.enabled=true adapters.appnexus.endpoint=http://localhost:8090/appnexus-exchange -adapters.appnexus.pbs-enforces-gdpr=true adapters.appnexus.usersync.url=//usersync-url/getuid? +adapters.appnexus.aliases.districtm.enabled=true +adapters.appnexus.aliases.districtm.endpoint=http://localhost:8090/districtm-exchange adapters.applogy.enabled=true adapters.applogy.endpoint=http://localhost:8090/applogy-exchange -adapters.applogy.pbs-enforces-gdpr=true -adapters.applogy.usersync.url=//applogy-usersync adapters.avocet.enabled=true adapters.avocet.endpoint=http://localhost:8090/avocet-exchange -adapters.avocet.pbs-enforces-gdpr=true -adapters.avocet.usersync.url=//avocet-usersync +adapters.axonix.enabled=true +adapters.axonix.endpoint=http://localhost:8090/axonix-exchange adapters.beachfront.enabled=true -adapters.beachfront.endpoint:http://localhost:8090/beachfront-exchange/banner -adapters.beachfront.video-endpoint:http://localhost:8090/beachfront-exchange/video?exchange_id= -adapters.beachfront.platform-id:142 -adapters.beachfront.pbs-enforces-gdpr:true -adapters.beachfront.usersync.url=//beachfront-usersync +adapters.beachfront.endpoint=http://localhost:8090/beachfront-exchange/banner +adapters.beachfront.video-endpoint=http://localhost:8090/beachfront-exchange/video?exchange_id= +adapters.beachfront.platform-id=142 adapters.beintoo.enabled=true adapters.beintoo.endpoint=http://localhost:8090/beintoo-exchange -adapters.beintoo.pbs-enforces-gdpr=true -adapters.beintoo.usersync.url=//beintoo-usersync +adapters.bidscube.enabled=true +adapters.bidscube.endpoint=http://localhost:8090/bidscube-exchange +adapters.bmtm.enabled=true +adapters.bmtm.endpoint=http://localhost:8090/bmtm-exchange adapters.brightroll.enabled=true adapters.brightroll.endpoint=http://localhost:8090/brightroll-exchange -adapters.brightroll.pbs-enforces-gdpr=true -adapters.brightroll.usersync.url=//brightroll-usersync +adapters.connectad.enabled=true +adapters.connectad.endpoint=http://localhost:8090/connectad-exchange adapters.cpmstar.enabled=true adapters.cpmstar.endpoint=http://localhost:8090/cpmstar-exchange -adapters.cpmstar.pbs-enforces-gdpr=true -adapters.cpmstar.usersync.url=//cpmstar-usersync adapters.consumable.enabled=true adapters.consumable.endpoint=http://localhost:8090/consumable-exchange -adapters.consumable.pbs-enforces-gdpr=true -adapters.consumable.usersync.url=//consumable-usersync adapters.conversant.enabled=true adapters.conversant.endpoint=http://localhost:8090/conversant-exchange -adapters.conversant.pbs-enforces-gdpr=true -adapters.conversant.usersync.url=//conversant-usersync +adapters.conversant.generate-bid-id=false +adapters.criteo.enabled=true +adapters.criteo.endpoint=http://localhost:8090/criteo-exchange +adapters.criteo.generate-slot-id=false adapters.datablocks.enabled=true adapters.datablocks.endpoint=http://{{Host}}/datablocks-exchange?sid={{SourceId}} -adapters.datablocks.pbs-enforces-gdpr=true -adapters.datablocks.usersync.url=//datablocks-usersync +adapters.decenterads.enabled=true +adapters.decenterads.endpoint=http://localhost:8090/decenterads-exchange +adapters.deepintent.enabled=true +adapters.deepintent.endpoint=http://localhost:8090/deepintent-exchange adapters.dmx.enabled=true adapters.dmx.endpoint=http://localhost:8090/dmx-exchange -adapters.dmx.pbs-enforces-gdpr=true -adapters.dmx.usersync.url=//dmx-usersync adapters.emxdigital.enabled=true adapters.emxdigital.endpoint=http://localhost:8090/emx_digital-exchange -adapters.emxdigital.pbs-enforces-gdpr=true -adapters.emxdigital.usersync.url=//emxdigital-usersync adapters.engagebdr.enabled=true adapters.engagebdr.endpoint=http://localhost:8090/engagebdr-exchange -adapters.engagebdr.pbs-enforces-gdpr=true -adapters.engagebdr.usersync.url=//engagebdr-usersync adapters.eplanning.enabled=true adapters.eplanning.endpoint=http://localhost:8090/eplanning-exchange -adapters.eplanning.pbs-enforces-gdpr=true -adapters.eplanning.usersync.url=//eplanning-usersync +adapters.epom.enabled=true +adapters.epom.endpoint=http://localhost:8090/epom-exchange +adapters.evolution.enabled=true +adapters.evolution.endpoint=http://localhost:8090/evolution-exchange adapters.facebook.enabled=true adapters.facebook.endpoint=http://localhost:8090/audienceNetwork-exchange -adapters.facebook.pbs-enforces-gdpr=true adapters.facebook.platform-id=101 adapters.facebook.app-secret=67234 -adapters.facebook.usersync.url=//facebook-usersync adapters.gamma.enabled=true adapters.gamma.endpoint=http://localhost:8090/gamma-exchange/ -adapters.gamma.pbs-enforces-gdpr=true -adapters.gamma.usersync.url=//gamma-usersync adapters.gamoshi.enabled=true adapters.gamoshi.endpoint=http://localhost:8090/gamoshi-exchange -adapters.gamoshi.pbs-enforces-gdpr=true -adapters.gamoshi.usersync.url=//gamoshi-usersync adapters.grid.enabled=true adapters.grid.endpoint=http://localhost:8090/grid-exchange -adapters.grid.pbs-enforces-gdpr=true -adapters.grid.usersync.url=//grid-usersync adapters.gumgum.enabled=true adapters.gumgum.endpoint=http://localhost:8090/gumgum-exchange -adapters.gumgum.pbs-enforces-gdpr=true -adapters.gumgum.usersync.url=//gumgum-usersync adapters.improvedigital.enabled=true adapters.improvedigital.endpoint=http://localhost:8090/improvedigital-exchange -adapters.improvedigital.pbs-enforces-gdpr=true -adapters.improvedigital.usersync.url=//improvedigital-usersync adapters.ix.enabled=true adapters.ix.endpoint=http://localhost:8090/ix-exchange -adapters.ix.pbs-enforces-gdpr=true -adapters.ix.usersync.url=//ix-usersync +adapters.jixie.enabled=true +adapters.jixie.endpoint=http://localhost:8090/jixie-exchange adapters.kidoz.enabled=true adapters.kidoz.endpoint=http://localhost:8090/kidoz-exchange -adapters.kidoz.pbs-enforces-gdpr=true -adapters.kidoz.usersync.url=//kidoz-usersync +adapters.colossus.enabled=true +adapters.colossus.endpoint=http://localhost:8090/colossus-exchange +adapters.krushmedia.enabled=true +adapters.kayzen.enabled=true +adapters.kayzen.endpoint=http://localhost:8090/kayzen-exchange +adapters.krushmedia.endpoint=http://localhost:8090/krushmedia-exchange adapters.kubient.enabled=true adapters.kubient.endpoint=http://localhost:8090/kubient-exchange -adapters.kubient.pbs-enforces-gdpr=true -adapters.kubient.usersync.url=//kubient-usersync -adapters.lifestreet.enabled=true -adapters.lifestreet.endpoint=http://localhost:8090/lifestreet-exchange -adapters.lifestreet.pbs-enforces-gdpr=true -adapters.lifestreet.usersync.url=//lifestreet-usersync adapters.lockerdome.enabled=true adapters.lockerdome.endpoint=http://localhost:8090/lockerdome-exchange -adapters.lockerdome.pbs-enforces-gdpr=true -adapters.lockerdome.usersync.url=//lockerdome-usersync +adapters.loopme.enabled=true +adapters.loopme.endpoint=http://localhost:8090/loopme-exchange adapters.logicad.enabled=true adapters.logicad.endpoint=http://localhost:8090/logicad-exchange -adapters.logicad.pbs-enforces-gdpr=true -adapters.logicad.usersync.url=//logicad-usersync adapters.lunamedia.enabled=true adapters.lunamedia.endpoint=http://localhost:8090/lunamedia-exchange?pubid= -adapters.lunamedia.pbs-enforces-gdpr=true -adapters.lunamedia.usersync.url=//lunamedia-usersync +adapters.madvertise.enabled=true +adapters.madvertise.endpoint=http://localhost:8090/madvertise-exchange adapters.marsmedia.enabled=true -adapters.marsmedia.endpoint=http://localhost:8090/marsmedia-exchange -adapters.marsmedia.pbs-enforces-gdpr=true -adapters.marsmedia.usersync.url=//marsmedia-usersync +adapters.marsmedia.endpoint=http://localhost:8090/marsmedia-exchange?param=testParam +adapters.medianet.enabled=true +adapters.medianet.endpoint=http://localhost:8090/medianet-exchange adapters.mgid.enabled=true adapters.mgid.endpoint=http://localhost:8090/mgid-exchange/ -adapters.mgid.pbs-enforces-gdpr=true -adapters.mgid.usersync.url=//mgid-usersync +adapters.mobfoxpb.enabled=true +adapters.mobfoxpb.endpoint=http://localhost:8090/mobfoxpb-exchange?c=__route__&m=__method__&key=__key__ adapters.mobilefuse.enabled=true adapters.mobilefuse.endpoint=http://localhost:8090/mobilefuse-exchange/ -adapters.mobilefuse.pbs-enforces-gdpr=true -adapters.mobilefuse.usersync.url=//mobilefuse-usersync adapters.nanointeractive.enabled=true adapters.nanointeractive.endpoint=http://localhost:8090/nanointeractive-exchange/ -adapters.nanointeractive.pbs-enforces-gdpr=true -adapters.nanointeractive.usersync.url=//nanointeractive-usersync adapters.ninthdecimal.enabled=true adapters.ninthdecimal.endpoint=http://localhost:8090/ninthdecimal-exchange?pubid= -adapters.ninthdecimal.pbs-enforces-gdpr=true -adapters.ninthdecimal.usersync.url=//ninthdecimal-usersync +adapters.nobid.enabled=true +adapters.nobid.endpoint=http://localhost:8090/nobid-exchange?pubid= +adapters.onetag.enabled=true +adapters.onetag.endpoint=http://localhost:8090/onetag-exchange adapters.openx.enabled=true adapters.openx.endpoint=http://localhost:8090/openx-exchange -adapters.openx.pbs-enforces-gdpr=true -adapters.openx.usersync.url=//openx-usersync adapters.orbidder.enabled=true adapters.orbidder.endpoint=http://localhost:8090/orbidder-exchange -adapters.orbidder.pbs-enforces-gdpr=true -adapters.orbidder.usersync.url=//orbidder-usersync +adapters.outbrain.enabled=true +adapters.outbrain.endpoint=http://localhost:8090/outbrain-exchange +adapters.pangle.enabled=true +adapters.pangle.endpoint=http://localhost:8090/pangle-exchange adapters.pubmatic.enabled=true adapters.pubmatic.endpoint=http://localhost:8090/pubmatic-exchange -adapters.pubmatic.pbs-enforces-gdpr=true -adapters.pubmatic.usersync.url=//pubmatic-usersync adapters.pubnative.enabled=true adapters.pubnative.endpoint=http://localhost:8090/pubnative-exchange -adapters.pubnative.pbs-enforces-gdpr=true -adapters.pubnative.usersync.url=//pubnative-usersync adapters.pulsepoint.enabled=true adapters.pulsepoint.endpoint=http://localhost:8090/pulsepoint-exchange -adapters.pulsepoint.pbs-enforces-gdpr=true -adapters.pulsepoint.usersync.url=//pulsepoint-usersync +adapters.revcontent.enabled=true +adapters.revcontent.endpoint=http://localhost:8090/revcontent-exchange adapters.rhythmone.enabled=true adapters.rhythmone.endpoint=http://localhost:8090/rhythmone-exchange -adapters.rhythmone.pbs-enforces-gdpr=true -adapters.rhythmone.usersync.url=//sync.1rx.io/usersync2/rmphb?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&redir= adapters.rtbhouse.enabled=true adapters.rtbhouse.endpoint=http://localhost:8090/rtbhouse-exchange -adapters.rtbhouse.pbs-enforces-gdpr=true -adapters.rtbhouse.usersync.url=//rtbhouse-usersync adapters.rubicon.enabled=true adapters.rubicon.endpoint=http://localhost:8090/rubicon-exchange?tk_xint=rp-pbs -adapters.rubicon.pbs-enforces-gdpr=true adapters.rubicon.XAPI.Username=rubicon_user adapters.rubicon.XAPI.Password=rubicon_password adapters.rubicon.usersync.url=http://localhost:8090/rubicon-usersync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}} +adapters.salunamedia.enabled=true +adapters.salunamedia.endpoint=http://localhost:8090/salunamedia-exchange adapters.smaato.enabled=true adapters.smaato.endpoint=http://localhost:8090/smaato-exchange -adapters.smaato.pbs-enforces-gdpr=true -adapters.smaato.usersync.url=//smaato-usersync adapters.smartadserver.enabled=true adapters.smartadserver.endpoint=http://localhost:8090/smartadserver-exchange -adapters.smartadserver.pbs-enforces-gdpr=true -adapters.smartadserver.usersync.url=//smartadserver-usersync adapters.smartrtb.enabled=true adapters.smartrtb.endpoint=http://localhost:8090/smartrtb-exchange/ -adapters.smartrtb.pbs-enforces-gdpr=true -adapters.smartrtb.usersync.url=//smartrtb-usersync +adapters.smartyads.enabled=true +adapters.smartyads.endpoint=http://localhost:8090/smartyads-exchange +adapters.smilewanted.enabled=true +adapters.smilewanted.endpoint=http://localhost:8090/smilewanted-exchange adapters.somoaudience.enabled=true adapters.somoaudience.endpoint=http://localhost:8090/somoaudience-exchange -adapters.somoaudience.pbs-enforces-gdpr=true -adapters.somoaudience.usersync.url=//somoaudience-usersync adapters.sonobi.enabled=true adapters.sonobi.endpoint=http://localhost:8090/sonobi-exchange -adapters.sonobi.pbs-enforces-gdpr=true -adapters.sonobi.usersync.url=//sonobi-usersync adapters.sovrn.enabled=true adapters.sovrn.endpoint=http://localhost:8090/sovrn-exchange -adapters.sovrn.pbs-enforces-gdpr=true -adapters.sovrn.usersync.url=//sovrn-usersync adapters.synacormedia.enabled=true adapters.synacormedia.endpoint=http://localhost:8090/synacormedia-exchange/{{Host}} -adapters.synacormedia.pbs-enforces-gdpr=true -adapters.synacormedia.usersync.url=//synacormedia-usersync adapters.sharethrough.enabled=true adapters.sharethrough.endpoint=http://localhost:8090/sharethrough-exchange -adapters.sharethrough.pbs-enforces-gdpr=true -adapters.sharethrough.usersync.url=//sharethrough-usersync +adapters.silvermob.enabled=true +adapters.silvermob.endpoint=http://localhost:8090/silvermob-exchange adapters.tappx.enabled=true adapters.tappx.endpoint=http:// -adapters.tappx.pbs-enforces-gdpr=true -adapters.tappx.usersync.url=//tappx-usersync adapters.telaria.enabled=true adapters.telaria.endpoint=http://localhost:8090/telaria-exchange/ -adapters.telaria.pbs-enforces-gdpr=true -adapters.telaria.usersync.url=//telaria-usersync adapters.triplelift.enabled=true adapters.triplelift.endpoint=http://localhost:8090/triplelift-exchange -adapters.triplelift.pbs-enforces-gdpr=true -adapters.triplelift.usersync.url=//triplelift-usersync adapters.tripleliftnative.enabled=true adapters.tripleliftnative.endpoint=http://localhost:8090/triplelift_native-exchange -adapters.tripleliftnative.pbs-enforces-gdpr=true -adapters.tripleliftnative.usersync.url=//triplelift_native-usersync adapters.tripleliftnative.extra-info.publisher_whitelist=[\"test\"] adapters.ttx.enabled=true adapters.ttx.endpoint=http://localhost:8090/ttx-exchange adapters.ttx.partner-id=partner -adapters.ttx.pbs-enforces-gdpr=true -adapters.ttx.usersync.url=//ttx-usersync +adapters.ttx.aliases.33across.enabled=true adapters.ucfunnel.enabled=true adapters.ucfunnel.endpoint=http://localhost:8090/ucfunnel-exchange -adapters.ucfunnel.pbs-enforces-gdpr=true -adapters.ucfunnel.usersync.url=//ucfunnel-usersync +adapters.unicorn.enabled=true +adapters.unicorn.endpoint=http://localhost:8090/unicorn-exchange +adapters.between.enabled=true +adapters.between.endpoint=http://localhost:8090/between-exchange?host={{Host}}&pubId={{PublisherId}} +adapters.bidmachine.enabled=true +adapters.bidmachine.endpoint=http://localhost:8090/bidmachine-exchange +adapters.bidmyadz.enabled=true +adapters.bidmyadz.endpoint=http://localhost:8090/bidmyadz-exchange adapters.unruly.enabled=true adapters.unruly.endpoint=http://localhost:8090/unruly-exchange -adapters.unruly.pbs-enforces-gdpr=true -adapters.unruly.usersync.url=//unruly-usersync adapters.valueimpression.enabled=true adapters.valueimpression.endpoint=http://localhost:8090/valueimpression-exchange -adapters.valueimpression.pbs-enforces-gdpr=true -adapters.valueimpression.usersync.url=//valueimpression-usersync adapters.verizonmedia.enabled=true adapters.verizonmedia.endpoint=http://localhost:8090/verizonmedia-exchange -adapters.verizonmedia.pbs-enforces-gdpr=true -adapters.verizonmedia.usersync.url=//verizonmedia-usersync adapters.vrtcal.enabled=true adapters.vrtcal.endpoint=http://localhost:8090/vrtcal-exchange -adapters.vrtcal.pbs-enforces-gdpr=true -adapters.vrtcal.usersync.url=//vrtcal-usersync adapters.visx.enabled=true adapters.visx.endpoint=http://localhost:8090/visx-exchange -adapters.visx.pbs-enforces-gdpr=true -adapters.visx.usersync.url=//visx-usersync adapters.inmobi.enabled=true adapters.inmobi.endpoint=http://localhost:8090/inmobi-exchange -adapters.inmobi.pbs-enforces-gdpr=true -adapters.inmobi.usersync.url=//inmobi-usersync +adapters.interactiveoffers.enabled=true +adapters.interactiveoffers.endpoint=http://localhost:8090/interactiveoffers-exchange +adapters.invibes.enabled=true +adapters.invibes.endpoint=http://localhost:8090/invibes-exchange adapters.yeahmobi.enabled=true adapters.yeahmobi.endpoint=http://localhost:8090/yeahmobi-exchange -adapters.yeahmobi.pbs-enforces-gdpr=true -adapters.yeahmobi.usersync.url=//yeahmobi-usersync adapters.yieldlab.enabled=true adapters.yieldlab.endpoint=http://localhost:8090/yieldlab-exchange -adapters.yieldlab.pbs-enforces-gdpr=true -adapters.yieldlab.usersync.url=//yieldlab-usersync adapters.yieldmo.enabled=true adapters.yieldmo.endpoint=http://localhost:8090/yieldmo-exchange -adapters.yieldmo.pbs-enforces-gdpr=true -adapters.yieldmo.usersync.url=//yieldmo-usersync adapters.yieldone.enabled=true adapters.yieldone.endpoint=http://localhost:8090/yieldone-exchange -adapters.yieldone.pbs-enforces-gdpr=true -adapters.yieldone.usersync.url=//yieldone-usersync adapters.zeroclickfraud.enabled=true adapters.zeroclickfraud.endpoint=http://{{Host}}/zeroclickfraud-exchange?sid={{SourceId}} -adapters.zeroclickfraud.pbs-enforces-gdpr=true -adapters.zeroclickfraud.usersync.url=//zeroclickfraud-usersync http-client.circuit-breaker.enabled=true http-client.circuit-breaker.opening-threshold=1 http-client.circuit-breaker.opening-interval-ms=1000 @@ -330,7 +259,7 @@ default-timeout-ms=2000 timeout-adjustment-ms=0 auction.default-timeout-ms=2000 auction.timeout-adjustment-ms=0 -auction.id-generator-type=none +auction.generate-source-tid=false currency-converter.external-rates.enabled=true currency-converter.external-rates.url=http://localhost:8090/currency-rates amp.default-timeout-ms=2000 @@ -372,9 +301,15 @@ admin-endpoints.logging-httpinteraction.enabled=true admin-endpoints.logging-httpinteraction.protected=false admin-endpoints.logging-changelevel.enabled=true admin-endpoints.logging-changelevel.protected=false +admin-endpoints.tracelog.enabled=true +admin-endpoints.tracelog.protected=false +admin-endpoints.e2eadmin.enabled=false +admin-endpoints.lineitem-status.enabled=false +admin-endpoints.deals-status.enabled=false status-response=ok analytics.log.enabled=true gdpr.host-vendor-id=1 +gdpr.default-value=1 gdpr.vendorlist.v1.cache-dir=src/test/resources/org/prebid/server/it/gdpr-vendorlist1 gdpr.vendorlist.v2.cache-dir=src/test/resources/org/prebid/server/it/gdpr-vendorlist2 ccpa.enforce=false diff --git a/src/test/resources/org/prebid/server/it/uid/test-uid-response.json b/src/test/resources/org/prebid/server/it/uid/test-uid-response.json new file mode 100644 index 00000000000..85c3341ce56 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/uid/test-uid-response.json @@ -0,0 +1,6 @@ +{ + "buyeruids": { + "rubicon": "J5VLCWQP-26-CWFT", + "adnxs": "12345" + } +} \ No newline at end of file diff --git a/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/changed-put-bid-request.json b/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/changed-put-bid-request.json new file mode 100644 index 00000000000..ab4b4d39252 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/changed-put-bid-request.json @@ -0,0 +1,136 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "a121a07f-1579-4465-bc5e-5c5b02a0c421", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.9, + "wurl": "http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c180", + "impid": "impIdWasChanged", + "crid": "crid1", + "price": 0.8, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c181", + "impid": "impStoredBidResponse", + "crid": "crid1", + "price": 0.8, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c181&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + } + }, + { + "type": "json", + "value": { + "id": "928185755156387460", + "impid": "impId131", + "price": 1, + "adm": "{\"assets\":[{\"id\":0,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 3 + } + }, + "wurl": "http://localhost:8080/event?t=win&b=928185755156387460&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "7706636740145184840", + "impid": "impId3", + "price": 5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "cat": [], + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs", + "ext": { + "appnexus": { + "brand_id": 350, + "brand_category_id": 350, + "auction_id": 8189378542222915031, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + } + } + } + }, + { + "type": "json", + "value": { + "id": "466223845", + "impid": "impId2", + "price": 4.26, + "adm": "adm2", + "crid": "crid2", + "w": 300, + "h": 600, + "wurl": "http://localhost:8080/event?t=win&b=466223845&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "xml", + "value": "", + "expiry": 120 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/missing-put-bid-request.json b/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/missing-put-bid-request.json new file mode 100644 index 00000000000..d72ed026ec2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/missing-put-bid-request.json @@ -0,0 +1,134 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "466223845", + "impid": "impId2", + "price": 4.26, + "adm": "adm2", + "crid": "crid2", + "w": 300, + "h": 600, + "wurl": "http://localhost:8080/event?t=win&b=466223845&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "7706636740145184841", + "impid": "impId3", + "price": 5.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + } + } + } + }, + { + "type": "json", + "value": { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + } + }, + { + "type": "json", + "value": { + "id": "928185755156387460", + "impid": "impId131", + "price": 1, + "adm": "{\"assets\":[{\"id\":0,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 3 + } + }, + "wurl": "http://localhost:8080/event?t=win&b=928185755156387460&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "a121a07f-1579-4465-bc5e-5c5b02a0c421", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.9, + "wurl": "http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c180", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.8, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c181", + "impid": "impStoredBidResponse", + "crid": "crid1", + "price": 0.8, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c181&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "xml", + "value": "", + "expiry": 120 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/original-bid-request.json b/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/original-bid-request.json new file mode 100644 index 00000000000..10550f10900 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/original-bid-request.json @@ -0,0 +1,164 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "a121a07f-1579-4465-bc5e-5c5b02a0c421", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.9, + "wurl": "http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "7706636740145184841", + "impid": "impId3", + "price": 5.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + } + } + } + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c180", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.8, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c181", + "impid": "impStoredBidResponse", + "crid": "crid1", + "price": 0.8, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c181&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + } + }, + { + "type": "json", + "value": { + "id": "928185755156387460", + "impid": "impId131", + "price": 1, + "adm": "{\"assets\":[{\"id\":0,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 3 + } + }, + "wurl": "http://localhost:8080/event?t=win&b=928185755156387460&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "7706636740145184840", + "impid": "impId3", + "price": 5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "cat": [], + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs", + "ext": { + "appnexus": { + "brand_id": 350, + "brand_category_id": 350, + "auction_id": 8189378542222915031, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + } + } + } + }, + { + "type": "json", + "value": { + "id": "466223845", + "impid": "impId2", + "price": 4.26, + "adm": "adm2", + "crid": "crid2", + "w": 300, + "h": 600, + "wurl": "http://localhost:8080/event?t=win&b=466223845&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "xml", + "value": "", + "expiry": 120 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/redundant-put-bid-request.json b/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/redundant-put-bid-request.json new file mode 100644 index 00000000000..6bc682d5835 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/redundant-put-bid-request.json @@ -0,0 +1,174 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "a121a07f-1579-4465-bc5e-5c5b02a0c421", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.9, + "wurl": "http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "466223845", + "impid": "impId2", + "price": 4.26, + "adm": "adm2", + "crid": "crid2", + "w": 300, + "h": 600, + "wurl": "http://localhost:8080/event?t=win&b=466223845&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "7706636740145184840", + "impid": "impId3", + "price": 5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "cat": [], + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs", + "ext": { + "appnexus": { + "brand_id": 350, + "brand_category_id": 350, + "auction_id": 8189378542222915031, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + } + } + } + }, + { + "type": "json", + "value": { + "id": "7706636740145184841", + "impid": "impId3", + "price": 5.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + } + } + } + }, + { + "type": "json", + "value": { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + } + }, + { + "type": "json", + "value": { + "id": "928185755156387460", + "impid": "impId131", + "price": 1, + "adm": "{\"assets\":[{\"id\":0,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 3 + } + }, + "wurl": "http://localhost:8080/event?t=win&b=928185755156387460&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "a121a07f-1579-4465-bc5e-5c5b02a0c421", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.9, + "wurl": "http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c180", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.8, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c181", + "impid": "impStoredBidResponse", + "crid": "crid1", + "price": 0.8, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c181&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "xml", + "value": "", + "expiry": 120 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/unordered-bid-request.json b/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/unordered-bid-request.json new file mode 100644 index 00000000000..fdacf300534 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/util/bidcacherequestpattern/unordered-bid-request.json @@ -0,0 +1,164 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "466223845", + "impid": "impId2", + "price": 4.26, + "adm": "adm2", + "crid": "crid2", + "w": 300, + "h": 600, + "wurl": "http://localhost:8080/event?t=win&b=466223845&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "7706636740145184840", + "impid": "impId3", + "price": 5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "cat": [], + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184840&a=5001&ts=1000&bidder=appnexusAlias&f=i&int=dmbjs", + "ext": { + "appnexus": { + "brand_id": 350, + "brand_category_id": 350, + "auction_id": 8189378542222915031, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + } + } + } + }, + { + "type": "json", + "value": { + "id": "7706636740145184841", + "impid": "impId3", + "price": 5.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs", + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 0, + "ranking_price": 0.0 + } + } + } + }, + { + "type": "json", + "value": { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs", + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + } + }, + { + "type": "json", + "value": { + "id": "928185755156387460", + "impid": "impId131", + "price": 1, + "adm": "{\"assets\":[{\"id\":0,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 3 + } + }, + "wurl": "http://localhost:8080/event?t=win&b=928185755156387460&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "a121a07f-1579-4465-bc5e-5c5b02a0c421", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.9, + "wurl": "http://localhost:8080/event?t=win&b=a121a07f-1579-4465-bc5e-5c5b02a0c421&a=5001&ts=1000&bidder=appnexus&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c180", + "impid": "impStoredAuctionResponse", + "crid": "crid1", + "price": 0.8, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c180&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "json", + "value": { + "id": "f227a07f-1579-4465-bc5e-5c5b02a0c181", + "impid": "impStoredBidResponse", + "crid": "crid1", + "price": 0.8, + "wurl": "http://localhost:8080/event?t=win&b=f227a07f-1579-4465-bc5e-5c5b02a0c181&a=5001&ts=1000&bidder=rubicon&f=i&int=dmbjs" + } + }, + { + "type": "xml", + "value": "", + "expiry": 120 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/vtrack/test-cache-request.json b/src/test/resources/org/prebid/server/it/vtrack/test-cache-request.json index e74d57329c1..ca96583ef8c 100644 --- a/src/test/resources/org/prebid/server/it/vtrack/test-cache-request.json +++ b/src/test/resources/org/prebid/server/it/vtrack/test-cache-request.json @@ -2,7 +2,7 @@ "puts": [ { "type": "xml", - "value": "prebid.org wrapper", + "value": "prebid.org wrapper", "ttlseconds": 3600 } ] diff --git a/src/test/resources/org/prebid/server/it/vtrack/test-cache-response.json b/src/test/resources/org/prebid/server/it/vtrack/test-cache-response.json new file mode 100644 index 00000000000..f3d51aac630 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/vtrack/test-cache-response.json @@ -0,0 +1,7 @@ +{ + "responses": [ + { + "uuid": "94531ab8-c662-4fc7-904e-6b5d3be43b1a" + } + ] +} diff --git a/src/test/resources/org/prebid/server/handler/version/empty.json b/src/test/resources/org/prebid/server/util/resource/version/empty.json similarity index 100% rename from src/test/resources/org/prebid/server/handler/version/empty.json rename to src/test/resources/org/prebid/server/util/resource/version/empty.json diff --git a/src/test/resources/org/prebid/server/handler/version/version.json b/src/test/resources/org/prebid/server/util/resource/version/version.json similarity index 100% rename from src/test/resources/org/prebid/server/handler/version/version.json rename to src/test/resources/org/prebid/server/util/resource/version/version.json diff --git a/src/test/resources/org/prebid/server/validation/schema/invalid/rubicon.json b/src/test/resources/org/prebid/server/validation/schema/invalid/rubicon.json index 0079b4102c2..985d2d7592f 100644 --- a/src/test/resources/org/prebid/server/validation/schema/invalid/rubicon.json +++ b/src/test/resources/org/prebid/server/validation/schema/invalid/rubicon.json @@ -1 +1 @@ -} 12345 : } \ No newline at end of file +} 12345: } \ No newline at end of file